Rust 借用与引用
上一篇讲了所有权,你可能会问:"每次调用函数都要丢一个变量,太麻烦了吧?"确实。所以 Rust 提供了借用(borrow)机制:用引用(reference)让你"借"值用,不拿走所有权。这是日常 Rust 编程 90% 的场景,务必掌握。
1. 引用:借而不取
引用用 & 操作符创建。把一个变量的引用传给函数,函数借用了它的值,用完还回来——原变量在调用后仍然有效:
fn main() {
let s = String::from("hello");
// 借用:传引用(用 & 操作符),不转移所有权
let len = calculate_length(&s);
println!("'{}' 的长度是 {}", s, len); // s 仍然能用!
}
// 参数 s: &String 表示"借用一个 String"
// 函数用完不释放原数据(因为不拥有它)
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开作用域,但因为它是引用,什么都不会释放
// 对比:不用引用时(上一篇的写法)
// fn calc(s: String) -> usize { s.len() }
// 调用后原 s 失效——很麻烦关键点:
- & 表示引用:可以理解为"指向 s 的指针",但不转移所有权。
- 函数参数
&T:表示"借用一个 T"。 - 函数返回时:因为不拥有数据,不会释放——这正是借用想要的效果。
引用在大多数情况下像普通值一样使用——Rust 会自动解引用(叫 deref coercion),你不需要写满星号。
2. 可变引用 &mut
默认的引用是不可变的——你只能读不能改。要修改借来的值,用可变引用 &mut:
fn main() {
let mut s = String::from("hello");
// 可变借用:&mut,允许函数修改借来的值
append_world(&mut s);
println!("{}", s); // hello world
// 注意:要可变借用,原变量本身必须声明 mut
// let s = String::from("hi");
// change(&mut s); // 编译错误!s 不可变
}
fn append_world(s: &mut String) {
s.push_str(" world"); // 修改借来的值
}
// 默认借用(&)是只读的,函数不能改
fn show(s: &String) {
// s.push_str("x"); // 编译错误!不可变借用不能修改
println!("{}", s);
}几个细节:
- 原变量必须声明 mut:要可变借用,值本身必须可变。
- &mut 是独占的:同一时刻只能有一个可变引用(下面详讲)。
- 不可变借用不能修改:这是编译期保证。
3. 借用检查器的铁律
这是借用系统的核心规则,也是"无畏并发"的根源:
fn main() {
let mut s = String::from("hello");
// 借用检查器的铁律:
// 同一时刻,要么有【多个不可变借用】,要么有【一个可变借用】
// 两者不能同时存在
// 情况一:多个不可变借用,OK(只读共享)
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1、r2 此后不再使用(NLL 优化)
// 情况二:一个可变借用,OK
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
// 情况三(错误):同时存在可变与不可变借用
// let r1 = &s;
// let r2 = &mut s; // 编译错误!
// println!("{} {}", r1, r2);
// 为什么这样设计?
// 不可变借用承诺"不会变",可变借用会修改,二者冲突
// 编译期阻止 -> 数据竞争从根源上无法发生(无畏并发!)
}一句话总结:同一时刻,要么多个不可变借用(共享只读),要么一个可变借用(独占可写),两者不能共存。这条规则让数据竞争在编译期就被消灭——这是 Rust 区别于所有语言的核心价值。
新手会觉得它"太严格",但习惯后你会发现:它消灭了多线程编程里最痛的 bug 类型(运行时偶发的数据竞争)。代价是少数情况下要重构代码,但收益是绝对的安全。
4. NLL:更智能的生命周期
Rust 2018 之后引入了 NLL(Non-Lexical Lifetimes,非词法生命周期),让借用检查更智能——引用的作用域算到最后一次使用为止,而不是花括号结束:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 又一个不可变借用
println!("{} {}", r1, r2);
// r1、r2 的"最后一次使用"在此结束
// NLL(Non-Lexical Lifetimes,非词法生命周期):
// Rust 2018 后,引用的作用域到【最后一次使用】为止
// 而不是到【花括号结束】为止
// 所以这里可以可变借用,不冲突!
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
// 在 NLL 之前(2018 之前),这会编译错误
// 因为 r1/r2 的生命周期被算到 main 结束
// NLL 让借用检查器更智能,代码更自然
}在 NLL 之前(2018 之前的 Rust),上面这段代码会编译错误,因为 r1/r2 的生命周期被机械地算到 main 结束。NLL 让编译器理解"r1、r2 在 println! 之后就没用了",从而允许后面的可变借用。这让现代 Rust 代码写起来自然得多。
5. 悬垂引用防护
Rust 永远不会出现悬垂引用(dangling reference)——引用指向已释放的内存。这是编译期保证的:
fn main() {
// Rust 永远不会出现"悬垂引用"
// 引用的生命周期不能超过它指向的数据
// 这段代码编译错误:
// fn dangle() -> &String {
// let s = String::from("hi");
// &s
// } // s 离开作用域被释放,引用指向无效内存!
// Rust 编译器会拒绝,提示 "missing lifetime specifier"
// 解决方案:直接返回 String(转移所有权给调用者)
fn no_dangle() -> String {
let s = String::from("hi");
s // 返回所有权,而不是引用
}
let s = no_dangle();
println!("{}", s); // hi
}
// 引用规则总结:
// 1. 任意时刻,要么多个 &,要么一个 &mut
// 2. 引用必须始终指向有效的数据(不能悬垂)Rust 编译器会扫描所有引用的生命周期,确保它们不会比指向的数据活得更久。如果违反,直接编译错误。这是"内存安全"承诺的关键一环。
当函数返回引用时,编译器需要确认引用的目标在函数返回后仍然有效。复杂场景下你需要显式标注生命周期参数(如 &'a str),这是进阶话题,这篇先不展开。日常代码大多不需要手写生命周期标注,编译器自动推断。
6. 切片:特殊的引用
切片(slice)是一种特殊的引用,指向集合的连续一段。最常见的是字符串切片 &str:
fn main() {
// 切片(slice)是一种特殊的引用,指向集合的连续一段
// 字符串切片类型写作 &str
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..11]; // "world"
// 简写:从 0 开始可以省略,到末尾可以省略
let hello = &s[..5]; // 同上
let whole = &s[..]; // 整个字符串
println!("{} {}", hello, world);
// 字符串字面量的类型就是 &str
// 它是"指向程序只读区"的切片,不可变
let literal: &str = "我是一个字面量";
// 数组也有切片
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
println!("{:?}", slice);
}
// 字符串字面量 &str 和 String 的关系:
// String 是堆上可变的字符串(拥有所有权)
// &str 是字符串切片(借用,不可变)
// String 可以"降级"成 &str(&s),
// &str 也可以升级成 String(s.to_string() 或 s.to_owned())几个要点:
- 切片是"胖指针":包含起始指针 + 长度,不拥有数据。
- 字符串字面量类型就是
&str,指向程序只读区。 - String 可以"降级"为 &str(用
&s),&str 可以"升级"为 String(用to_string())。下一篇字符串会详讲。 - 数组也有切片:
&[i32]指向数组的一段。
7. 借用的最佳实践
- 优先用 &T:函数参数默认用不可变引用,避免所有权转移。
- 需要修改才用 &mut T:且确保只有一个可变引用。
- 需要拥有才用 T(按值):当函数要"接管"数据时(比如把它存进结构体)。
- 谨慎 clone:不要为了"绕开所有权"就无脑 clone,通常有更优的引用方案。
8. 一个实战例子:函数同时返回值和引用
借用让函数可以"读"数据但还给原所有者。看一个常用模式——返回一个不拥有数据的切片:
- 接收
&String,返回&str(借用返回)。编译器需要生命周期标注,但大多能自动推断。 - 这种"借用进、借用出"的模式非常常见,Rust 会自动推断最严格的生命周期约束。
9. 借用错误怎么读
遇到借用检查器报错时,关键是读懂错误信息。常见格式:
- "cannot borrow X as mutable because it is also borrowed as immutable":存在不可变借用时又可变借用。解决:让不可变借用的最后使用提前结束(NLL),或重构。
- "cannot borrow X as mutable more than once":同时有两个可变借用。解决:合并逻辑,或让第一个可变借用先结束。
- "borrowed value does not live long enough":引用比目标活得长。解决:调整变量顺序,或返回拥有所有权的值。
Rust 编译器的错误信息会画图指出每个引用的作用域,仔细看就能理解。多练几次,大脑会逐渐建立"借用直觉"。
小结
这一篇你掌握了 Rust 借用系统的全部要点:引用 &(只读借用)、可变引用 &mut(独占可写)、借用规则(多个 & 或一个 &mut,不可同时)、NLL(更智能的作用域)、悬垂引用防护、切片(&str、&[T])。借用的铁律是"无畏并发"的根基——它让数据竞争在编译期就被消灭。
所有权 + 借用是 Rust 的全部灵魂。如果你这两篇读得有点晕,别担心——这是正常的"撞墙期"。下一篇字符串会通过具体场景巩固这些概念,你会看到 String 和 &str 在实际代码里如何配合所有权和借用。
← 上一篇 Rust 所有权(重点)
下一篇 Rust 字符串 →