Skip to content

借用与引用

&&mut 在不转移所有权的情况下使用数据,以及生命周期初步。

引用与借用

借用的动机

如果每次「只是读一下」都要 move,函数签名会变成 fn f(s: String) -> String 那样的往返接力。 引用(reference)解决了这个问题:看一眼,但不拿走

rust
fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);      // 把 &s 借出去,s 仍然是所有者
    println!("'{s}' 的长度是 {len}。");   // s 依然可用
}

fn calculate_length(s: &String) -> usize {
    s.len()
}   // s 是一个引用,离开作用域时不会 drop 任何堆内存

输出:'hello' 的长度是 5。

  借用(borrow)的内存图

  main 栈帧                              calculate_length 栈帧
  ┌────────────────────────┐            ┌────────────────────────────┐
  │ s ──[ptr|5|5]──────────┼──┐         │ s ──[ptr 指向 main 的 s]───┼──┐
  └────────────────────────┘  │         └────────────────────────────┘  │
                              │         引用本身也是栈上的一个指针      │
                              │                                        │
                              └────────────────────────────────────────┘
                                 两个 s 名字,一个所有者,一个借用者

💡 对照:借用 ≈ C++ 的 const T&,但更强:C++ 的 const& 只保证不走这个引用改, 不阻止别人(包括原对象自己)改。Rust 的 &T 在借用存活期间真的锁住了可变访问 —— 由编译器强制。

&T&mut T

rust
fn main() {
    let mut s = String::from("hello");

    let r = &s;                      // 不可变引用(shared reference),可以同时有多个
    let r2 = &s;
    println!("{r} {r2}");            // 输出:hello hello

    let m = &mut s;                  // 可变引用(mutable reference),必须独占
    m.push_str(", world");
    println!("{m}");                 // 输出:hello, world
}

改内容需要 &mut T,而 &mut T 要求变量本身先声明为 mut

rust
fn main() {
    let s = String::from("hello");
    change(&s);                 // 编译失败
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
// error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
// help: consider changing this to be mutable
//   3 |     let mut s = String::from("hello");

💡 对照:Java 的方法参数引用与 Python 的对象引用都是永远可变的, 想表达「只读」只能靠约定(final 只锁引用本身,不锁内容)。 Rust 把「只读」和「可写」编码进类型 &T / &mut T,调用方一眼看得出。

借用规则:读-写互斥

在任意给定时刻,对同一个值:

   要么  ┌────────────────────────────────────────────┐
         │ 任意多个不可变引用 &T (读者可以很多)      │
         └────────────────────────────────────────────┘
   要么  ┌────────────────────────────────────────────┐
         │ 恰好一个可变引用 &mut T   (写者必须独占)  │
         └────────────────────────────────────────────┘

   两者不可同时存在。

为什么会这样? 三条直觉,任选一条都能自洽:

  1. 数据竞争(data race)的最小定义就是「读与写并发 + 无同步」。 禁止 &T&mut T 共存,等于静态消灭了单线程里的数据竞争。 (多线程里的数据竞争还需要 Send/Sync,见〈并发编程〉,但那部分建立在同一套规则上。)
  2. 迭代器失效(iterator invalidation)Vec 扩容会重新分配缓冲区, 旧指针变悬垂。C++ 里这是经典 UB,Rust 用「push 时不能再有迭代中的引用」直接拦下。
  3. &mut T 隐含了「这一段内存只被我一个人碰」的承诺, 优化器可以据此把值缓存在寄存器里(noalias)。多个 &mut 会打破这个承诺。
rust
fn main() {
    let mut v = vec![1, 2, 3];
    let a = &mut v;
    let b = &mut v;              // 编译失败
    a.push(4);
    b.push(5);
}
// error[E0499]: cannot borrow `v` as mutable more than once at a time

E0502:不可变与可变不能共存

rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;                 // 不可变借用开始
    let r2 = &s;
    println!("{} and {}", r1, r2);

    let r3 = &mut s;             // 编译失败:r1/r2 之后还要用(下一行 println!)
    r3.push_str(" world");
    println!("{}", r1);
}

