读 Rust 代码时,很容易卡在这样两行上:
rust
let tools = build_tools(&ctx, args);
let mut tools = tools;
同一个名字怎么能声明两次?第二行已经拿到了所有权,为什么还要写 mut?既然东西归我,难道还不能改吗?
如果之前主要写 Java、C# 或 JavaScript,这种困惑很自然。我们习惯了把对象交给另一个变量,再从任何一个变量修改它。Rust 把这里的几件事分开了,需要稍微换一下思路。
这篇笔记从这些具体问题讲起。先看常见场景,最后留几道题,可以自己判断一下,不用急着记术语。
先分清三个问题
看到一段 Rust 代码,可以先问:
- 所有权:谁持有这个值,负责在不再需要时释放它?
- 借用:谁临时使用这个值?
- 可变性:当前使用方式是否允许修改它?
拥有一个值,并不意味着当前变量就能修改它。把值交给别人,也不一定是在复制数据。后面的报错,大多能从这三个问题里找到原因。
1. 换个变量接住,原来的变量就不能用了
rust
let a = String::from("hello");
let b = a;
println!("{b}"); // 可以
println!("{a}"); // 编译错误:a 的值已经被移动
第一次遇到这个错误,可能会觉得:我只是赋了个值,怎么就不能用了?
String 管理着堆上的字符串数据。这里的赋值把所有权从 a 转移给了 b,没有复制字符串内容。转移之后,a 就不能再访问这个值。
为什么要这样规定?如果两个变量都独立负责释放同一块内存,程序就可能重复释放它。Rust 在编译时明确谁负责这份数据。
确实需要两个独立的字符串时,可以写:
rust
let a = String::from("hello");
let b = a.clone();
println!("{a}, {b}");
这次 clone() 复制了内容,两个字符串各自管理自己的数据。
2. 为什么整数又没这个问题?
rust
let a = 10;
let b = a;
println!("{a}, {b}"); // 可以
同样是 let b = a,结果却不同。关键在类型:整数实现了 Copy,这里复制的是数值,a 仍然有效。
整数、布尔值、浮点数、字符都是常见的 Copy 类型。String 和 Vec 则不是。
为什么要区分?这些简单值可以直接复制,不需要像 String 那样管理堆上的资源。但不要把它记成"栈上的都能复制":赋值时能否保留原变量,要看类型有没有实现 Copy。
所以,只看赋值这一行还不够,也要看变量是什么类型。
3. 函数只是打印一下,为什么把字符串拿走了?
rust
fn show(text: String) {
println!("{text}");
}
fn main() {
let text = String::from("hello");
show(text);
println!("{text}"); // 编译错误
}
函数体确实只打印了字符串,但参数写的是 String。这个签名表示:函数接收字符串的所有权。
为什么调用后不能再用?传参和赋值遵循相同的移动规则。字符串进入 show 后,由参数 text 持有,函数结束时会被释放。
如果只需要读取,可以借用:
rust
fn show(text: &str) {
println!("{text}");
}
fn main() {
let text = String::from("hello");
show(&text);
println!("{text}"); // 可以
}
这里 &text 在调用时可以转换为 &str。函数临时读取字符串,所有权仍在调用方。读取文本的接口经常用 &str,因为它也能接收字符串字面量等文本切片。
遇到"调用后变量不能用"的报错,先看函数签名。很多时候需要的是借用,不必马上加 clone()。
4. 值归我,为什么还要 mut?
rust
let text = String::from("hello");
text.push_str(" world"); // 编译错误
这段里的 text 确实拥有字符串。问题在于,它是一个不可变绑定,不能通过它修改字符串。
rust
let mut text = String::from("hello");
text.push_str(" world"); // 可以
为什么所有权还不够?所有权回答谁管理值,mut 表达当前绑定允许修改。Rust 不会因为值归你,就默认你准备修改它。
也可以把已有的值交给一个可变的新绑定:
rust
let text = String::from("hello");
let mut text = text;
text.push_str(" world");
第二行叫变量遮蔽。右边是旧变量,左边是同名的新变量。它接收旧值的所有权,并被声明为可变。字符串内容没有因此复制。
这就解释了开头的两行。如果中间没有别的操作,一般也可以直接写成:
rust
let mut tools = build_tools(&ctx, args);
5. 不拿走所有权,也可以修改
有时希望函数帮忙修改字符串,修改完以后自己继续用:
rust
fn append(text: &mut String) {
text.push_str(" world");
}
fn main() {
let mut text = String::from("hello");
append(&mut text);
println!("{text}"); // hello world
}
&mut String 是可变引用。函数临时获得修改能力,没有取得字符串的所有权。
为什么这里用可变借用?因为调用方还要保留字符串,而函数需要修改它。对于这里的普通字符串变量,调用方也要声明 mut,才能创建这个可变借用。
| 参数 | 函数获得什么 |
|---|---|
String |
字符串的所有权 |
&String |
临时读取字符串的引用 |
&mut String |
临时修改字符串的引用 |
还有一个容易混淆的细节:引用变量自己不一定需要写 mut。
rust
let mut number = 1;
let reference = &mut number;
*reference = 2;
reference 没有声明为可变,但它的类型是可变引用,所以能修改指向的数据。let mut reference 控制的是能否给引用变量重新赋值。
另外,Cell、RefCell、Mutex 等类型提供了内部可变性,也可以通过不可变绑定修改内部数据。因此,"修改一定要在变量前写 mut"不是完整的规则。
6. 为什么读着读着,就不能改了?
rust
let mut text = String::from("hello");
let reader = &text;
text.push_str(" world"); // 编译错误
println!("{reader}");
reader 后面还要使用,因此这次不可变借用仍然有效。而 push_str 需要可变借用,两者发生了冲突。
为什么不能同时存在?例如,往字符串追加内容可能导致重新分配内存。如果允许保留旧引用再修改,引用就可能失效。Rust 的普通借用规则要求:对于同一份数据,可以同时有多个不可变借用,或者一个可变借用。
把读取放到修改之前,就可以:
rust
let mut text = String::from("hello");
let reader = &text;
println!("{reader}"); // reader 最后一次使用
text.push_str(" world");
引用变量还在作用域里,但借用通常能在最后一次使用之后结束。遇到借用冲突时,可以检查:这个引用后面是否真的还需要使用?
7. 返回引用,为什么也不行?
rust
fn make_text() -> &String {
let text = String::from("hello");
&text
}
这段不能编译。局部变量 text 会在函数结束时被释放,返回它的引用,调用方就会拿到指向失效数据的引用。
为什么不能加一个生命周期标记解决?生命周期描述引用与数据之间的有效关系,不能让已经要被释放的局部变量活得更久。
这里直接返回值就行:
rust
fn make_text() -> String {
let text = String::from("hello");
text
}
let text = make_text();
所有权转移给调用方,字符串不会在函数结束时被释放。返回 String 也不意味着必须复制一遍字符串内容。
8. 打印了一遍集合,集合怎么没了?
rust
let names = vec![String::from("Alice"), String::from("Bob")];
for name in names {
println!("{name}");
}
println!("{names:?}"); // 编译错误
如果你以前写 Java 或 JavaScript,看到这里可能会觉得很别扭:循环里没有删除元素,没有修改集合,只是打印了一下,为什么集合就不能用了?
这里说的"没了",是原来的 names 不能继续使用,并不是遍历把一个可用的集合变成了空集合。所有权在进入循环时就已经转移了。
for 会先把右边的东西变成迭代器
理解这段代码,可以先把 for 想成下面这种写法。这不是编译器展开的全部细节,但能看清所有权往哪里走:
rust
let mut iterator = IntoIterator::into_iter(names);
while let Some(name) = iterator.next() {
println!("{name}");
}
关键在第一行。names 的类型是 Vec<String>,按值调用 into_iter 会把整个 Vec 的所有权交给迭代器。从这一刻开始,原来的变量 names 就不能再使用了,甚至不需要等到循环执行完。
之后每次调用 next(),迭代器会取出一个 String,把它的所有权交给这一轮的 name。这里没有克隆字符串内容。
在这个例子中,println! 只是借用 name 来格式化输出。打印结束后,name 仍然持有字符串;这一轮循环结束时,name 被释放,对应的字符串也随之释放。
所以,即使循环体只打印,外面的集合仍然已经被移动。Rust 根据你选择的遍历方式处理所有权,不会因为循环体看起来只读取,就自动改成借用。
只想看一遍,就借用集合
如果后面还要用 names,可以在集合前加上 &:
rust
for name in &names {
println!("{name}");
}
println!("{names:?}"); // 可以
这次循环处理的是 &Vec<String>,没有拿走集合。每轮的 name 是 &String,也就是指向集合中某个元素的不可变引用。
可以把类型明确写出来观察:
rust
for name in &names {
let name: &String = name;
println!("{name}");
}
打印时没有复制字符串,也没有把元素从集合里取走。循环结束后,集合和里面的元素都还在。
这个写法也常写成:
rust
for name in names.iter() {
println!("{name}");
}
对于这里的 Vec,&names 和 names.iter() 都是在借用元素进行遍历。只读一遍时,通常不需要为了保留集合而写 names.clone()。
想改集合里的元素,就用可变借用
如果想给每个名字加上感叹号,可以这样写:
rust
let mut names = vec![String::from("Alice")];
for name in &mut names {
name.push('!');
}
println!("{names:?}"); // ["Alice!"]
这次每轮的 name 是 &mut String,所以修改的是集合里的原字符串。它也可以写成 names.iter_mut()。
注意,循环里的 name 不需要声明成 mut name。可变引用本身已经允许修改指向的数据,这和前面 reference 的例子是同一个道理。
不过,在这种循环中,不能同时调用 names.push(...):迭代器正在可变借用集合中的元素,再修改集合结构会产生借用冲突。扩容还可能改变元素所在的内存位置。如果需要新增元素,可以先收集待添加的值,等循环结束后再追加。
把三种方式放在一起,区别会更清楚:
| 写法 | 每轮拿到的类型(以 Vec<String> 为例) |
循环后还能用原集合吗? |
|---|---|---|
for name in names |
String |
不能,集合已被移动 |
for name in &names 或 names.iter() |
&String |
能,元素没有被取走 |
for name in &mut names 或 names.iter_mut() |
&mut String |
能,元素可能被修改 |
那按值遍历有什么用?
如果集合已经不需要了,或者你想把元素交给另一个地方,按值遍历就很合适:
rust
let names = vec![String::from("Alice"), String::from("Bob")];
let mut selected = Vec::new();
for name in names {
if name.starts_with('A') {
selected.push(name);
}
}
println!("{selected:?}"); // ["Alice"]
这里把符合条件的字符串直接移动到 selected,不需要克隆内容。被选中的 name 已经移走,不会在这一轮结束时释放;没有选中的字符串则会在对应那一轮结束时释放。
即使提前 break,原来的 names 也不会恢复可用:整个集合早已交给迭代器,退出循环时,迭代器会释放尚未取出的元素。
还有个常见误会:如果元素是整数,集合是不是就能继续用了?
rust
let numbers = vec![1, 2, 3];
for number in numbers {
println!("{number}");
}
println!("{numbers:?}"); // 仍然是编译错误
整数实现了 Copy,但 Vec<i32> 没有实现 Copy。被移动的是整个集合,元素能复制并不能阻止这次移动。
如果想保留集合,同时让每轮拿到一个整数值,可以用:
rust
for number in numbers.iter().copied() {
// number 是 i32,来自对借用元素的复制
println!("{number}");
}
选择遍历方式时,先问自己:后面还要不要这个集合?这一轮需要的是元素本身,还是它的引用?这两个问题通常比"编译器报错后该加什么符号"更容易判断。
9. Arc 可以共享,为什么还要 clone?
rust
use std::sync::Arc;
let a = Arc::new(String::from("hello"));
let b = a;
println!("{a}"); // 编译错误
Arc 支持共享,但这个赋值仍然移动了 Arc 本身。需要两个持有者,就要明确克隆句柄:
rust
let a = Arc::new(String::from("hello"));
let b = Arc::clone(&a);
println!("{a}, {b}");
为什么这里的克隆不会复制字符串?Arc::clone 增加引用计数,两个指针仍然指向同一份底层数据。最后一个持有者释放时,数据才会释放。
| 操作 | 结果 |
|---|---|
String::clone() |
复制字符串内容,得到独立字符串 |
Arc::clone(&a) |
增加引用计数,共享底层数据 |
这也是项目代码中"先创建,再 clone 给不同组件"的常见原因:每个组件需要持有一个句柄,又必须连接到同一份状态。创建两次通常会得到两份独立状态,效果不同。
不过,共享持有不代表能随意修改。需要多个地方共享修改的数据时,常见做法是用 Arc<Mutex<T>>,通过锁取得访问能力。
留几道题,试试能不能说出原因
每道题都先判断能否编译,再解释原因。如果不能编译,尝试做最小修改。能把原因说清楚,比只把报错消掉更有用。
题目 1:相同的赋值,结果为什么不同?
考查 Copy 和移动的区别。下面两组代码分别能否编译?
rust
let a = 100;
let b = a;
println!("{a}, {b}");
rust
let x = String::from("Rust");
let y = x;
println!("{x}, {y}");
题目 2:只算长度,需要拿走字符串吗?
考查函数签名如何表达借用。修改函数签名和调用方式,让最后一行正常执行,不使用 clone()。
rust
fn length(text: String) -> usize {
text.len()
}
fn main() {
let text = String::from("hello");
let size = length(text);
println!("{text}: {size}");
}
题目 3:所有权已经转移,为什么不能 push?
考查所有权和可变性的区别。第二行接收了所有权,第三行能否编译?如果不能,应该修改哪里?
rust
let values = vec![1, 2, 3];
let values = values;
values.push(4);
题目 4:引用变量没写 mut,能修改数据吗?
考查绑定可变性和可变引用的区别。判断这段代码能否编译,并解释 *reference 修改的是谁。
rust
let mut number = 10;
let reference = &mut number;
*reference = 20;
println!("{number}");
题目 5:只换个顺序,为什么结果不同?
考查借用的结束位置。下面 A、B 哪段能编译?
代码 A:
rust
let mut text = String::from("hello");
let reader = &text;
println!("{reader}");
text.push('!');
代码 B:
rust
let mut text = String::from("hello");
let reader = &text;
text.push('!');
println!("{reader}");
题目 6:遍历结束后还要用集合
考查按值遍历和借用遍历。只修改 for 所在的那一行,让最后的输出正常执行。
rust
let words = vec![String::from("hello"), String::from("world")];
for word in words {
println!("{word}");
}
println!("一共有 {} 个单词", words.len());
题目 7:支持共享的 Arc,也会被移动吗?
考查共享句柄的移动和克隆。修改第二个 let,让两个变量都能使用。修改后,底层有几份字符串数据?
rust
use std::sync::Arc;
let a = Arc::new(String::from("hello"));
let b = a;
println!("{a}");
println!("{b}");
刚开始写 Rust,很容易把解决报错变成试排列组合:加个 &,不行再加 mut,最后加 clone()。暂时编译通过了,下次还是会卡住。
下次遇到类似错误,可以先沿着值的使用过程看一遍:它有没有被移动,借出去的引用后面还用不用,当前操作是否需要修改。把这几件事看清楚,编译器的提示也会更容易读懂。