跳转到内容

Rust 借用

在所有权那一页你遇到过一个麻烦:把 String 传给函数,它就会被移动走,之后你就不能再用它了。如果总是把值移来移去(或者克隆一份),写起来会很累。Rust 给出的办法就是借用(borrowing)。

借用的意思是:让一段代码使用某个值,但不拿走它的所有权。就像把书借给朋友:朋友读完还给你,书一直是你的。

在值前面加上 &,就得到一个引用(reference)。有了引用,函数就能查看数据,却不必拥有它:

fn print_length(s: &String) { // 借用 s,并不拥有它
println!("长度:{}", s.len());
}
fn main() {
let text = String::from("hello");
print_length(&text); // 把 text 借给函数
println!("还是我的:{text}"); // ✅ text 在这里依然有效
}

输出:

长度:5
还是我的:hello
▶ 动手试一试

&text 的意思是“让这个函数借用一下 text”。函数只是借用,所有权还留在 main 手里,所以之后你还能继续用 text。对比一下所有权那一页:同样的代码少了 &,就会报错。

普通的 & 引用是只读的:只能看,不能动。如果函数需要修改借来的值,就要用 &mut 创建一个可变引用(mutable reference):

fn add_excitement(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut text = String::from("hello");
add_excitement(&mut text); // 以可变的方式借出去
println!("{text}"); // hello!!!
}

输出:

hello!!!
▶ 动手试一试

注意这里有三处必须对得上:text 声明时加了 mut,函数参数要的是 &mut String,调用时传的是 &mut text。想修改借来的值,这三样缺一不可。

Rust 对引用有两条规则。它们是为了防止数据竞争(data race):两段代码同时访问同一份数据,结果把数据弄坏了,这是一整类非常难查的 bug。

在任意时刻,你只能二选一:

  • 任意多个只读引用(&),或者
  • 恰好一个可变引用(&mut),

但不能同时拥有两者。

说白了就是:可以有很多个读者,或者只有一个写者,两者不能同时存在。下面这段代码就违反了规则:

fn main() {
let mut text = String::from("hello");
let r1 = &text; // 一个只读借用
let r2 = &mut text; // ❌ r1 还在,不能再可变借用
println!("{r1} {r2}");
}

编译器会拦住你:

error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable

这条错误的意思是:text 已经被不可变地借用了(r1),所以不能再把它以可变的方式借出去。

你可能以为借用会一直持续到作用域的 }。其实不是。借用只持续到引用最后一次被使用的地方。在那之后,这个值就可以再次被借用,哪怕是可变借用。

所以上面那个出错的例子,只要在创建 r2 之前用完 r1,就能修好:

fn main() {
let mut text = String::from("hello");
let r1 = &text;
println!("正在读:{r1}"); // r1 最后一次使用,它的借用到这里结束
let r2 = &mut text; // ✅ 没问题:已经没人在读了
r2.push_str(", world");
println!("{text}");
}

输出:

正在读:hello
hello, world
▶ 动手试一试

规则还是“很多个读者或者一个写者”,只不过编译器检查的是每个引用真正被使用的那一段代码。写者 r2 出现时,读者 r1 已经用完了;最后一个 println! 再次读取 text 之前,r2 也已经用完了。没有任何重叠,Rust 就放行了。

这是修复借用报错最常见的办法:调整顺序。先把需要读的都读完,再去写(或者反过来),不要把读和写交错在一起。

这些规则完全在编译期检查,所以程序运行时没有任何开销。Rust 正是靠这一点,既能保证“没有数据竞争”,又能跑得和 C 一样快:安全性在程序启动之前就已经被证明了。

先自己动手试一试,可以用 Rust Playground,也可以用本地项目。如果编译器报错,认真读一读错误信息:它通常会准确指出借用在哪里重叠了。

写一个函数 total_letters,它借用一个 Vec<String>,返回所有单词加起来一共有多少个字母。调用完之后,main 还要能打印这个 vector。