rustc 原始输出(1.98.1 实测):

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src\main.rs:8:14
  |
3 |     let r1 = &s;
  |              -- immutable borrow occurs here
...
8 |     let r3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
9 |     r3.push_str(" world");
10|     println!("{}", r1);
  |                    -- immutable borrow later used here

解读:编译器不是在抱怨第 8 行,而是在抱怨「第 3 行的借用活到了第 10 行」。 immutable borrow later used here 这一行是全文最重要的线索 —— 它告诉你这个借用的实际结束点在哪儿

修法:把这个不可变借用用完就收

rust
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &s;
        let r2 = &s;
        println!("{} and {}", r1, r2);      // 输出:hello and hello
    }                                        // r1、r2 在这里结束

    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);                      // 输出:hello world
}

引用的作用域:NLL 与临时值生命周期

这是初学者最大的困惑源头:「作用域」不等于「花括号范围」

下面这段代码里,r1/r2 的借用在第 4 行就结束了,尽管它们的花括号要到第 7 行才闭合:

rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);       // r1、r2 的最后一次使用
    // 从这里起 r1、r2 已「死」,借用到此为止 —— 这就是 NLL
    let r3 = &mut s;
    println!("{}", r3);                  // 输出:hello
}

NLL(Non-Lexical Lifetimes,非词法生命周期) 是新的借用检查器(MIR borrowck)的核心, 自 Rust 1.31 / 2018 edition 起稳定,并在 Rust 1.36 起成为所有 edition 的默认行为。 它按控制流图上的活跃区间(liveness)判定借用结束,结束点是最后一次使用, 而不是包含它的那个 }

⚠️ 陷阱:在 rustc 1.98.1 上,NLL 对所有 edition 都生效 —— 你无法用 --edition 2015 复现「花括号式」的旧报错。 换句话说,「升级 edition 就能通过借用检查」在今天的 NLL 场景里已经不成立, edition 影响的是下面这类临时值作用域规则。

Rust 2024 edition 改小了临时值(temporary)在一种具体位置上的存活范围: if let $pat = $expr { ... } else { ... } 中,由 $expr 产生的临时值 在进入 else 分支之前就被 drop,而不是等到整个 if let 语句结束。 这消除了「读锁一直持有到 else 分支里再去拿写锁」的经典诡异死锁(或 RefCell panic)。

rust
use std::cell::RefCell;

fn f(value: &RefCell<Option<bool>>) {
    if let Some(x) = *value.borrow() {       // 这里产生一个临时的 Ref 守卫
        println!("value is {x}");
    } else {
        let mut v = value.borrow_mut();      // 2021:Ref 还活着 ⇒ panic
        if v.is_none() { *v = Some(true); }
    }
}

fn main() {
    let c = RefCell::new(None);
    f(&c);
    f(&c);
    println!("{:?}", c.borrow());            // 2024 输出:Some(true)
}
editionelse 分支里的 borrow_mut()结果
2021临时 Ref 借用的作用域被延长到整个 if let 语句之后thread 'main' panicked at ...: RefCell already borrowed
2024临时 Ref 在进入 else 前已 drop正常输出 Some(true)

🧠 原理:edition 只改作用域推断规则,不改语法。同一份 .rs 文件用 --edition 2021--edition 2024 编译都成功(这不是编译错误差异, 而是运行期行为差异),但运行结果可能完全不同。 cargo fix --edition 会通过 if_let_rescope lint 把有风险的 if let 改写成 matchmatch 的临时值作用域仍是「到语句结束」,即 2021 的行为)。

⚠️ 陷阱:不要用花括号去「推理」借用何时结束,要看最后一次使用在哪里。 唯一的例外是临时值生命周期延长(temporary lifetime extension)—— let r = &String::from("x"); 这类「引用直接作为 let 初始化式」的写法会被延长到变量作用域。



悬垂引用与生命周期入门

悬垂引用(dangling reference)

