Rust 所有权:为什么值是我的,我却不能改?

2,800 字#Rust #学习笔记

读 Rust 代码时,很容易卡在这样两行上:

let tools = build_tools(&ctx, args);
let mut tools = tools;

同一个名字怎么能声明两次?第二行已经拿到了所有权,为什么还要写 mut?既然东西归我,难道还不能改吗?

如果之前主要写 Java、C# 或 JavaScript,这种困惑很自然。我们习惯了把对象交给另一个变量,再从任何一个变量修改它。Rust 把这里的几件事分开了,需要稍微换一下思路。

这篇笔记从这些具体问题讲起。先看常见场景,最后留几道题,可以自己判断一下,不用急着记术语。

先分清三个问题

看到一段 Rust 代码,可以先问:

  • 所有权:谁持有这个值,负责在不再需要时释放它?
  • 借用:谁临时使用这个值?
  • 可变性:当前使用方式是否允许修改它?

拥有一个值,并不意味着当前变量就能修改它。把值交给别人,也不一定是在复制数据。后面的报错,大多能从这三个问题里找到原因。

1. 换个变量接住,原来的变量就不能用了

let a = String::from("hello");
let b = a;

println!("{b}"); // 可以
println!("{a}"); // 编译错误:a 的值已经被移动

第一次遇到这个错误,可能会觉得:我只是赋了个值,怎么就不能用了?

String 管理着堆上的字符串数据。这里的赋值把所有权从 a 转移给了 b,没有复制字符串内容。转移之后,a 就不能再访问这个值。

为什么要这样规定?如果两个变量都独立负责释放同一块内存,程序就可能重复释放它。Rust 在编译时明确谁负责这份数据。

确实需要两个独立的字符串时,可以写:

let a = String::from("hello");
let b = a.clone();

println!("{a}, {b}");

这次 clone() 复制了内容,两个字符串各自管理自己的数据。

2. 为什么整数又没这个问题?

let a = 10;
let b = a;

println!("{a}, {b}"); // 可以

同样是 let b = a,结果却不同。关键在类型:整数实现了 Copy,这里复制的是数值,a 仍然有效。

整数、布尔值、浮点数、字符都是常见的 Copy 类型。String 和 Vec 则不是。

为什么要区分?这些简单值可以直接复制,不需要像 String 那样管理堆上的资源。但不要把它记成“栈上的都能复制”:赋值时能否保留原变量,要看类型有没有实现 Copy。

所以,只看赋值这一行还不够,也要看变量是什么类型。

3. 函数只是打印一下,为什么把字符串拿走了?

fn show(text: String) {
    println!("{text}");
}

fn main() {
    let text = String::from("hello");
    show(text);
    println!("{text}"); // 编译错误
}

函数体确实只打印了字符串,但参数写的是 String。这个签名表示:函数接收字符串的所有权。

为什么调用后不能再用?传参和赋值遵循相同的移动规则。字符串进入 show 后,由参数 text 持有,函数结束时会被释放。

如果只需要读取,可以借用:

fn show(text: &str) {
    println!("{text}");
}

fn main() {
    let text = String::from("hello");
    show(&text);
    println!("{text}"); // 可以
}

这里 &text 在调用时可以转换为 &str。函数临时读取字符串,所有权仍在调用方。读取文本的接口经常用 &str,因为它也能接收字符串字面量等文本切片。

遇到“调用后变量不能用”的报错,先看函数签名。很多时候需要的是借用,不必马上加 clone()。

4. 值归我,为什么还要 mut?

let text = String::from("hello");
text.push_str(" world"); // 编译错误

这段里的 text 确实拥有字符串。问题在于,它是一个不可变绑定,不能通过它修改字符串。

let mut text = String::from("hello");
text.push_str(" world"); // 可以

为什么所有权还不够?所有权回答谁管理值,mut 表达当前绑定允许修改。Rust 不会因为值归你,就默认你准备修改它。

也可以把已有的值交给一个可变的新绑定:

let text = String::from("hello");
let mut text = text;
text.push_str(" world");

第二行叫变量遮蔽。右边是旧变量,左边是同名的新变量。它接收旧值的所有权,并被声明为可变。字符串内容没有因此复制。

这就解释了开头的两行。如果中间没有别的操作,一般也可以直接写成:

let mut tools = build_tools(&ctx, args);

5. 不拿走所有权,也可以修改

有时希望函数帮忙修改字符串,修改完以后自己继续用:

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。

let mut number = 1;
let reference = &mut number;
*reference = 2;

reference 没有声明为可变,但它的类型是可变引用,所以能修改指向的数据。let mut reference 控制的是能否给引用变量重新赋值。

另外,Cell、RefCell、Mutex 等类型提供了内部可变性,也可以通过不可变绑定修改内部数据。因此,“修改一定要在变量前写 mut”不是完整的规则。

