所有权:编译器为什么拒绝我
移动:赋值为什么会让变量失效
它在解决什么
你已经会的那门语言,多半用两种办法之一管内存:运行时有个垃圾回收器
(Java、Go、JavaScript、C#、Python),或者你自己 free(C)。
第一种的代价是你交出了「什么时候释放」的控制权,换来不用操心;
第二种的代价是 free 早了、晚了、或者两次,都会在运行时咬你。
Rust 选了第三条:每块内存在任一时刻有且只有一个「主人」,主人离开作用域时它被释放。
没有回收器,也没有你手写的 free —— 释放的位置由编译器根据「主人是谁」算出来,
插在代码里。
这条规则本身不难懂。难的是它的一个直接推论,而那个推论会在你写第一段真代码时
就把你拦下来:既然只能有一个主人,那 let t = s; 之后,主人是谁?
这一篇就讲这个推论,以及它长出来的五种形状。
三条规则
Rust 的所有权只有三条:
- 每个值都有一个所有者(owner)。
- 同一时刻只能有一个所有者。
- 所有者离开作用域时,值被丢弃(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` 当一个整体再用。