所有权:编译器为什么拒绝我

移动:赋值为什么会让变量失效

它在解决什么

你已经会的那门语言,多半用两种办法之一管内存:运行时有个垃圾回收器 (Java、Go、JavaScript、C#、Python),或者你自己 free(C)。

第一种的代价是你交出了「什么时候释放」的控制权,换来不用操心; 第二种的代价是 free 早了、晚了、或者两次,都会在运行时咬你。

Rust 选了第三条:每块内存在任一时刻有且只有一个「主人」,主人离开作用域时它被释放。 没有回收器,也没有你手写的 free —— 释放的位置由编译器根据「主人是谁」算出来, 插在代码里。

这条规则本身不难懂。难的是它的一个直接推论,而那个推论会在你写第一段真代码时 就把你拦下来:既然只能有一个主人,那 let t = s; 之后,主人是谁?

这一篇就讲这个推论,以及它长出来的五种形状。

三条规则

Rust 的所有权只有三条:

  1. 每个值都有一个所有者(owner)。
  2. 同一时刻只能有一个所有者。
  3. 所有者离开作用域时,值被丢弃(drop)。

第 3 条是自动的,你不用写。下面这段里 s 在 } 那一行被释放, 不需要任何一行释放代码:

fn main() {
    {
        let s = String::from("hi");
        println!("{s}");
    } // ← s 在这里被 drop,那块堆内存还给系统
}

第 2 条是本篇的全部内容。

赋值就是移动

let t = s; 在 Java/Go/JS 里的意思是「多一个名字指向同一个东西」。 在 Rust 里,因为第 2 条规则不允许两个所有者,它的意思变成了: 所有权换人了,s 这个名字作废。

这段代码编译不过,报的是 error[E0382]。

编译器说的是 borrow of moved value: s —— 被移走的值又被借用了。 注意它用的词是 moved,不是 copied:s 没有被复制,它被搬走了。

搬走的具体含义值得说清楚,因为它决定了这件事的代价:

栈                          堆
┌──────────────┐
│ s            │
│  ptr ────────┼──────────► [ 'h', 'i' ]
│  len   2     │
│  cap   2     │
└──────────────┘

let t = s; 之后

┌──────────────┐
│ s  (作废)  │
├──────────────┤
│ t            │
│  ptr ────────┼──────────► [ 'h', 'i' ]   ← 堆上这块一个字节都没动
│  len   2     │
│  cap   2     │
└──────────────┘

栈上那三个字段被复制了一份(64 位平台上实测 24 字节 —— 指针 + 长度 + 容量各 8,见语法速通那篇的示例), 堆上的内容一个字节都没动。 所以「移动」在运行时几乎是免费的 —— 它不是深拷贝,也没有引用计数。 真正发生的事全在编译期:编译器从此不再认 s 这个名字。

⇒ 这也解释了为什么 s.clone() 能让它通过: clone 是真的去堆上又开一块、把内容抄过去,于是有了两个独立的值、两个所有者, 第 2 条规则没被违反。代价是一次真实的内存分配和拷贝 —— 所以 clone 不是「让编译器闭嘴的咒语」,它是一个有价格的操作。

为什么有些类型不受影响

如果每次赋值都会让原变量失效,那 let a = 5; let b = a; 之后 a 也该作废 —— 但它照样能用。

判据不是「小的就复制」,而是这个类型有没有实现 Copy。 Copy 的含义很具体:这个值可以靠逐位复制得到一个完全等价的副本, 而且两份都被丢弃时不会出任何问题。

i32 满足:复制 4 个字节就是另一个完整的 i32,丢弃它什么也不用做。 String 不满足:逐位复制会得到两个指向同一块堆内存的 String, 它们各自被丢弃时会把那块内存释放两次 —— 正是 C 里那个经典的 double free。 所以 String 不可能是 Copy,这不是设计者的选择,是类型本身决定的。

实现了 Copy 的有:所有整数、浮点、bool、char、不可变引用 &T, 以及全部字段都是 Copy 的元组、数组和结构体。 可变引用 &mut T 不是 Copy(否则就能同时存在两个可变引用, 而借用那篇的规则不允许 —— 它和本篇同在这一章)。

函数传参也是移动

let t = s; 会移动,f(s) 也一样 —— 把值交给函数,就是把所有权交出去。

这段编译不过:eat(s) 之后 s 已经不归你了, 函数返回时它在函数体末尾被丢弃。

