讲解
把值传给函数就要交出所有权,用完还得还回来——太啰嗦。引用(reference)解决了这个问题:&s 创建对 s 的引用,函数通过引用「借用」值,所有权纹丝不动。形如 fn len(s: &String) -> usize 的签名读作「借一个 String 用用,看完就还」。默认的引用是只读的,借来不能改。
想改要用可变引用 &mut s,函数签名 fn append(s: &mut String)。但可变引用附带一条铁律:同一作用域、同一时刻,对同一个值要么存在任意多个不可变引用,要么存在且只存在一个可变引用——两者不能共存。这条规则在编译期消灭了数据竞争:没有人会正在读的同时另一个人在改。注意「同一时刻」按引用的最后一次使用计算(非词法生命周期,NLL),先用完不可变引用再创建可变引用是合法的。
借用检查器还堵死了悬垂引用:函数返回指向局部变量的引用必然编译失败,因为局部变量在函数结束时已释放,引用会指向无效内存。编译器要求所有引用始终指向有效数据——其他语言里的 use-after-free 这类线上事故,在 Rust 里连编译都过不去。
示例
不可变引用:借用而不占有:
fn calculate_length(s: &String) -> usize {
s.len()
} // s 是引用,不拥有值,离开作用域什么也不释放
fn main() {
let text = String::from("借用一下");
let len = calculate_length(&text);
// text 所有权没动,继续正常使用
println!("「{text}」的长度是 {len} 字节");
assert_eq!(len, 12);
}
可变引用:借来修改:
fn add_suffix(s: &mut String) {
s.push_str(",世界");
}
fn main() {
let mut greeting = String::from("你好");
add_suffix(&mut greeting);
// NLL:不可变借用在最后一次使用后结束,之后可以再借可变的
let r1 = &greeting;
println!("r1 看到:{r1}");
let r2 = &mut greeting;
r2.push_str("!");
println!("r2 改完:{r2}");
assert_eq!(greeting, "你好,世界!");
}
同时持有两个可变引用会被编译器拦下(构建脚本验证这段确实编译失败):
// compile-fail: 同时存在两个可变引用
fn main() {
let mut s = String::from("数据");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1} {r2}");
}
报错大意(E0499,第二次可变借用越界):
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> main.rs:4:14
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | println!("{r1} {r2}");
| ---- first borrow later used here
常见坑
- & 和 &mut 同时存活:let a = &s; let b = &mut s; println!("{a}") 编译失败——读者和作者不能同时在场。先用完读者,再请作者。
- 以为规则是「词法作用域」:NLL 按最后一次使用计算借用有效期,所以 { let r = &mut s; use(r); } 之后再借是合法的,不必强行用小括号圈作用域。
- 函数返回局部变量的引用:fn f() -> &String { let s = ...; &s } 是悬垂引用,编译必挂;返回 owned 值(String)才是出路,生命周期章节再细说。
- 给 Copy 类型传引用:传 &i32 给函数是画蛇添足——i32 本来就复制,直接按值传更简洁。
小结
& 借用只读、&mut 借用可改;铁律:多读或单写,不可兼得;悬垂引用编译期根除。引用让所有权留在原地,是 Rust 函数的默认沟通方式。下一章看引用的近亲:切片。