可变引用规则
本教程共 78 篇 · 第 23 篇 · 更新于 2026-08-08 · 约 11 分钟阅读
本节目标:用
&mut创建可变引用去修改借用的值,吃透那条核心铁律——同一作用域里可变引用与不可变引用不能共存,理解它如何消灭数据竞争,并认识引用活跃区间与悬垂引用。
上一章的引用是只读的。可现实里函数常要”借过来改一改”,比如往字符串里追加内容。Rust 用可变引用 &mut 支持这个,但给它上了很严的规矩。这章讲可变引用和那条著名的”借用规则”。这条规则看着束缚,实则是 Rust 给你的最大安全红利。
23-1 创建可变引用
要先让原值本身可变(let mut),再取 &mut:
fn main() {
let mut s = String::from("hello");
change(&mut s); // 借出可变引用
println!("{s}"); // 输出 hello, world
}
fn change(text: &mut String) {
text.push_str(", world"); // 通过可变引用修改
}
let mut s 声明 s 可变;&mut s 借出可变引用;函数里 text.push_str 真把内容改了。调用结束,s 带着改动回到 main。所有权没动,只是”借去改了一下”。注意:原值必须 mut,引用才能是 &mut,两者要配套。
23-2 铁律:可变与不可变不能共存
Rust 规定,在同一作用域内,对同一个值:
- 要么只能有一个可变引用
&mut; - 要么可以有多个不可变引用
&; - 二者不能同时存在。
下面会报错:
let mut s = String::from("hi");
let r1 = &s; // 不可变借用
let r2 = &mut s; // 错!已有不可变借用时,不能再借可变
println!("{r1} {r2}");
编译器拒绝:你既然已经让人”只读看着”,就不能又让人”动手改”,否则看的人可能读到一半数据变了,产生不一致。这是”读写互斥”的直接体现。
Warning这条规则是 Rust “无畏并发”的本地版基础。它直接消灭了”数据竞争”——多个引用抢改同一份数据导致的混乱。刚学时觉得限制多,习惯后你会感谢它替你挡掉的 bug。
23-3 同一时刻只能一个可变引用
即便没有不可变引用掺和,同一个值在同一作用域也只能有一个 &mut:
let mut s = String::from("hi");
let a = &mut s;
let b = &mut s; // 错!不能有两个可变引用
println!("{a} {b}");
为什么?两个都能改的引用同时存在,你改我也改,谁也不知道最终状态,典型的并发灾难源头。Rust 在编译期就禁掉。写作时如果真需要多处修改,通常是按顺序、错开区间来借,而不是同时持有两个 &mut。
23-4 作用域不重叠就放行(NLL)
上面的限制针对”同一作用域同时活跃”。如果引用的使用区间不重叠,编译器是允许的:
let mut s = String::from("hi");
{
let a = &mut s;
a.push_str("1");
} // a 在这里最后一次使用,之后失效
let b = &mut s; // OK,a 已不在作用域
b.push_str("2");
因为 a 在 b 创建前就”用完退休”了,两者从不同时活跃。Rust 的借用检查器很聪明,判断的是”引用的活跃区间”(从创建到最后一次使用),而不是死板地看花括号。这个优化叫 NLL(Non-Lexical Lifetimes,非词法生命周期)。所以只要错开使用,就能反复借可变。
Note判断标准不是”有没有声明两个 &mut”,而是”它们是否在同一时刻都还活着、都能被用到”。只在各自的小范围内用,互不打架,编译器就放行。新版编译器(远早于本书的 1.97.1)早已支持 NLL,你几乎感受不到它的存在。
23-5 悬垂引用的再警示
不管可变还是不可变引用,都不能指向已释放的值:
fn dangle() -> &String { // 错!
let s = String::from("temp");
&s // 返回指向局部变量的引用
} // s 在这里 drop,返回的引用悬垂
这个函数想返回对局部变量 s 的引用,但 s 一离开函数就释放,引用成了悬垂。编译器直接拒绝这种签名。正确做法是返回拥有的 String(移动出来),而不是引用。这是”借用不能比所有者活得久”的体现。
Tip编译器报 “missing lifetime specifier” 或 “borrows value with lifetime…”,多半是你返回了引用、却没说明它和谁的生命周期绑定。新手阶段最简单的修复:别返回引用,改成返回拥有的值(返回
String而非&String)。生命周期深入内容属于进阶,先会用这招顶住。
23-6 一个完整对照表
| 场景 | 是否允许 | 原因 |
|---|---|---|
多个 &T(不可变) | 允许 | 都只读,互不干扰 |
一个 &mut T(可变) | 允许 | 唯一改写者 |
多个 &mut T 同时活跃 | 禁止 | 防数据竞争 |
&T 与 &mut T 同时活跃 | 禁止 | 读的不希望被写中途改 |
23-7 一个真实的借用冲突与修复
看一个借用检查器实际拦下错误的例子,以及怎么改。假设你想同时持有一个可变引用去修改、又一个不可变引用去读:
let mut s = String::from("hi");
let r1 = &s; // 不可变借用
let r2 = &mut s; // 错!已有不可变借用时不能再借可变
println!("{r1} {r2}");
编译器拒绝 r2,因为 r1 还在使用区间里(后面 println! 要用它),这时再来可变借用会破坏”读的人看到一致数据”的保证。改法是让不可变借用先用完:
let mut s = String::from("hi");
{
let r1 = &s;
println!("{r1}"); // r1 在这里最后一次使用
} // r1 离开作用域,借用结束
let r2 = &mut s; // 现在 OK
r2.push_str("!");
把 r1 的使用局限在更小的块里,它一退休,r2 就能借。这正是”引用的活跃区间不重叠就放行”的体现。写代码时若编译器报借用冲突,先想”是不是两个引用活得太久了”,缩小其中一个的作用域往往就解决。
Warning别为了绕过借用检查而胡乱加
clone()。clone 能”复制一份”让两个引用各自持有副本,从而避开门规则,但它有性能成本、还掩盖设计问题。优先用”调整作用域/拆分逻辑”解决冲突,clone 留作最后手段。
23-8 借用规则与并发的关系
这套”同一时刻要么一个可变、要么多个不可变”的规则,其实正是 Rust “无畏并发”的本地版根基。多线程里最经典的 bug 是”数据竞争”——多个线程同时改同一份数据、且至少有一方在写,结果不可预测。Rust 的借用规则在编译期就禁止这种同时存在的读写,于是单线程里的这套检查,天然能推广到多线程,让并发代码在编译期就安全。
Note你现阶段只写单线程代码,体会不到这条规则对并发的意义。但只要记住:今天学的借用规则不是随便定的,它是 Rust 整套安全保证(包括并发安全)的一块基石。后面学多线程时,你会发现”编译期就拦下数据竞争”有多省心。
23-9 小结
&mut 可变引用能改值,但受铁律约束:同一作用域要么一个可变、要么多个不可变,不能混存。这条规则在编译期根除了数据竞争。它看着约束多,实则是 Rust 给你的最大安全红利。下一章讲”切片(Slice)“——一种不持有所有权、只指向集合中一段的引用,字符串处理里天天用。