Rust 借用
在所有权那一页你遇到过一个麻烦:把 String 传给函数,它就会被移动走,之后你就不能再用它了。如果总是把值移来移去(或者克隆一份),写起来会很累。Rust 给出的办法就是借用(borrowing)。
借用的意思是:让一段代码使用某个值,但不拿走它的所有权。就像把书借给朋友:朋友读完还给你,书一直是你的。
引用:用 & 借用
Section titled “引用:用 & 借用”在值前面加上 &,就得到一个引用(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。对比一下所有权那一页:同样的代码少了 &,就会报错。
可变引用:借来修改
Section titled “可变引用:借来修改”普通的 & 引用是只读的:只能看,不能动。如果函数需要修改借来的值,就要用 &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),所以不能再把它以可变的方式借出去。
借用什么时候结束?
Section titled “借用什么时候结束?”你可能以为借用会一直持续到作用域的 }。其实不是。借用只持续到引用最后一次被使用的地方。在那之后,这个值就可以再次被借用,哪怕是可变借用。
所以上面那个出错的例子,只要在创建 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}");}输出:
正在读:hellohello, world规则还是“很多个读者或者一个写者”,只不过编译器检查的是每个引用真正被使用的那一段代码。写者 r2 出现时,读者 r1 已经用完了;最后一个 println! 再次读取 text 之前,r2 也已经用完了。没有任何重叠,Rust 就放行了。
这是修复借用报错最常见的办法:调整顺序。先把需要读的都读完,再去写(或者反过来),不要把读和写交错在一起。
为什么这很重要
Section titled “为什么这很重要”这些规则完全在编译期检查,所以程序运行时没有任何开销。Rust 正是靠这一点,既能保证“没有数据竞争”,又能跑得和 C 一样快:安全性在程序启动之前就已经被证明了。
先自己动手试一试,可以用 Rust Playground,也可以用本地项目。如果编译器报错,认真读一读错误信息:它通常会准确指出借用在哪里重叠了。
1. 只数不拿
Section titled “1. 只数不拿”写一个函数 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! 也就编译不过了。
2. 购物清单
Section titled “2. 购物清单”写两个函数: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 借用都在那次调用返回时结束,所以下一次调用可以再借一次。
3. 两个写者同时出现
Section titled “3. 两个写者同时出现”下面这个程序编译不过。修改它,让它打印 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 here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | 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
- 引用与借用 — Rust 程序设计语言:官方教程中的对应章节,还讲了悬垂引用,以及 Rust 为什么拒绝创建它们。
- 借用 — 通过例子学 Rust:
&借用的可运行示例,后面还有讲可变借用和别名的页面。 - Non-lexical lifetimes — The Edition Guide(英文):正是这项改动,让借用在最后一次使用时结束,而不是等到右花括号。
- Slice 类型 — Rust 程序设计语言:只借用字符串或数组的一部分,学完引用之后自然的下一步。
reference— 标准库文档(英文):&T和&mut T到底是什么,以及它们实现了哪些 trait。