第 04 篇
所有权与借用
解释所有权、移动和借用,区分绑定的 mut、引用的 &mut、解引用、再借用和引用复制。
Hex4C59 15 分钟阅读 更新于 2026-10-17
编程·原创·实战教程
前三章把整数传给函数后,调用方仍可以继续使用这些整数。换成可以增长的文本,你可能第一次遇到“这个值已经被移动”的报错。
本章从这个报错开始,说明一个值归谁管理,以及函数怎样读取或修改调用方的数据。学完后,你应该能让一个函数打印标题,让另一个函数修改标题,并在调用后继续使用原变量。暂时不需要自己写生命周期标注,也不需要背内存布局。
继续使用自己的 hello-rust 项目。下面的完整程序可以替换整个 src/main.rs,保存后运行 cargo run。明确写着“故意无法编译”的例子,改用 cargo check 观察错误,完成实验后恢复正常代码。
String 拥有一段可以增长的文本 #
第二章中的 "学习 Rust" 是字符串字面量。现在用它创建一个 String,再追加文字。完整程序:
fn main() {
let mut title = String::from("学习 Rust");
title.push_str(",每天一点");
title.push('!');
println!("{title}");
}预期输出:
学习 Rust,每天一点!String::from(...) 创建一个拥有文本内容的字符串。push_str 追加一段文本,push 追加一个 char。前者使用双引号,后者使用单引号。title 需要声明为 mut,因为我们要修改它。
字符串可以增长,存放文本的空间也需要确定由谁负责释放。Rust 用所有权规则决定谁负责一个值:值可以交给新的所有者;拥有它的变量离开作用域时,值会被丢弃,所持资源按相应规则清理。在这个例子中,标题变量负责管理字符串,后面创建的引用只负责访问,不接管清理责任。
先用局部变量观察作用域。完整程序:
fn main() {
let title = String::from("学习 Rust");
{
let note = String::from("先运行,再修改");
println!("{title}:{note}");
}
println!("标题仍可使用:{title}");
}预期输出:
学习 Rust:先运行,再修改
标题仍可使用:学习 Rustnote 只在内层花括号中可用。离开那里后,它拥有的字符串被清理。title 定义在外层,还能继续使用。
所有权检查主要发生在编译时。下面几个实验会让你看到,编译器怎样拒绝可能继续使用无效数据的写法。
移动后,旧绑定不能继续使用 #
下面是故意无法编译的完整程序:
fn main() {
let first = String::from("Rust");
let second = first;
println!("{first} {second}");
}运行 cargo check,应看到 E0382,错误说明中会提到值已经被移动。下载包的 compile-fail/moved_value.rs 保存了同一个例子,在解压后的 rust-course 目录也可以运行:
rustc --edition 2024 compile-fail/moved_value.rs关键在 let second = first;:这个 String 的所有权交给了 second,没有自动复制出另一份独立文本。之后通过 first 读取整个字符串就不合法了。接下来由新绑定管理这份字符串,并在需要时清理它;旧绑定不能继续把同一份字符串当成自己拥有的值使用。
“移动”描述值的使用权限发生了转移,不必把它想象成文字在内存里搬到了另一个地址。现在只用新的所有者,程序就能运行。完整修复版本:
fn main() {
let first = String::from("Rust");
let second = first;
println!("{second}");
}预期输出:
Rust新绑定是否允许修改,由它自己的声明决定。let mut title = original; 这类写法,可以接收原本通过不可变绑定持有的字符串,再通过新绑定修改它。原绑定是否写了 mut,不影响新绑定的这个选择。
下面这张图跟踪同一份字符数据:先由 title 拥有,移动后改由 moved 拥有,再从 moved 创建 &str 视图。三行表示依次发生的状态,不是同时存在的三份字符串。
图中的实线表示所有权,虚线表示借用;文字和线型也标明了关系。它只解释谁管理、谁借用,不表示变量在栈或堆中的实际布局。借用的完整代码见后面的“借用允许临时访问”一节。
函数参数和返回值也会转移所有权 #
第三章说过,调用函数时要把输入交给参数。参数类型如果是 String,传入字符串就会转移所有权。下面仍是故意无法编译的完整程序:
fn print_title(title: String) {
println!("标题:{title}");
}
fn main() {
let title = String::from("学习借用");
print_title(title);
println!("调用后:{title}");
}print_title(title) 把字符串移进函数。函数结束时,参数持有的字符串被丢弃。调用方最后一行仍想访问旧绑定,因此得到 E0382。
如果函数需要取得字符串、处理后再交回来,可以返回 String。完整程序:
fn add_note(mut title: String) -> String {
title.push_str("(已复习)");
title
}
fn main() {
let title = String::from("学习所有权");
let title = add_note(title);
println!("{title}");
}预期输出:
学习所有权(已复习)调用时,字符串进入 add_note;返回时,所有权又交给调用方。第二条 let title 建立新绑定,接收返回值,这是第二章讲过的遮蔽。函数体最后的 title 没有分号,因为它是返回结果。
参数中的 mut title 允许函数修改自己已经取得的字符串。它与下一节的可变借用有区别:这里的参数拥有字符串,而可变借用只是暂时获得修改权限。
借用允许临时访问 #
刚才的打印函数只想看一眼标题,没必要拿走整个字符串。把参数改成共享引用,就能在调用后继续使用原值。完整程序:
fn show_title(title: &str) {
println!("标题:{title}");
}
fn main() {
let title = String::from("学习借用");
show_title(&title);
show_title("先从一句固定文本开始");
println!("调用后:{title}");
}预期输出:
标题:学习借用
标题:先从一句固定文本开始
调用后:学习借用引用让函数访问已有的数据,而不取得这份数据的所有权。&title 创建共享借用,字符串仍由 main 中的 title 管理。
&str 是一个文本切片类型,让函数只读地访问已有文本的一段或全部,不创建独立文本。这里把 &String 作为参数传入时,Rust 可以把它转换成函数需要的 &str;字符串字面量也能直接传入。第五章会继续比较 String 和 &str。
对本章的字符串,不能通过共享引用追加文字。只读取标题的函数接收 &str,也能让调用者从签名上看出这个意图。
可变借用修改的是原来的字符串 #
需要修改调用方的字符串时,接收 &mut String。完整程序:
fn add_suffix(title: &mut String) {
title.push_str("(已复习)");
}
fn main() {
let mut title = String::from("学习 Rust");
add_suffix(&mut title);
println!("{title}");
}预期输出:
学习 Rust(已复习)这里没有返回新字符串。add_suffix 通过可变引用修改了原来的 title,调用结束后,main 直接打印修改后的内容。
这几个位置各有作用:
| 写法 | 含义 |
|---|---|
let mut title | 这个绑定允许修改其持有的值 |
参数 title: &mut String | 函数暂时取得字符串的独占修改权限 |
调用时 &mut title | 把原字符串以可变借用的形式交给函数 |
函数参数不需要写成 mut title: &mut String 才能调用 push_str。&mut String 已经允许修改它指向的字符串;参数绑定本身是否可重新赋值,是另一件事。
把常用签名放在一起比较:
| 参数形式 | 这次调用能做什么 | 调用后调用方是否还能使用原值 |
|---|---|---|
String | 取得所有权,可以自行保留或丢弃 | 原绑定已移动,除非重新取得所有权 |
&str | 查看文本 | 可以 |
&mut String | 修改借来的字符串 | 借用结束后可以 |
选择签名前,先问函数要做什么:保留数据、读取数据,还是修改现有数据。遇到报错时,也先检查函数需要哪种权限,再调整参数。
同一份数据不能一边被共享引用使用,一边被修改 #
下面是故意无法编译的完整程序,下载包中对应 compile-fail/borrow_conflict.rs:
fn main() {
let mut text = String::from("Rust");
let borrowed = &text;
text.push('!');
println!("{borrowed}");
}运行 cargo check,或在 rust-course 目录运行 rustc --edition 2024 compile-fail/borrow_conflict.rs,可以观察 E0502。
最后一行还要使用 borrowed,所以共享借用需要跨过 push 这一行。push 要修改同一个字符串,而共享借用在仍需使用时不允许这种修改,编译器因此拒绝。判断冲突要看引用是否还会用到,不能只看这些语句在代码中一前一后的位置。
先用完共享引用,再修改原值,就符合这里的规则。完整修复版本:
fn main() {
let mut text = String::from("Rust");
let borrowed = &text;
println!("{borrowed}");
text.push('!');
println!("{text}");
}预期输出:
Rust
Rust!borrowed 最后用在第一个 println!。编译器能判断后面不再需要这次共享借用,因此允许修改。不能只看引用变量还在不在同一对花括号里,还要看它之后是否继续使用。
多个共享引用可以一起读同一份数据。可变借用则需要独占访问;下面故意同时保留两次可变借用,也不能通过编译:
fn main() {
let mut text = String::from("Rust");
let first = &mut text;
let second = &mut text;
first.push('!');
second.push('?');
}这里应得到 E0499。first 还没用完,就创建了同一字符串的 second 可变借用。可以先完成第一段修改,再开始第二段。完整修复版本:
fn main() {
let mut text = String::from("Rust");
let first = &mut text;
first.push('!');
let second = &mut text;
second.push('?');
println!("{text}");
}预期输出:
Rust!?对本章的普通引用,可以记为:同一份数据在引用需要使用的期间,要么有多个共享引用,要么有一个独占的可变引用。同一个函数里的顺序代码也要遵守这条规则。
Copy 与 clone #
为什么第三章可以把整数传给函数,之后还继续使用它?整数实现了 Copy,赋值或按值传参时可以复制数值,不使原绑定失效。完整程序:
fn twice(value: u32) -> u32 {
value * 2
}
fn main() {
let count = 3;
let copied = count;
let result = twice(count);
println!("原值:{count},副本:{copied},计算结果:{result}");
}预期输出:
原值:3,副本:3,计算结果:6布尔值、char 等一些简单类型也具有这个性质。第三章使用的固定长度整数数组同样可以复制。能否复制取决于类型,不能只看变量名,或简单按“内容小不小”判断。
String 没有 Copy。如果确实需要两份以后可以分别修改的文本,显式调用 clone()。完整程序:
fn main() {
let mut title = String::from("学习 Rust");
let backup = title.clone();
title.push('!');
println!("当前:{title}");
println!("备份:{backup}");
}预期输出:
当前:学习 Rust!
备份:学习 RustString 的 clone() 创建独立字符串,通常需要分配并复制文本。它适合保存一份独立备份。函数只需要打印或统计时,借用已经足够,不必为了让旧绑定还能用就到处加 clone()。
引用不能指向已经销毁的数据 #
下面是故意无法编译的完整程序,下载包中对应 compile-fail/dangling.rs:
fn invalid() -> &'static str {
let text = String::from("Rust");
&text
}
fn main() {
println!("{}", invalid());
}运行 cargo check,或运行 rustc --edition 2024 compile-fail/dangling.rs,应得到 E0515。
函数返回时,局部变量 text 拥有的字符串就被清理了。调用方如果只拿到它的引用,将没有有效的文本可读。
'static 是一种生命周期标注,这里要求返回的引用指向整个程序运行期间都有效的数据。写上这个标注不会让局部字符串自动活得更久;第九章再解释这类标注的具体写法。
函数自己创建了文本,并希望交给调用方保留时,直接返回 String。完整修复版本:
fn make_title() -> String {
String::from("学习 Rust")
}
fn main() {
let title = make_title();
println!("{title}");
}预期输出:
学习 Rust这里所有权随返回值转移给调用方。函数返回了,字符串仍由 main 中的 title 管理,直到这个绑定离开作用域。
语法细读:mut 改绑定,&mut 提供访问权限 #
本章语法细读里的每个 Rust 程序可以分别替换自己项目的 src/main.rs:正常例子运行 cargo run,故意失败的例子运行 cargo check。下载包另保存为独立文件,直接编译的方法见下载包里的 syntax/README.md。各个程序相互独立。
mut 出现的位置不同,约束的对象也不同。先用普通整数拆开引用,避免把字符串方法调用和引用规则混在一起。
变量名可以改绑,与引用能否修改数据是两件事 #
| 声明 | 能给这个变量换一个引用吗 | 能通过它修改普通整数吗 |
|---|---|---|
let view = &number; | 不能 | 不能 |
let mut view = &number; | 能 | 不能 |
let slot = &mut number; | 不能 | 能 |
let mut slot = &mut number; | 能 | 能 |
表中的“能”仍要求其他借用和类型约束允许。这张表只讨论普通整数的引用;有些类型通过专门接口允许共享引用修改内部数据,这称为内部可变性,需要按那些类型的接口另行理解。
完整程序,下载包中为 syntax/syntax04-binding-mut.rs:
fn increment(slot: &mut i32) {
*slot += 1;
}
fn main() {
let mut first = 1;
let mut second = 10;
increment(&mut first);
let mut slot = &mut first;
*slot += 1;
slot = &mut second;
*slot += 3;
println!("{first} / {second}");
let mut view = &first;
println!("改绑前:{view}");
view = &second;
println!("改绑后:{view}");
}预期输出:
3 / 13
改绑前:3
改绑后:13increment 的参数绑定没有 mut,但它的类型是 &mut i32,已经允许修改所指的整数。*slot 是解引用表达式,指向被引用的位置;*slot += 1 修改那个位置,slot = &mut second 才是在给引用变量重新赋值。
最后的 view 有 mut,所以能从指向 first 改成指向 second,但类型一直是共享引用 &i32。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax04-shared-write.rs:
fn main() {
let number = 1;
let mut view = &number;
*view = 2;
println!("{view}");
}预期诊断包含 E0594。
给绑定加 mut 没有把 &i32 改成 &mut i32。这次失败发生在对 *view 的写入上,不是对变量 view 的重新赋值上。
&mut *slot 是从已有可变引用再借用 #
完整程序,下载包中为 syntax/syntax04-reborrow.rs:
fn main() {
let mut number = 10;
let slot = &mut number;
*slot += 1;
let shorter = &mut *slot;
*shorter += 2;
*slot += 3;
println!("{number}");
}预期输出:
16先由 slot 借用 number,再用 &mut *slot 建立较短的可变借用 shorter。shorter 还要使用时,不能同时通过 slot 访问同一数据;最后一次使用结束后,这里可以恢复通过 slot 修改。数值先从 10 加到 11,再通过较短的借用加到 13,最后回到原引用加到 16,始终修改同一个整数。这个过程称为再借用(reborrow),没有复制整数,也没有复制独占权限。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax04-reference-move.rs:
fn main() {
let mut number = 10;
let slot = &mut number;
let later = slot;
println!("{} / {}", *slot, *later);
}预期诊断包含 E0382。
let later = slot 在这里移动了 &mut i32。可变引用没有 Copy,不能把它当作两份可同时使用的独占权限。移动的是引用值,整数仍留在原变量中;旧引用绑定却不能再使用,所以打印时同时使用新旧引用会失败。若需要临时把权限交给下一步,再借用和按值移动要分别判断;函数调用在期望引用类型的位置也可能由编译器进行再借用。
完整程序,下载包中为 syntax/syntax04-shared-copy.rs:
fn main() {
let text = String::from("Rust");
let first = &text;
let second = first;
println!("{first} / {second} / {text}");
}预期输出:
Rust / Rust / Rust普通共享引用 &T 可以复制,复制的是引用本身,所指的 String 没有获得独立副本,也没有移出 text。读 *、&、&mut 时,先确定正在谈的是变量绑定、引用值,还是被引用的数据。
把打印、修改和备份组合起来 #
下面的完整程序对应下载包的 basics/examples/ch04_ownership.rs:
fn show_title(title: &str) {
println!("标题:{title}");
}
fn add_suffix(title: &mut String) {
title.push_str("(已复习)");
}
fn main() {
let original = String::from("学习 Rust");
let mut title = original;
show_title(&title);
add_suffix(&mut title);
show_title(&title);
let backup = title.clone();
title.push('!');
println!("当前:{title}");
println!("备份:{backup}");
let count = 3;
let copied = count;
println!("两个整数:{count}、{copied}");
}预期输出:
标题:学习 Rust
标题:学习 Rust(已复习)
当前:学习 Rust(已复习)!
备份:学习 Rust(已复习)
两个整数:3、3在解压后的 rust-course 目录运行:
cargo run -p rust-basics --example ch04_ownership按执行顺序检查所有权:original 移给 title;打印函数共享借用;修改函数可变借用;backup 获得独立副本;最后复制整数。哪一步之后原绑定不能再用,哪一步只暂时限制访问,应该能从代码中指出来。
练习:追加一个感叹号 #
写 append_mark,参数是 &mut String,给文本追加 !。调用后在 main 中打印原变量,确保它仍能使用。再想一想:如果连续调用两次,输出应该是什么?
参考答案:先自己完成再展开
fn append_mark(text: &mut String) {
text.push('!');
}
fn main() {
let mut message = String::from("继续练习");
append_mark(&mut message);
println!("{message}");
assert_eq!(message, "继续练习!");
}预期输出:
继续练习!运行 cargo run -p rust-basics --example answer04。连续调用两次时,应得到 继续练习!!,检查结果的断言也要相应修改。再把初始字符串改成空字符串,一次调用后应得到 !。
如果把参数改成 String 并按值调用,函数就取得了所有权。调用方再继续使用原绑定,会遇到移动后的访问错误。观察后改回可变借用,解释两种签名表达的不同意图。
练习:接收所有权,再把字符串返回 #
写 reviewed_title,接收一个 String,追加 (已复习),再返回这个 String。在 main 中用返回值建立新绑定并打印。此次练习不使用 clone()。
参考答案:先自己完成再展开
fn reviewed_title(mut title: String) -> String {
title.push_str("(已复习)");
title
}
fn main() {
let title = String::from("再写一遍");
let title = reviewed_title(title);
println!("{title}");
assert_eq!(title, "再写一遍(已复习)");
}预期输出:
再写一遍(已复习)这份答案在正文中给出,可以替换自己项目的 src/main.rs 运行。旧绑定在调用时被移动,新绑定接收返回的所有权;两个绑定同名,不表示移动没有发生。
进入下一章前检查 #
不看参考答案,写两个函数:一个接收 &str 打印标题,一个接收 &mut String 追加文字。在 main 中创建标题,依次调用两个函数,再打印修改后的结果。最后创建备份,继续修改原字符串,确认备份不变。
你还应能解释:为什么整数赋值后两个变量都能使用?为什么 String 默认不会自动复制?共享借用最后一次使用的位置,怎样影响后面的修改?函数创建的新字符串为什么应返回 String?
下一章会把这些规则用到中文文本和多条记录上,学习字符串、可增长的列表,以及按名称保存数值的字典。
官方延伸阅读 #
外部文档查阅日期:2026-10-13。代码验证使用 Rust 2024 edition;工具链版本和实际验证范围见学习指南。
一起讨论
选中正文中的文字,可以针对这段内容发起讨论。
正在读取讨论…
在 GitHub 查看与管理讨论 ↗