第一反应通常是「那我把它还回来」,而这确实能行: 让函数把值连同结果一起返回,再接住它。

fn eat_and_give_back(s: String) -> (String, usize) {
    let n = s.len();
    (s, n)   // 把所有权交回去
}

但你只要写第二个这样的函数就会发现这条路走不通 —— 每个只想读一下字符串的函数都得返回一个元组,签名全被记账逻辑污染了。

⚠️ 顺带注意那个对照项:函数把值还回来了, 但如果你写的是 let (_, n) = ...,没接住,它照样在那一行结束时被丢弃。 所有权是被「接住的那个名字」拿到的,不是被函数调用本身还回来的。

这就是为什么必须有一套「不转移所有权地用一下」的机制。那套机制叫借用, 是所有权这一章的第二块拼图。

容器和结构体:拿不走的两种形状

前面四条都是「一整个值」的移动。真实代码里更常撞见的是从别的东西里面 往外拿一个值,而那会有不同的报错。

从容器里拿。 let first = v[0]; 编译不过, 但报的不是 E0382 而是 E0507。编译器在说:不能从这里移出去。 理由很直白 —— Vec 本身还活着,把它的一个元素挖走会在它身上留个洞, 之后遍历到那个位置会读到什么?所以这条路直接被堵死了。 解法是借一个(&v[0]),或者 clone 一份。

从结构体里拿。 结构体不一样,它允许你把一个字段搬走,这叫部分移动:

这段代码里 let name = u.name; 之后, u.age 照样能用(部分移动只带走了 name),但 u 作为一个整体已经不完整了 —— 再写 let u2 = u; 就会报 E0382。

这个细节值得留意,因为它是「报错行号和你以为的不是同一行」的常见来源: 出问题的那一行是 let u2 = u;,而原因在上面那个不起眼的 let name = u.name;。

怎么读 E0382 这段报错

Rust 的报错比大多数语言长,但结构是固定的,读熟了很快。 展开本篇任意一条编译不过的示例的「rustc 原文」,都能看到同样的三段 (跑得通的那两条没有原文可看,因为编译器什么都没说):

error[E0382]: borrow of moved value: `s`     ← ① 结论:什么值、出了什么事
 --> move-assign.rs:4:16
  |
2 |     let s = String::from("hi");
  |         - move occurs because `s` has type `String`, ...
3 |     let t = s;
  |             - value moved here            ← ② 案发现场:在哪一行被搬走的
4 |     println!("{s}");
  |                ^ value borrowed here after move
  |
help: consider cloning the value ...          ← ③ 它给的建议

②「value moved here」那一行才是要改的地方,而不是报错箭头指的那一行。 这是读 Rust 报错最该先学会的一件事:箭头指的是「哪里出的事」, 而 moved here 指的是「哪里造的因」。

③ 的建议经常是对的,但不总是你想要的。这里它建议 clone, 而在真实代码里更好的答案往往是借用。什么时候该听它的,是这一章后面专门一篇的事。

下一步

这一篇留了一个没答的问题:既然交出去再接回来这条路不可扩展, 那「只是想读一下,不想要所有权」该怎么写?答案是借用 —— & 和 &mut,以及它们那两条看起来很严格的规则: 《借用:& 与 &mut 的两条规则》。

这一章的全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

赋值就是移动:原变量当场失效

fn main() {
    let s = String::from("hi");
    let t = s;
    println!("{s}");
    println!("{t}");
}

编译不过 · error[E0382]

rustc 原文
error[E0382]: borrow of moved value: `s`
 --> move-assign.rs:4:16
  |
2 |     let s = String::from("hi");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let t = s;
  |             - value moved here
4 |     println!("{s}");
  |                ^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let t = s.clone();
  |              ++++++++
对照:把 `let t = s;` 改成 `let t = s.clone();`
fn main() {
    let s = String::from("hi");
    let t = s.clone();
    println!("{s}");
    println!("{t}");
}

编译通过,输出:"hi\nhi\n"

这就是从别的语言过来的人第一次被拦下的地方:`let t = s` 在 Java/Go/JS 里是「多一个名字指向同一个东西」,在 Rust 里是「所有权换人了,原来的名字作废」。

不是所有类型都会移动:Copy 类型照旧

fn main() {
    let a = 5;
    let b = a;
    println!("{a} {b}");
}

编译通过 · 输出 "5 5\n"

对照:把 `i32` 换成 `String`(其余一字不改)
fn main() {
    let a = String::from("5");
    let b = a;
    println!("{a} {b}");
}