Rust 永远不允许产生悬垂引用。下面这段代码的失败信息很有教学价值:

rust
fn main() {
    let s = no_dangle();
    println!("{}", s);
}

fn no_dangle() -> &String {      // 编译失败:这个 &String 该借谁?
    let s = String::from("hello");
    &s                            // s 在函数末尾被 drop,返回的引用会悬垂
}

rustc 原始输出(1.98.1 实测):

error[E0106]: missing lifetime specifier
 --> src\main.rs:6:19
  |
6 | fn no_dangle() -> &String {
  |                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
          but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're
      returning a borrowed value from a `const` or a `static`
  |
6 | fn no_dangle() -> &'static String {
  |                    +++++++
help: instead, you are more likely to want to return an owned value
  |
6 - fn no_dangle() -> &String {
6 + fn no_dangle() -> String {

为什么是 E0106(缺少生命周期标注)而不是「悬垂引用」? 因为对编译器来说,这个签名信息不足:返回值的引用可能来自参数(合法), 也可能来自函数内部(非法)。它先要求你把来源写清楚。 如果来源写清楚了但只可能是函数内部,才会报 E0515

rust
struct Foo {
    name: String,
}

fn make<'a>() -> &'a Foo {       // 编译失败示例
    let f = Foo { name: String::from("a") };
    &f
}
// error[E0515]: cannot return reference to local variable `f`
//   |     &f
//   |     ^^ returns a reference to data owned by the current function

正确做法:返回拥有所有权的值,让所有权正常转移出去。

rust
fn no_dangle() -> String {
    let s = String::from("hello");
    s                             // 移动出去,不是借用
}

fn main() {
    let s = no_dangle();
    println!("{s}");              // 输出:hello
}

🧠 原理E0106E0515 是同一件事的两个阶段。 E0106 = 「你没告诉我这个引用从哪来」;E0515 = 「你告诉我了,但那个来源活不过函数」。

生命周期标注是「关系」而不是「时长」

先记住一句反直觉但极其重要的话:

生命周期参数 'a 不表示「活多久」,它表示「这些引用必须活得一样久」这个约束。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 读作:

对于任意一个生命周期 'a,只要 xy 都至少活到 'a, 那么返回值也至少活到 'a

要求 xy 本身同样长 —— 只是要求它们覆盖同一个区间, 由调用方各自「取交集」。

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string is long");
    {
        let s2 = String::from("xyz");
        let result = longest(s1.as_str(), s2.as_str());
        println!("最长的字符串是 {result}");       // 输出:最长的字符串是 long string is long
    }
    // 出了这个块 result 就不能再用了:'a 被取成了 s2 的存活区间
}
   'a 的实际取值 = 两个实参存活区间的交集(短的那个)

   s1 存活  ├────────────────────────────────────────────┤
   s2 存活          ├──────────────────┤
   'a               ├──────────────────┤   ← 交集,由 s2 决定
   result 只能在这个区间内使用

如果强行越界使用:

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string is long");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
    }                                             // s2 在这里 drop
    println!("最长的字符串是 {result}");           // E0597
}
// error[E0597]: `s2` does not live long enough
//   |         let s2 = String::from("xyz");
//   |             -- binding `s2` declared here
//   |         result = longest(s1.as_str(), s2.as_str());
//   |                                      ^^ borrowed value does not live long enough
//   |     }                                - `s2` dropped here while still borrowed
//   |     println!("最长的字符串是 {result}");
//   |                              ------ borrow later used here

⚠️ 陷阱'a 不是「变量的寿命」,也不是一个可以「延长」的东西。 你无法通过「把 'a 写大一点」让代码通过编译 —— 编译器只会告诉你实参不允许。

生命周期省略规则(elision)

如果每个引用都要手写生命周期,签名会非常吵。所以编译器内置三条省略规则。 重要前提:省略规则只在「函数 / 方法签名」上生效,结构体定义上不生效(见「结构体中的引用与生命周期参数」)。