提示:参数类型是 &Vec<String>,调用时传 &words。用 for word in list 遍历借来的 vector,把 word.len() 加起来。

查看答案
fn total_letters(list: &Vec<String>) -> usize {
let mut total = 0;
for word in list {
total += word.len();
}
total
}
fn main() {
let words = vec![String::from("borrow"), String::from("check")];
let count = total_letters(&words); // 把 vector 借出去
println!("一共 {count} 个字母");
println!("{:?} 仍然属于 main", words);
}

输出:

一共 11 个字母
["borrow", "check"] 仍然属于 main
▶ 动手试一试

这个函数只需要看一看这些单词,所以一个只读的 & 借用就够了。单词保留英文,是因为 len() 数的是字节数:一个汉字占 3 个字节,换成中文结果就不是“字母数”了。如果你把参数写成 list: Vec<String>,vector 就会被移动进函数,最后那个 println! 也就编译不过了。

写两个函数:add_item 往购物清单里添加一项,print_list 打印清单里的每一项。添加三项,然后打印清单。

提示:一个函数会修改清单,另一个只是读取。哪个需要 &mut Vec<String>,哪个用 &Vec<String> 就够了?要把 &str 变成 String,可以用 String::from(item)。

查看答案
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &Vec<String>) {
for item in list {
println!("- {item}");
}
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "牛奶");
add_item(&mut list, "面包");
add_item(&mut list, "苹果");
print_list(&list);
}

输出:

- 牛奶
- 面包
- 苹果
▶ 动手试一试

add_item 会修改清单,所以它拿的是 &mut 借用,main 里的 list 也必须是 mut。print_list 只读不改,普通的 & 就够了。每次 &mut list 借用都在那次调用返回时结束,所以下一次调用可以再借一次。

下面这个程序编译不过。修改它,让它打印 Score: 10 points,并且保留 first 和 second 两个变量。

fn main() {
let mut message = String::from("Score:");
let first = &mut message;
let second = &mut message;
first.push_str(" 10");
second.push_str(" points");
println!("{message}");
}
error[E0499]: cannot borrow `message` as mutable more than once at a time
--> src/main.rs:4:18
|
3 | let first = &mut message;
| ------------ first mutable borrow occurs here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

编译器说:message 不能同时被可变借用两次。第 3 行是第一次可变借用,第 4 行是第二次,而第 5 行还在使用第一次的借用。

提示:同一时间只能有一个写者。回头看看上面的借用什么时候结束?:能不能在创建 second 之前就把 first 用完?

查看答案
fn main() {
let mut message = String::from("Score:");
let first = &mut message;
first.push_str(" 10"); // first 最后一次使用:它的借用到这里结束
let second = &mut message;
second.push_str(" points");
println!("{message}");
}

输出:

Score: 10 points
▶ 动手试一试

原来的代码里,第 4 行创建 second 时,first 还要在第 5 行被使用,这就等于同时有两个写者。把 first.push_str 挪到前面,first 在 second 出现之前就用完了,所以任何时候都只有一个 &mut。

  • 借用(&)让代码使用一个值,却不拿走所有权:用完会还回来。
  • 普通的 & 引用是只读的;要修改借来的值,就用 &mut。
  • 想通过引用修改值,变量、参数和调用处都需要 mut / &mut。
  • 规则:很多个读者或者一个写者,两者不能同时存在。
  • 借用在引用最后一次使用时结束,所以调整代码顺序常常就能修好借用报错。
  • 这一切都在编译期检查,运行时没有任何开销。

小测验

1. 把值传给函数时,在值前面加 & 有什么作用?

2. 按照借用规则,下面哪种组合可以同时存在?

3. 想通过引用修改一个值,需要什么?

4. 为什么 Rust 执行借用规则不会带来任何运行时性能开销?

得分: 0 / 4