编译不过:error[E0382]

判据不是「小的就复制」,是这个类型实现了 `Copy` 没有。整数、浮点、bool、char、以及全由它们组成的元组都实现了;只要里面有一个需要释放的东西(比如 `String` 拥有的那块堆内存),就不可能是 `Copy`。

函数传参也是一次移动

fn eat(s: String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let n = eat(s);
    println!("{n} {}", s.len());
}

编译不过 · error[E0382]

rustc 原文
error[E0382]: borrow of moved value: `s`
 --> move-into-fn.rs:8:24
  |
6 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 |     let n = eat(s);
  |                 - value moved here
8 |     println!("{n} {}", s.len());
  |                        ^ value borrowed here after move
  |
note: consider changing this parameter type in function `eat` to borrow instead if owning the value isn't necessary
 --> move-into-fn.rs:1:11
  |
1 | fn eat(s: String) -> usize {
  |    ---    ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     let n = eat(s.clone());
  |                  ++++++++
对照:参数改成 `&String`,调用处改成 `eat(&s)`
fn eat(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let n = eat(&s);
    println!("{n} {}", s.len());
}

编译通过,输出:"5 5\n"

对照项里的 `&String` 是**能跑但不地道**的写法,正确的是 `&str`。这里先不展开,因为它属于切片那一篇。

把所有权交回来:能行,但很快就会难看

fn eat_and_give_back(s: String) -> (String, usize) {
    let n = s.len();
    (s, n)
}

fn main() {
    let s = String::from("hello");
    let (s, n) = eat_and_give_back(s);
    println!("{n} {}", s.len());
}

编译通过 · 输出 "5 5\n"

对照:把 `let (s, n)` 改成 `let (_, n)`(没接住交回来的那个)
fn eat_and_give_back(s: String) -> (String, usize) {
    let n = s.len();
    (s, n)
}

fn main() {
    let s = String::from("hello");
    let (_, n) = eat_and_give_back(s);
    println!("{n} {}", s.len());
}

编译不过:error[E0382]

对照项说明了一件容易忽略的事:所有权是**被接住的那个名字**拿到的。函数把它还回来了,你没接,它照样在那一行结束时被丢掉。

从容器里「拿」一个出来,拿不走

fn main() {
    let v = vec![String::from("a"), String::from("b")];
    let first = v[0];
    println!("{first}");
}

编译不过 · error[E0507]

rustc 原文
error[E0507]: cannot move out of index of `Vec<String>`
 --> move-out-of-index.rs:3:17
  |
3 |     let first = v[0];
  |                 ^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
  |
help: consider borrowing here
  |
3 |     let first = &v[0];
  |                 +
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let first = v[0].clone();
  |                     ++++++++
对照:把 `v[0]` 改成 `&v[0]`
fn main() {
    let v = vec![String::from("a"), String::from("b")];
    let first = &v[0];
    println!("{first}");
}

编译通过,输出:"a\n"

这条的错误码**不是** E0382 而是 E0507 —— 编译器在说「不能从这里移出去」,因为 `Vec` 还在,挖走一个元素会在它身上留个洞。

部分移动:拿走一个字段,整体就不完整了

struct User {
    name: String,
    age: u32,
}

fn main() {
    let u = User { name: String::from("doudou"), age: 10 };
    let name = u.name;
    println!("{name} {}", u.age);
    let u2 = u;
    println!("{}", u2.age);
}

编译不过 · error[E0382]

rustc 原文
error[E0382]: use of partially moved value: `u`
  --> move-partial-field.rs:10:14
   |
 8 |     let name = u.name;
   |                ------ value partially moved here
 9 |     println!("{name} {}", u.age);
10 |     let u2 = u;
   |              ^ value used here after partial move
   |
   = note: partial move occurs because `u.name` has type `String`, which does not implement the `Copy` trait
对照:把 `let name = u.name;` 改成 `let name = u.name.clone();`
struct User {
    name: String,
    age: u32,
}

fn main() {
    let u = User { name: String::from("doudou"), age: 10 };
    let name = u.name.clone();
    println!("{name} {}", u.age);
    let u2 = u;
    println!("{}", u2.age);
}

编译通过,输出:"doudou 10\n10\n"

注意 `println!("{name} {}", u.age)` 那一行是**过得去的** —— 部分移动只带走了 `name`,`age` 还在。真正过不去的是后面把 `u` 当一个整体再用。