规则 1(输入位置):每个省略了生命周期的引用参数,各自获得一个独立的生命周期参数。
        fn f(x: &str, y: &str)  ⇒  fn f<'a, 'b>(x: &'a str, y: &'b str)

规则 2(输出位置——若只有一个输入生命周期):输出引用的生命周期 = 那个唯一的输入生命周期。
        fn f(x: &str) -> &str   ⇒  fn f<'a>(x: &'a str) -> &'a str

规则 3(方法接收者):若签名里有 &self 或 &mut self,则输出引用的生命周期 = self 的生命周期。
        fn f(&self, x: &str) -> &str  ⇒  输出与 &self 同寿命('b 被忽略)

若三条规则都不能确定输出引用的生命周期 ⇒ 报 E0106,要求你手写。
rust
// 规则 2 的例子:一个输入,输出跟着它,无需手写生命周期
fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    println!("{}", first_word("hello world"));      // 输出:hello
}
cpp
// 规则 2 不适用:两个输入,输出不确定借谁 ⇒ 必须手写 'a(否则 E0106)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    println!("{}", longest("abcd", "xy"));          // 输出:abcd
}

💡 对照:C++ 完全没有这个概念 —— 返回引用是否悬垂靠你自己看; std::string_view 的悬垂是 C++ 社区公认的雷区。Rust 把「这个 view 能活多久」 变成函数类型签名的一部分,调用方拿到返回值就知道能用多久。 代价是你要写 'a

&'static str:字符串字面量

rust
fn main() {
    let s: &'static str = "我在程序的只读数据段里";

    let owned: String = s.to_string();       // 需要可修改/可拥有时再转成 String
    println!("{s} / {owned}");
}

&'static str 表示「这个引用在整个程序运行期间都有效」。 字符串字面量被编译进二进制(通常是 .rdata 只读段),因此天然是 'static

⚠️ 陷阱不要为了让 E0106 消失而随手加 'staticfn f(x: &str) -> &'static str 意味着「返回的东西不依赖 x」, 而 fn no_dangle() -> &'static String 编译也会继续失败(String 不是 'static 数据)。 'static最强的承诺,只在返回字面量或全局常量时才对。

🚀 进阶const X: &str = "..."static X: &str = "..." 都是 'static; 泛型约束里的 T: 'static 含义是「T 不含任何短命借用」,不是T 是全局变量」, 常被误读。详见〈智能指针与闭包〉讲 Box<dyn Trait + 'static> 的地方。



结构体中的引用与生命周期参数

结构体持有引用必须标注生命周期

结构体定义上,省略规则不生效:只要字段里有引用,就必须给结构体加生命周期参数。

rust
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("找不到 '.'");
    let excerpt = Excerpt { part: first_sentence };
    println!("节选:{}", excerpt.part);      // 输出:节选:Call me Ishmael
}

Excerpt<'a> 读作:「一个 Excerpt 实例,不能比它 part 字段借用的那份数据活得更久。」

   novel(String,拥有堆缓冲区)

     │  &novel[0..16]  ← 只是两个整数的描述符(ptr + len)

   ┌──────────────────┐        ┌─────────────┐
   │ excerpt.part ────┼───────►│ Call me ... │  堆上数据仍归 novel 所有
   └──────────────────┘        └─────────────┘
     excerpt 只借,不拥有;novel 若被 drop/move,excerpt 立刻失效
rust
// 编译失败示例
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let excerpt;
    {
        let novel = String::from("Call me Ishmael.");
        excerpt = Excerpt { part: novel.as_str() };
    }                                        // novel drop 了
    println!("{}", excerpt.part);            // E0597: `novel` does not live long enough
}

🧠 原理:生命周期参数在结构体上是「传染性」的:Excerpt<'a> 的实例一旦存在, 编译器就会追踪 'a,直到最后一个实例消失才允许 novel 被释放。 这就是为什么 'a 常被描述为「关系」:它把 Excerptnovel 绑在一起。

初学者为什么更该让结构体「拥有」数据

rust
// 推荐写法:结构体拥有数据,没有任何生命周期参数
struct Excerpt {
    part: String,
}

