首页 / Rust 入门教程 / 可变引用规则

Rust 入门教程

可变引用规则

本教程共 78 篇 · 第 23 篇 · 更新于 2026-08-08 · 约 11 分钟阅读

RustRust 入门教程可变引用mutable reference借用规则数据竞争悬垂引用NLL

本节目标:用 &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");

因为 ab 创建前就”用完退休”了,两者从不同时活跃。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)“——一种不持有所有权、只指向集合中一段的引用,字符串处理里天天用。