6. 为什么读着读着,就不能改了?

let mut text = String::from("hello");
let reader = &text;

text.push_str(" world"); // 编译错误
println!("{reader}");

reader 后面还要使用,因此这次不可变借用仍然有效。而 push_str 需要可变借用,两者发生了冲突。

为什么不能同时存在?例如,往字符串追加内容可能导致重新分配内存。如果允许保留旧引用再修改,引用就可能失效。Rust 的普通借用规则要求:对于同一份数据,可以同时有多个不可变借用,或者一个可变借用。

把读取放到修改之前,就可以:

let mut text = String::from("hello");
let reader = &text;
println!("{reader}"); // reader 最后一次使用

text.push_str(" world");

引用变量还在作用域里,但借用通常能在最后一次使用之后结束。遇到借用冲突时,可以检查:这个引用后面是否真的还需要使用?

7. 返回引用,为什么也不行?

fn make_text() -> &String {
    let text = String::from("hello");
    &text
}

这段不能编译。局部变量 text 会在函数结束时被释放,返回它的引用,调用方就会拿到指向失效数据的引用。

为什么不能加一个生命周期标记解决?生命周期描述引用与数据之间的有效关系,不能让已经要被释放的局部变量活得更久。

这里直接返回值就行:

fn make_text() -> String {
    let text = String::from("hello");
    text
}

let text = make_text();

所有权转移给调用方,字符串不会在函数结束时被释放。返回 String 也不意味着必须复制一遍字符串内容。

8. 打印了一遍集合,集合怎么没了?

let names = vec![String::from("Alice"), String::from("Bob")];

for name in names {
    println!("{name}");
}

println!("{names:?}"); // 编译错误

问题在 for name in names:它按值遍历,消费这个集合,逐个取出元素。

为什么只是打印也会这样?遍历方式由循环表达式决定,不由循环体决定。循环体只读取,并不会自动把按值遍历变成借用遍历。

只读取时写:

for name in &names {
    println!("{name}");
}

println!("{names:?}"); // 可以

需要修改元素时写:

let mut names = vec![String::from("Alice")];

for name in &mut names {
    name.push('!');
}

这三个写法很值得放在一起看:names 交出集合,&names 借来读取,&mut names 借来修改。

9. Arc 可以共享,为什么还要 clone?

use std::sync::Arc;

let a = Arc::new(String::from("hello"));
let b = a;

println!("{a}"); // 编译错误

Arc 支持共享,但这个赋值仍然移动了 Arc 本身。需要两个持有者,就要明确克隆句柄:

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 和移动的区别。下面两组代码分别能否编译?

let a = 100;
let b = a;
println!("{a}, {b}");
let x = String::from("Rust");
let y = x;
println!("{x}, {y}");

题目 2:只算长度,需要拿走字符串吗?

考查函数签名如何表达借用。修改函数签名和调用方式,让最后一行正常执行,不使用 clone()。

fn length(text: String) -> usize {
    text.len()
}

fn main() {
    let text = String::from("hello");
    let size = length(text);
    println!("{text}: {size}");
}

题目 3:所有权已经转移,为什么不能 push?

考查所有权和可变性的区别。第二行接收了所有权,第三行能否编译?如果不能,应该修改哪里?

let values = vec![1, 2, 3];
let values = values;
values.push(4);

题目 4:引用变量没写 mut,能修改数据吗?

考查绑定可变性和可变引用的区别。判断这段代码能否编译,并解释 *reference 修改的是谁。

let mut number = 10;
let reference = &mut number;
*reference = 20;
println!("{number}");

题目 5:只换个顺序,为什么结果不同?

考查借用的结束位置。下面 A、B 哪段能编译?

代码 A:

let mut text = String::from("hello");
let reader = &text;
println!("{reader}");
text.push('!');

代码 B:

let mut text = String::from("hello");
let reader = &text;
text.push('!');
println!("{reader}");

题目 6:遍历结束后还要用集合

考查按值遍历和借用遍历。只修改 for 所在的那一行,让最后的输出正常执行。

let words = vec![String::from("hello"), String::from("world")];

for word in words {
    println!("{word}");
}

println!("一共有 {} 个单词", words.len());

题目 7:支持共享的 Arc,也会被移动吗?

考查共享句柄的移动和克隆。修改第二个 let,让两个变量都能使用。修改后,底层有几份字符串数据?

use std::sync::Arc;

let a = Arc::new(String::from("hello"));
let b = a;

println!("{a}");
println!("{b}");

刚开始写 Rust,很容易把解决报错变成试排列组合:加个 &,不行再加 mut,最后加 clone()。暂时编译通过了,下次还是会卡住。

下次遇到类似错误,可以先沿着值的使用过程看一遍:它有没有被移动,借出去的引用后面还用不用,当前操作是否需要修改。把这几件事看清楚,编译器的提示也会更容易读懂。