impl Excerpt {
    fn from_sentence(text: &str) -> Self {
        let end = text.find('.').unwrap_or(text.len());
        Excerpt { part: text[..end].to_string() }
    }
}

fn main() {
    let e = Excerpt::from_sentence("Call me Ishmael. Some years ago...");
    println!("节选:{}", e.part);        // 输出:节选:Call me Ishmael
}

代价与收益:

方案优点代价
struct Excerpt<'a> { part: &'a str }零拷贝;适合解析器、零分配场景生命周期参数传染到所有使用它的函数/结构体;难以放进 Vec、跨线程、跨 async 边界
struct Excerpt { part: String }自包含、可自由 move / 存入集合 / 跨线程;API 简单一次分配 + 一次拷贝

⚠️ 陷阱:初学阶段强行设计「零拷贝结构体」,会让每个函数签名都长出 <'a>, 报错也从「借用的值活了多久」变成一片 E0597/E0515先让结构体拥有 String,把程序跑通;等到剖析(profiling)证明拷贝是瓶颈再改。

🚀 进阶:真正的零拷贝解析库(如 serde#[serde(borrow)]nom&'a [u8] 输出)确实大量使用生命周期参数,但那是性能驱动的设计, 不是入门姿势。



切片与所有权

切片是「借来的一段连续数据」

切片(slice)是胖指针(fat pointer):一个地址 + 一个长度,共 16 字节(64 位平台), 指向别人拥有的连续内存。它自己不拥有任何数据。

   &str 或 &[T] 的运行时表示(16 字节,栈上)
   ┌──────────────────┬──────────┐
   │ ptr ─────────────┼─► 数据   │      数据本身归 String / Vec / 数组所有
   │ 0x55f0_2c10      │ len = 5  │      切片只是「这个范围借我看看」
   └──────────────────┴──────────┘

&strString 的借用

rust
fn main() {
    let s = String::from("hello world");

    let hello: &str = &s[0..5];      // 借用第 0..5 字节
    let world: &str = &s[6..11];
    let all: &str = &s[..];
    let tail: &str = &s[6..];

    println!("{hello} / {world} / {all} / {tail}");
    // 输出:hello / world / hello world / world
}
   s ──[ptr|11|11]──►┌─h─e─l─l─o─ ─w─o─r─l─d─┐
                     └─┬───────────┬───────────┘
                       │           │
   hello = [ptr+0, 5] ─┘           └─ world = [ptr+6, 5]
   三个变量共享同一块堆内存,没有任何字节被复制

⚠️ 陷阱&s[0..5]字节切。如果切在 UTF-8 字符中间, 运行期 panic:end byte index 2 is not a char boundary; it is inside '你', 这不是编译错误。想要安全地按字符处理,用 s.chars()s.char_indices()s.get(0..5)(返回 Option<&str> 而不是 panic)。

rust
fn main() {
    let s = String::from("你好世界");
    // &s[0..2] 会 panic:'你' 占 3 字节。安全写法:
    let safe = s.get(0..3).unwrap_or("");
    println!("{safe}");              // 输出:你
}

&[T]Vec<T> 与数组的借用

rust
fn main() {
    let v: Vec<i32> = vec![1, 2, 3, 4, 5];
    let arr: [i32; 5] = [1, 2, 3, 4, 5];

    let vs: &[i32] = &v;             // Vec<i32> → &[i32]
    let as_: &[i32] = &arr;          // [i32; 5] → &[i32]
    let mid: &[i32] = &v[1..3];      // 子切片,元素是 2, 3

    println!("{} {} {:?}", vs.len(), as_.len(), mid);   // 输出:5 5 [2, 3]
}

同一个函数可以同时处理 Vec 和数组,因为它接受的是切片:

cpp
fn sum_all(values: &[i32]) -> i32 {
    values.iter().sum()
}

fn main() {
    let v = vec![1, 2, 3];
    let a = [4, 5];
    println!("{}", sum_all(&v));    // 输出:6
    println!("{}", sum_all(&a));    // 输出:9
}

💡 对照&[T] ≈ C++ 的 std::span<T>(C++20)或 Go 的切片(但 Go 切片自带 长度且不区分所有权);&strstd::string_view(C++17)。 区别在于 Rust 的借用检查器会保证被切的那块内存活得比切片长。

参数类型选择的经验法则

不要写改写成原因
fn f(s: &String)fn f(s: &str)&String 只接受 &String&str 同时接受 &String&str&'static str。靠解引用强制转换(deref coercion)自动完成转换
fn f(v: &Vec<T>)fn f(v: &[T])同理:&[T] 还能接受数组、子切片、Vec 的切片
fn f(s: String)fn f(s: &str) 或保持 String需要拥有权(存进结构体、跨线程)才要 String;只读就借
fn f(v: Vec<T>)fn f(v: &[T])只读时没必要夺取 Vec 的所有权
rust
// 反面示例:&String 参数把调用方限制死了
fn old_style(s: &String) -> usize { s.len() }

// 正面示例:&str 参数接受多种实参
fn new_style(s: &str) -> usize { s.len() }

fn main() {
    let owned = String::from("hello");
    let literal = "hello";
    println!("{} {}", old_style(&owned), new_style(literal));   // 输出:5 5
    // old_style(literal);  // 编译失败:expected `&String`, found `&str`
    println!("{}", new_style(&owned));                          // 输出:5
}

🧠 原理解引用强制转换只朝一个方向走:&String → &str&Vec<T> → &[T]&Box<T> → &T。反过来不行(&str 不能变成 &String)。 所以参数类型要选更宽松的那一端。 clippyptr_arg lint 会自动指出这类写法:cargo clippy 见「常见坑与编译错误」。

🚀 进阶String&str 可以发生在 AsRef<str> 泛型参数上 (fn f(s: impl AsRef<str>)),但会让签名变复杂且影响类型推断。 入门与多数库代码用 &str 就够了。



内部可变性预告

一句话预告

借用规则听起来很死:「共享就不许改,改就不许共享」。 但有些数据结构(缓存、图、观察者列表、Rc 的共享状态)逻辑上必须在共享的同时修改。 Rust 的出路叫内部可变性(interior mutability):把借用检查推迟到运行期, 用 Cell/RefCell/Rc/Mutex/RwLock 这些类型自己保证安全。 完整讲解见内部可变性与 Arc

Cell / RefCell / Rc 适用场景总结

类型共享方式可变性是否线程安全典型用途
Cell<T>按值进出(get/set/replace),从不给出 &T通过 &self 改内部值(要求 T: Copy 或用 replace否(!Sync计数器、标志位、Cell<bool> 做惰性初始化开关
RefCell<T>运行时借用:borrow()Ref<T>borrow_mut()RefMut<T>通过 &self 改;违反规则时 panic 而不是编译错误否(!Sync单线程图、树节点互指、测试替身(mock)、需要共享可变的缓存
Rc<T>多个所有者,引用计数 +1/-1不可变Rc<T> 只给 &T,要改就套 RefCell否(!Send/!Sync,计数非原子)单线程多所有者;Rc<Node> 构成的图 / DOM 树
Rc<RefCell<T>>多所有者 + 可改组合:Rc 管共享,RefCell 管可变单线程共享可变状态的事实标准写法
Arc<T>原子引用计数不可变Send/Sync,要求 T: Send + Sync跨线程共享只读数据
Arc<Mutex<T>>原子计数 + 互斥锁加锁后可改跨线程共享可变状态
Mutex<T> / RwLock<T>独占 / 读写锁加锁后可改线程间互斥;RwLock 适合读多写少

⚠️ 陷阱RefCell 的失败是运行期 panicalready borrowed: BorrowMutError), 不是编译错误。这正是它被称为「把借用检查搬到了运行期」的含义 —— 你换来了灵活性,放弃了编译期保证。别把它当默认选择。

⚠️ 陷阱Rc 的循环引用会泄漏(计数永远不归零)。 需要打破环时用 Weak<T>(弱引用,不增加强计数)。


与其他语言的对照

同一件事,五种语言

场景JavaPythonC++GoRust
赋值 b = a(对象 / 堆类型)拷贝引用(别名),ab 同一对象拷贝引用(别名)拷贝构造:深拷贝一份(a 仍可用)拷贝指针(别名),GC 管回收movea 立即作废,堆数据不复制
想让两个名字都能用天然可以(共享可变)天然可以b = a; 隐式拷贝构造天然可以显式 a.clone() 或借用 &a
想让「交出所有权」无此概念无此概念(del a 只是删名字)b = std::move(a);a 未指定状态)无此概念let b = a;(默认行为)
只读传参引用传递,靠约定不改引用传递,靠约定不改const T&值传递或指针,无 const 保证&T,编译期保证
可写传参引用传递,直接改引用传递,直接改T&指针 *T&mut T,且调用方需 let mut
返回内部数据的引用/视图返回引用,GC 保证不悬垂返回引用,GC 保证不悬垂返回 T&string_view可能悬垂(UB)返回指针/slice,GC 保证不悬垂返回值必须带生命周期,E0515 拦下悬垂
释放内存GC 自动引用计数 + GC析构函数(RAII)+ 手动 deleteGC 自动作用域结束自动 drop(编译期定时机)
双重释放不可能不可能可能delete p; delete p;不可能不可能(CopyDrop 互斥)
内存泄漏可能(长生命周期容器)可能(循环引用 / 全局缓存)可能(漏 delete、异常路径)可能(goroutine 持有、全局)可能(Rc 成环、Box::leakmem::forget
读写别名同时存在允许(线程不安全则运行期出错)允许(GIL 掩盖了部分问题)允许(UB 的常见来源)允许(go vet 只能警告一部分)编译错误E0502/E0499
共享可变状态的默认解法synchronized / AtomicXxx / 不可变类threading.Lock / 不可变类型std::mutex / 智能指针sync.Mutex / channelRefCell(单线程)/ Arc<Mutex<T>>(多线程)
迭代中修改容器ConcurrentModificationException(运行期)RuntimeError: dictionary changed size(运行期)UB(未定义行为)行为未定义或 panic编译错误E0502

「Rust 的 move 在机器码上常常就是 memcpy,甚至什么都不做」

这一句要拆成三层来理解:

  1. 栈上描述符的复制String 的 move 在机器码上等价于 3 个机器字(24 字节)的复制, 通常就是两三条 mov 指令,或者一个内联后的 memcpy。 堆上的实际数据完全没动,也没有 free/malloc
  2. 优化器经常把它消掉:如果 move 之后旧变量再没人用, LLVM 会把新旧变量合并成同一个栈槽或寄存器,此时 move 在机器码上一条指令都没有。 它纯粹是一个编译期的「所有权归属」变更。
  3. 编译期约束,零运行期机制:没有引用计数、没有标记位、没有运行期检查。 Rust 没有「这个值被 move 过」的运行时标志 —— 一旦你违反规则,程序根本编译不出来。
   Rust move 的三个层次

   源码:  let b = a;                 ← 你看到的
   ├─ 语义层(编译期):所有权从 a 转移到 b,a 作废          ← 编译器实际做的事
   ├─ MIR/HIR 层:a 被标记为「已移动」,后续使用报 E0382
   └─ 机器码层:3 个机器字复制 / 或被优化器完全消除          ← 通常是 0~3 条指令

🧠 原理:这就是「零成本抽象(zero-cost abstraction)」的字面含义 —— 你得到的是编译期的安全检查,付出的运行期成本是 0; 付出的编译期成本是「借用检查器可能拒绝你」。

💡 对照:Go 的切片/指针对应「总是别名 + GC」;Go 里想把 []byte 交给别人 而不担心它被改,只能 copy() 一份。Rust 里 &[u8] 就够, 因为只要这个借用活着,原作者根本改不了



内容以 rustc 1.98.1 · Rust 2024 edition 为基准