rust所有权机制
本节内容主要是讲所有权机制,理论为主,文末给了实验代码,理解所有权机制还需配合所有的实验代码来理解
1. 为什么需要所有权?—— 从 C 语言的”内存困境”说起
要理解 Rust 的所有权机制,最好的方式不是直接看 Rust 的规则,而是先回到一个更原始的问题:在没有 GC 的语言里,堆上申请的内存到底谁来管?
1.1 C 语言的内存管理:自由背后的代价
在 C 语言中,堆内存的管理完全由程序员手动负责。来看一段最基础的代码:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int*)malloc(sizeof(int)); // 向操作系统申请一块堆内存
*p = 42;
printf("%d\n", *p);
free(p); // 用完了,手动归还
return 0;
}
这段代码看起来很正常——申请、使用、释放,三步走。但问题在于,编译器不会帮你检查这三步是否都做对了。现实中,大型 C 项目里充斥着这样的错误:
- 忘记
free→ 内存泄漏,程序跑久了内存越占越多 free了两次 → 双重释放,破坏内存分配器的内部结构,程序崩溃free之后继续用p→ 悬垂指针(use-after-free),读到垃圾数据或者直接段错误- 多线程同时读写同一块内存 → 数据竞争,结果不可预测
这些 bug 的共同特点是:编译能通过,运行时才爆炸,而且很难复现和定位。
为什么 C 语言管不好内存?不是程序员不够细心,而是有一个根本性的缺失——
1.2 问题的根源:C 语言没有”所有权”这个概念
我们用悬垂指针来具体感受一下这个”缺失”到底是什么。
假设你在写一个函数,需要返回一个字符串:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* create_greeting(const char* name) {
char buffer[100]; // 在栈上分配
snprintf(buffer, 100, "Hello, %s!", name);
return buffer; // 返回栈变量的指针!
} // buffer 在这里随着函数返回被销毁
int main() {
char *msg = create_greeting("Rust");
printf("%s\n", msg); // 悬垂指针!msg 指向已经销毁的栈内存
return 0;
}
现在停下来想一想,这段代码里,有几个关于”所有权”的问题 C 语言完全没有回答:
buffer这块内存是谁的?—— 是create_greeting这个函数的?还是main的?语言层面没有定义。- 谁负责释放
buffer?—— 编译器会自动回收栈内存,但main里的msg并不知道这件事。它拿到了一个地址,然后理所当然地用了。 - 什么时候可以安全地使用
msg?—— 没有规则告诉你这个指针还有没有效。你只能靠”脑子记”。
本质问题:C 语言只提供了
malloc和free这两个操作,但没有建立一套 ”这块内存属于谁、由谁负责释放、什么时候释放”的清晰规则。程序员在脑子里各自维护一套隐式的、不统一的约定——这就是所有内存 bug 的温床。
1.3 什么是”所有权”?
从上面的分析中,我们可以自然地推导出”所有权”应该回答的三个问题:
- 谁创建了这块资源?(资源从哪来)
- 谁持有这块资源?(在使用期间,谁是它的主人)
- 谁负责销毁这块资源?(用完了,谁来收尾)
C 语言的困境告诉我们:这三个问题如果没有明确的答案,内存管理就全靠程序员的”自觉”,bug 不可避免。
那么,怎么回答这三个问题?实际上有两种基本的解决思路:
思路一:独占所有权(一个所有者)
一块资源在任何时刻有且仅有一个所有者。 所有者独占使用权,并在自己离开作用域时负责销毁资源。如果别人需要用到这块资源,所有者可以把所有权转移出去,但转移之后自己就不再持有。
这正是 C++ 的 std::unique_ptr 和 Rust 默认变量语义采用的模型。它的好处是逻辑简单:一个人说了算,责任明确,不会出现”你以为我释放了、我以为你还拿着”的混乱。
思路二:共享所有权(多个所有者,但有明确的释放规则)
一块资源可以同时有多个所有者,但必须建立一套明确且固定的规则来决定”什么时候释放”。 最常见的规则是引用计数:每多一个人持有,计数器 +1;每有一个人退出,计数器 -1;当计数归零时,自动释放资源。
C++ 的 std::shared_ptr 和 Rust 的 Rc<T>/Arc<T> 采用的就是这个模型。它的好处是更灵活——适合那些确实需要多处共享、无法提前确定谁最后用完了的场景。
关键认知:共享所有权不是没有所有权,而是把释放规则从”唯一的那个所有者离开时释放”变成了”最后一个所有者离开时释放”。规则变了,但依然有规则——这个规则在编译期或运行期被严格执行,不允许模棱两可。
所以,所有权到底是什么?
所有权 = 对一块资源生命周期(创建→使用→销毁)的管理权责,以及一套明确的、”谁在什么时候负责释放”的规则。
不管是独占还是共享,核心都是同一件事:消灭”这块内存到底谁来管”的模糊地带。C 语言的悲剧在于,它既没有独占的约定,也没有共享的规则——每个人都在裸用指针,每个人都不知道别人会不会 free、自己该不该 free。
回到上面悬垂指针的例子:buffer 的所有者应该是 create_greeting 函数,它在函数返回时销毁了 buffer。但 main 里的 msg 拿到了一个指向已销毁内存的指针——它以为自己能用这块内存,但实际上它既不是所有者、也不知道内存已经没了。所有权不清晰,bug 就趁虚而入。
1.4 为什么必须有所有权?
因为计算机的内存资源是有限的、需要被回收的。而回收的前提是:你必须明确知道”什么时候没人用了”。
如果一块堆内存没有所有者:
- 没人知道该不该释放 → 内存泄漏
- 或者多个人都以为自己是所有者,各自释放一次 → 双重释放
- 或者一个人释放了,另一个人还在用 → 悬垂指针
如果一块内存有所有者,但规则不被强制:
- 程序员可以绕过规则 → C++ 的困境(下面会讲)
所以,GC 语言(Java、Go、Python 等)用一种方式解决了这个问题:让运行时追踪所有引用,自动判断”没人用了”。代价是运行时的 GC 开销和不可预测的停顿。
而 C 语言选择了另一个极端:完全交给程序员。代价是上面那些难以排查的 bug。
有没有第三条路?—— 在编译期就确定每一块内存的所有者,不依赖运行时 GC,也不依赖程序员的自觉。这就是 Rust 的做法。
1.5 C++ 的尝试:引入所有权思想,但不强制
在讨论 Rust 之前,有必要提一下 C++,因为它是最早在语言层面引入”所有权”概念的先行者。
C++ 在 C 的基础上做了三件重要的事:
- RAII(资源获取即初始化):利用构造函数获取资源、析构函数释放资源。对象离开作用域时,析构函数自动被调用——这是”自动回收”思想的雏形。
- 智能指针:
std::unique_ptr(独占所有权,不可拷贝只可移动)、std::shared_ptr(共享所有权,引用计数)、std::weak_ptr(弱引用,打破循环)。 - 移动语义(C++11):通过
std::move显式转移所有权,避免深拷贝。
来看一个 C++ 的例子:
#include <memory>
void demo() {
std::unique_ptr<int> p1 = std::make_unique<int>(42); // p1 独占所有权
std::unique_ptr<int> p2 = std::move(p1); // 所有权转移给 p2,p1 变为空
// p2 离开作用域时,自动释放内存——不需要手动 delete
}
这已经很接近”所有权系统”了。但 C++ 的关键问题是:这些规则只是”建议”,编译器不强制执行。你仍然可以:
- 绕过智能指针,直接用
new/delete,让 RAII 形同虚设 - 裸指针和智能指针混用,一片混乱
std::move之后继续访问原对象——编译器不会报错;对象通常仍然有效,但处于“已移动后”的状态,具体内容可能为空或未指定,很容易被误用shared_ptr循环引用导致内存泄漏——编译器也帮不了你
C++ 的所有权更像一本推荐规范——你可以遵守,也可以不遵守。而历史遗留的庞大代码库中,”不遵守”的代码比比皆是。
1.6 Rust 的答案:所有权不是建议,是法律
Rust 从 C++ 的经验中吸取了教训:所有权规则必须由编译器强制执行,否则就毫无意义。
Rust 的做法是:
- 在语言层面定义了三条硬性的所有权规则(见第 3 节)
- 通过**借用检查器(Borrow Checker)**在编译期验证所有代码是否遵守这些规则
- 违反规则的代码直接编译不通过——没有”不小心”,没有”下不为例”
从 C 的手动管理 → C++ 的建议式所有权 → Rust 的强制所有权,演进的脉络很清晰:把内存管理的正确做法从”程序员脑子里的约定”变成”编译器能检查的规则”。
2. Rust 所有权核心三原则
Rust 的所有权系统建立在三条核心规则之上。每一条规则都不是随意设计的——它们各自解决了 C 语言内存管理中的一类具体问题。
原则一:每个值都有一个所有者
每个值在 Rust 中都有一个变量,称为其所有者。(一个值对应一个 owner)
为什么这么设计?——解决“谁负责释放“的问题。
回顾 C 语言的困境:一块 malloc 出来的内存,到底谁应该 free?没有规则,全靠约定。Rust 用这条规则给出了明确的答案:每个值都绑定到一个所有者变量上,这个变量负责该值的最终释放。没有人需要猜测“这块内存归谁管“——编译器帮你追踪,所有者一目了然。
原则二:同一时刻只能有一个所有者
同一时刻只能有一个所有者。(所有权可以转移,但无法共享——此处指默认的独占所有权;共享所有权通过
Rc/Arc另行提供)
为什么这么设计?——解决“双重释放“和“数据竞争“的问题。
如果多个变量同时拥有同一块内存,每个变量离开作用域时都会尝试释放它——这就是双重释放。更隐蔽的是,如果两个所有者分别在不同的线程中修改这块内存,就产生了数据竞争。
Rust 的策略是:默认独占。一个值只有一个所有者,转移所有权后原变量立刻失效(编译器禁止你再使用它)。这从根本上杜绝了“两个人同时管一块内存“的混乱。对于确实需要共享的场景,Rust 提供了 Rc/Arc(引用计数)——这仍然是有规则的共享,而不是 C 语言那种“谁都能拿指针、谁都搞不清谁在管“的无序状态。
原则三:所有者离开作用域时自动释放
当所有者离开作用域,该值被自动丢弃。(自动调用
drop,释放资源)
为什么这么设计?——解决“忘记释放(内存泄漏)“的问题。
C 语言中,你必须在合适的时机手动调用 free。忘了?内存泄漏。在复杂的控制流(提前 return、break、异常路径)中,“合适的时机“往往很难找准。
Rust 的答案是:把释放和所有者的作用域绑定。所有者离开作用域的那一刻,编译器自动插入释放代码——不依赖程序员记得、不依赖运行时 GC、不会遗漏任何一条退出路径。这正是 C++ RAII 思想的彻底贯彻,区别在于 Rust 的编译器会验证所有路径上所有权的正确性。
三原则配合的效果
三条规则单独看都很简单,但组合起来威力巨大:
#![allow(unused)]
fn main() {
{
let s = String::from("hello"); // s 是所有者(原则一:值有了明确的主人)
// 使用 s ...
} // 作用域结束,s 被销毁,String 占用的内存自动释放(原则三:自动回收)
// 在此期间没有其他变量能同时拥有这块内存(原则二:独占,避免冲突)
}
3. Rust 的变量语义:默认移动,而非拷贝
许多主流语言(Python、Java、Go 等)在赋值或传参后,原变量通常仍然可以继续使用:有的语言复制对象引用,有的语言复制值本身,所有权关系大多被运行时或语言规则隐藏起来。Rust 走了另一条路:默认移动语义,把“这个值现在归谁负责”显式体现在类型检查里。
3.1 移动是默认,拷贝需显式声明
赋值或传参时,Rust 的行为取决于类型是否实现了 Copy trait:
- 实现了
Copy的类型(整数、布尔、字符、不可变引用等简单类型):赋值时自动拷贝,开销极小,原变量仍可用。 - 没有实现
Copy的类型(String、Vec、大多数自定义类型):赋值时发生移动——所有权转移,原变量失效。
#![allow(unused)]
fn main() {
// Copy 类型:自动拷贝
let x = 5;
let y = x; // y 是 x 的拷贝,两者独立
println!("x = {}, y = {}", x, y); // x 仍然可用
// 非 Copy 类型:移动
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
// println!("{}", s1); // 编译错误:s1 已经失效
println!("s2 = {}", s2);
}
3.2 为什么 Rust 默认移动?
回到所有权三原则的第二条:同一时刻只能有一个所有者。如果赋值默认拷贝,对于堆上数据(如 String 底层的字节数组)会发生什么?
#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 如果默认拷贝,s1 和 s2 各有一份堆数据的副本
}
这会带来两个问题:
- 性能陷阱:每次赋值都深拷贝堆数据,代价高昂。GC 语言靠运行时优化(copy-on-write 等)部分缓解,但 Rust 没有 GC。
- 语义混乱:两个变量持有两份”独立”的数据,修改一个不影响另一个——这真的是你想要的行为吗?在很多场景下,你只是想把数据传到另一个地方处理。
以 String / Vec 这类句柄型类型为例,移动时通常只是移动栈上的表示(如指针、长度、容量),不会深拷贝堆上的数据。移动之后,原变量失效,只有新变量拥有这块堆内存。对大多数拥有堆资源的类型来说,这比深拷贝便宜得多;但一般地说,move 仍可能搬动类型的栈上表示,具体成本取决于类型大小。
3.3 如果确实需要深拷贝:显式调用 .clone()
移动是默认、廉价的操作。但如果你确实需要一份独立的、完整的数据副本,就显式调用 .clone():
#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 显式深拷贝:在堆上新分配一块内存,复制数据
// s1 和 s2 现在是两个独立的 String,各自拥有一份 "hello"
println!("s1 = {}, s2 = {}", s1, s2); // 两个变量都能用
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // Vec 也一样,显式 clone 才做深拷贝
println!("v1 = {:?}, v2 = {:?}", v1, v2);
}
关键认知:Rust 并没有禁止拷贝——它只是把选择权交给了你。默认移动(零成本),需要拷贝时显式写
.clone()(有成本但语义清晰)。如果你发现代码里到处在写.clone(),那通常是一个信号:你的所有权设计可能需要重新考虑——也许你应该用借用(引用)而不是拷贝数据。
4. 借用(Borrowing)—— 暂时看一眼,不抱走
所有权转移(移动)会让原变量失去值,这太严格了。很多时候我们只需要 临时访问 一个值,而不想获取所有权。这就是 借用。
借用:通过引用(
&T或&mut T)访问一个值,而不取得其所有权。
4.1 借用的核心规则
借用有三条核心规则,每一条都是为了阻止一类特定的内存 bug:
规则一:引用的生命周期不能超过被引用值的生命周期
编译器自动检查:引用(
&T/&mut T)的有效范围必须短于被引用值的有效范围。
为什么这么设计?——阻止悬垂指针。 这是最直接的安全保证。如果引用可以比数据活得更久,那引用就变成了悬垂指针(指向已销毁的内存)。Rust 的借用检查器在编译期追踪每个引用和每个值的生命周期关系,一旦发现引用可能“活过“数据本身,直接报错。C 语言中最常见的 use-after-free 在 Rust 中连编译都过不了。
规则二:同一时刻,要么只能有一个可变引用,要么可以有任意多个不可变引用
读(
&T)可以多人同时读;写(&mut T)必须独占,写的时候别人不能读也不能写。
为什么这么设计?——同时阻止两类问题。
第一,数据竞争。如果两个线程(或同一线程的两段代码)同时写同一块内存,或者一个写一个读,结果不可预测。规则二确保了“写是独占的“,从根源消灭了数据竞争的可能。
第二,迭代器失效。在 C++ 中,如果你在遍历一个 vector 的同时修改它(比如 push_back 触发 reallocation),迭代器就会指向被释放的旧内存——这是一个经典的运行时 bug。Rust 通过规则二可以在编译期阻止这种情况:遍历时持有的是不可变引用(&T),在此期间任何试图获取可变引用(&mut T)来修改数据的操作都会被编译器拒绝。
这条规则和数据库的“读写锁“思路一致:多个读锁可以共存,写锁是排他的。区别在于 Rust 把它做在了编译期,零运行时开销。
规则三:引用必须始终有效
不能存在指向已销毁数据的引用。
为什么这么设计?——这是规则一的自然推论。 规则一保证了引用的生命周期更短,规则三从“有效性“的角度补了一刀:不仅生命周期要对,引用指向的数据本身也必须是活着的。这意味着你不能返回局部变量的引用,也不能在数据被移动/释放后还保留它的引用。编译器在每一条代码路径上验证这一点。
4.2 借用的分类
| 类型 | 语法 | 允许的操作 | 是否允许其他引用同时存在 |
|---|---|---|---|
| 不可变引用 | &T | 读(只读) | 可以有多个 |
| 可变引用 | &mut T | 读写 | 只能有一个 |
4.3 基本语法和示例
4.3.1 不可变引用
fn main() {
let s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 可以创建多个
println!("{}, {}", r1, r2);
// s 的所有权没有转移,仍然可以使用 s
} // r1, r2 超出作用域,但不会释放 s 的内存(因为只是借用)
4.3.2 可变引用
fn main() {
let mut s = String::from("hello");
let r = &mut s; // 可变引用
r.push_str(", world");
println!("{}", r);
// 在 r 存在期间,不能有其他任何引用(包括不可变引用)
}
4.3.3 错误示例:可变引用与不可变引用混用
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &mut s; // 错误:不能同时存在可变引用和不可变引用
println!("{}, {}", r1, r2);
}
4.4 深入分析所有权机制(R/W/O 模型)
我们可以用一个通俗的模型来理解所有权和借用的权限:
假设一个值有三类权限:
- R(Read):读取值的内容
- W(Write):修改值的内容
- O(Own):决定值的生命周期(释放它)
- 完全所有权:拥有 R + W + O 全部权限。
- 不可变借用:只借出 R 权限(读),不能写,不能释放。
- 可变借用:借出 R + W 权限(读写),但不能释放(O 依然属于原所有者)。
4.4.1 分析示例一:多个不可变引用同时存在
fn main() {
let mut data = vec![1, 2, 3]; // data: R + W + O
let r1 = &data; // r1: R
let r2 = &data; // r2: R (第二个不可变引用)
// 此时有两个不可变引用,data 自身的 W 被冻结
// data.push(4); // 错误:不能写,因为有不可变借用
println!("r1: {:?}, r2: {:?}", r1, r2); // 读操作 OK
// r1, r2 离开作用域后,data 的 W 恢复
}
权限分析:
data原本拥有 R+W+O。r1和r2各自借出 R(只读)。- 由于存在不可变借用,
data的 W 被冻结,直到所有不可变引用结束。 - O 始终留在
data手中,值的生命周期由其决定。
结论:多个不可变引用可以共存,每个都只读;原所有者的写权限在此期间被剥夺。
4.4.2 分析示例二:可变引用期间通过原所有者访问(被阻止)
fn main() {
let mut data = vec![1, 2, 3]; // data: R + W + O
let r = &mut data; // r: R + W (没有 O)
// 尝试通过原所有者读取或写入 —— 全部被禁止
// println!("{:?}", data); // 错误:不能读,因为存在可变借用
// data.push(4); // 错误:不能写,因为存在可变借用
r.push(4); // 只能通过可变引用修改
println!("{:?}", r); // 通过可变引用读取
// r 结束后,data 的 R+W 恢复
}
权限分析:
- 可变引用
r拿走了 R+W,但没有拿走 O。 - 在此期间,原所有者
data的 R 和 W 完全被剥夺(即使它名义上还有 O,但不可访问),这是为了独占写的安全保证。 - 只有
r能读写数据。 结论:可变引用存在时,原所有者完全冻结(不可读不可写);所有访问必须通过可变引用。
4.4.3 分析示例三:借用结束后权限自动恢复
fn main() {
let mut data = vec![1, 2, 3]; // data: R + W + O
// 第一阶段:不可变借用
{
let r = &data; // r: R
println!("{:?}", r); // 只读,OK
} // r 离开作用域,借用结束
// 第二阶段:可变借用
{
let r = &mut data; // r: R + W
r.push(4);
} // r 离开作用域,借用结束
// 第三阶段:完全所有权恢复
data.push(5); // 可以直接修改
println!("{:?}", data); // 可以直接读取
}
权限分析:
- 每个借用只在其作用域内生效。
- 借用结束后,原所有者自动恢复全部 R+W+O 权限。
- 不同借用阶段可以交替使用,只要不重叠。 结论:借用是临时的,作用域结束即归还权限;所有者始终保留 O,并在无借用时恢复全部操作能力。
4.4.4 不同借用类型对应的规则表
| 场景 | 是否存在不可变引用 | 是否存在可变引用 | 是否允许通过所有者修改 | 是否允许通过所有者读取 |
|---|---|---|---|---|
| 无借用 | 否 | 否 | ✅ 允许 | ✅ 允许 |
| 有不可变引用 | 是(≥1个) | 否 | ❌ 禁止(冻结) | ✅ 允许 |
| 有可变引用 | 否 | 是(1个) | ❌ 禁止(所有权暂时“托管”) | ❌ 禁止(除非通过可变引用) |
这个模型清晰地解释了为什么 Rust 能避免数据竞争:读和写不能同时存在。
5. 实验代码示例
//! # Rust 所有权与借用完整实验示例
//!
//! 本程序演示 Rust 所有权系统的各种核心机制。
//! 每个测试函数都独立演示一个特定场景。
//!
//! 运行方式:
//! 1. 复制整个代码到 `main.rs` 或 `lib.rs`
//! 2. 执行 `cargo run` 或 `rustc main.rs && ./main`
//!
//! 注意:部分函数(如违反借用规则的示例)会编译失败,
//! 它们已被注释掉或在 `main` 中没有调用。你可以手动取消注释来观察编译器错误。
// ========================= 1. 所有权移动 =========================
/// 测试1:基本类型与堆类型的移动差异
///
/// 行为:
/// - `i32` 实现了 `Copy` trait,赋值时会**拷贝**,原变量仍可用。
/// - `String` 未实现 `Copy`,赋值时发生**移动**,原变量失效。
///
/// 运行结果:编译通过,输出正常。
fn test_move_and_copy() {
println!("\n=== 1. 移动 vs 拷贝 ===");
// i32 是 Copy 类型
let x = 42;
let y = x; // 拷贝,x 仍然有效
println!("x = {}, y = {}", x, y);
// String 是非 Copy 类型
let s1 = String::from("hello");
let s2 = s1; // 移动所有权,s1 不再有效
// println!("{}", s1); // 取消注释会编译错误:value used here after move
println!("s2 = {}", s2);
}
/// 测试2:函数传参发生所有权移动
///
/// 行为:将变量传入函数时,所有权会移动到函数参数中。
/// 一旦移动,原变量无法再使用(除非函数将所有权返回)。
///
/// 运行结果:编译通过,展示移动后无法使用原变量。
fn take_ownership(s: String) {
println!("函数获得所有权: {}", s);
} // s 在这里被自动 drop
fn test_function_move() {
println!("\n=== 2. 函数传参移动所有权 ===");
let s = String::from("Rust");
take_ownership(s);
// println!("{}", s); // 编译错误:s 的所有权已转移
}
/// 测试3:函数返回所有权
///
/// 行为:函数返回 String 时,所有权从函数内部移出到调用者。
/// 这允许动态创建数据而不丢失所有权。
fn give_ownership() -> String {
let inner = String::from("Created inside");
inner // 所有权移出
}
fn test_return_ownership() {
println!("\n=== 3. 函数返回所有权 ===");
let s = give_ownership();
println!("得到所有权: {}", s);
// 现在 s 持有所有权,离开作用域时释放
}
// ========================= 2. 借用(引用) =========================
/// 测试4:不可变借用(只读访问)
///
/// 行为:使用 `&T` 创建不可变引用,可以同时存在多个。
/// 在不可变引用存在期间,不能修改原变量。
///
/// 运行结果:编译通过,输出正确。
fn test_immutable_borrow() {
println!("\n=== 4. 不可变借用 ===");
let s = String::from("readonly");
let r1 = &s;
let r2 = &s;
println!("r1 = {}, r2 = {}", r1, r2);
// s.push_str("!"); // 编译错误:不能修改,因为有不可变借用
// r1 和 r2 离开作用域后,s 才可修改
}
/// 测试5:可变借用(读写访问)
///
/// 行为:使用 `&mut T` 创建可变引用,同一时刻只能有一个。
/// 可变引用允许修改数据,但在其存活期间不能有其他任何引用(包括不可变引用)。
///
/// 运行结果:编译通过,数据被修改。
fn test_mutable_borrow() {
println!("\n=== 5. 可变借用 ===");
let mut s = String::from("hello");
{
let r = &mut s;
r.push_str(", world"); // 通过可变引用修改
println!("内部修改后: {}", r);
} // r 离开作用域,可变借用结束
println!("外部原变量: {}", s); // 现在可以访问原变量
}
/// 测试6:违反借用规则(演示编译错误)
///
/// 下面的代码会编译失败,演示不能同时拥有可变引用和不可变引用。
/// 取消注释来观察错误。
#[allow(dead_code)]
fn test_illegal_borrow_combining() {
let mut s = String::from("conflict");
let r1 = &s; // 不可变借用
let r2 = &mut s; // 尝试创建可变借用 → 编译错误
println!("{}, {}", r1, r2);
}
// 更安全的写法:分开作用域
fn test_legal_borrow_scope() {
println!("\n=== 6. 借用作用域分离 ===");
let mut s = String::from("scope");
{
let r1 = &s;
println!("不可变借用: {}", r1);
} // r1 结束
let r2 = &mut s;
r2.push_str(" modified");
println!("可变借用后: {}", r2);
}
// ========================= 3. 生命周期与悬垂引用 =========================
/// 测试7:悬垂引用(编译错误)
///
/// 尝试返回局部变量的引用,这会导致悬垂指针。
/// Rust 编译期会拒绝。
///
/// 以下函数无法编译,因为 `s` 在函数返回时被销毁。
#[allow(dead_code)]
fn dangling_reference() -> &String {
let s = String::from("I will die");
&s // 错误:返回局部变量的引用
}
/// 测试8:正确的返回方式 — 返回所有权而非引用
///
/// 通过返回 `String`(转移所有权),而不是返回引用,避免悬垂。
fn no_dangle() -> String {
let s = String::from("I live on");
s // 所有权移出
}
fn test_no_dangle() {
println!("\n=== 7. 避免悬垂引用:返回所有权 ===");
let s = no_dangle();
println!("{}", s);
}
// ========================= 4. 借用作为函数参数 =========================
/// 测试9:函数接受不可变引用
///
/// 这样可以借用而不获取所有权,调用后原变量仍可用。
fn print_length(s: &String) {
println!("字符串长度: {}", s.len());
} // s 是引用,不会 drop 原数据
fn test_borrow_as_param() {
println!("\n=== 8. 函数参数借用 ===");
let s = String::from("Hello, world!");
print_length(&s); // 传递不可变引用
println!("原变量仍可用: {}", s);
}
/// 测试10:函数接受可变引用
///
/// 函数可以直接修改传入的数据。
fn add_suffix(s: &mut String, suffix: &str) {
s.push_str(suffix);
}
fn test_mutable_borrow_param() {
println!("\n=== 9. 可变引用作为参数 ===");
let mut s = String::from("Hello");
add_suffix(&mut s, "!!!");
println!("修改后: {}", s);
}
// ========================= 5. 切片引用 &str 的特殊性 =========================
/// 测试11:String 与 &str 的关系
///
/// `&str` 是对字符串字面量或 String 某一部分的不可变引用(字符串切片)。
/// 它本身是一个引用,所以也遵循借用规则。
fn test_string_slice() {
println!("\n=== 10. 字符串切片引用 &str ===");
let s = String::from("Rust programming");
let hello = &s[0..4]; // &str 切片
println!("切片: {}", hello);
// 切片借用期间,原 String 不能修改
// s.clear(); // 编译错误:不能修改,因为还有切片引用存在
// 但切片结束后可以
drop(hello); // 显式结束借用(通常不需要,作用域结束自动结束)
s.clear(); // 现在可以了
println!("清空后: '{}'", s);
}
// ========================= 6. 自定义结构体所有权 =========================
struct Person {
name: String,
age: u32,
}
impl Person {
/// 方法:不可变借用 self
fn introduce(&self) {
println!("我叫 {},今年 {} 岁。", self.name, self.age);
}
/// 方法:可变借用 self,可以修改字段
fn have_birthday(&mut self) {
self.age += 1;
println!("生日快乐!现在 {} 岁了。", self.age);
}
/// 方法:取得所有权(很少用,演示用)
fn destroy(self) {
println!("{} 被销毁了", self.name);
// self 在此函数结束时被 drop
}
}
fn test_struct_ownership() {
println!("\n=== 11. 结构体与所有权 ===");
let mut p = Person {
name: String::from("Alice"),
age: 30,
};
p.introduce(); // 不可变借用
p.have_birthday(); // 可变借用
// 尝试使用 p 的字段
println!("再次访问: {} 现在 {} 岁", p.name, p.age);
// 如果调用 p.destroy(),p 的所有权会移入函数,之后 p 失效
// 这里演示但不调用,以免破坏后续示例。
// p.destroy();
// println!("{}", p.name); // 编译错误
}
// ========================= 7. 常见陷阱:可变引用独占性 =========================
/// 测试12:试图在可变引用存续期间访问原变量
///
/// 尽管原变量仍然存在,但可变借用期间不允许通过原变量读写。
fn test_mut_borrow_blocks_owner() {
println!("\n=== 12. 可变借用期间原变量被冻结 ===");
let mut data = 100;
{
let r = &mut data;
*r += 50;
// println!("原变量 = {}", data); // 编译错误:不能读取 data
println!("通过可变引用读取 = {}", r);
} // r 结束
println!("可变借用结束后,原变量 = {}", data);
}
// ========================= 8. 解引用与内部可变性(Cell/RefCell 简介) =========================
/// 测试13:不可变变量也可以借用可变引用吗?—— 不能,必须声明 mut
///
/// 以下代码编译错误。
#[allow(dead_code)]
fn test_cannot_borrow_mut_from_immut() {
let x = 5;
let y = &mut x; // 错误:不能对不可变变量创建可变引用
}
/// 测试14:对引用解引用修改值
///
/// 使用 `*` 解引用操作符来修改可变引用指向的值。
fn test_deref_mut() {
println!("\n=== 13. 解引用修改 ===");
let mut x = 10;
let r = &mut x;
*r = 20; // 解引用并赋值
println!("修改后 x = {}", x);
}
// ========================= 主函数:选择性执行 =========================
fn main() {
println!("=============================================");
println!("Rust 所有权与借用完整实验套件");
println!("=============================================");
// 所有可以正常编译运行的测试函数
test_move_and_copy();
test_function_move();
test_return_ownership();
test_immutable_borrow();
test_mutable_borrow();
test_legal_borrow_scope();
test_no_dangle();
test_borrow_as_param();
test_mutable_borrow_param();
test_string_slice();
test_struct_ownership();
test_mut_borrow_blocks_owner();
test_deref_mut();
// 以下函数会编译失败,已注释。取消注释可查看编译器错误。
// test_illegal_borrow_combining();
// dangling_reference();
// test_cannot_borrow_mut_from_immut();
println!("\n=============================================");
println!("所有可运行的测试完成!");
println!("尝试修改部分代码,观察编译器的借用检查。");
println!("=============================================");
}
5.1 实验:test_move_and_copy
#![allow(unused)]
fn main() {
fn test_move_and_copy() {
println!("\n=== 1. 移动 vs 拷贝 ===");
// i32 是 Copy 类型
let x = 42;
let y = x; // 拷贝,x 仍然有效
println!("x = {}, y = {}", x, y);
// String 是非 Copy 类型
let s1 = String::from("hello");
let s2 = s1; // 移动所有权,s1 不再有效
// println!("{}", s1); // 取消注释会编译错误:value used here after move
println!("s2 = {}", s2);
}
}
- 实验:分别对
i32和String类型变量赋值给新变量,然后尝试使用原变量。 - 现象:
i32赋值后原变量x仍可正常打印;String赋值后,若取消注释println!("{}", s1)则编译报错value used here after move。 - 分析:
i32实现了Copytrait——它只占 4 字节栈空间,没有堆资源,编译器直接按位拷贝,两个变量完全独立。String未实现Copy,因为它内部持有指向堆内存的指针。赋值时,栈上的“指针+长度+容量“三元组被拷贝到s2,同时编译器将s1标记为“已移动“(moved),后续任何对s1的访问都被静态禁止。这直接体现了所有权原则二:同一时刻只有一个所有者。 - 结论:实现了
Copy的类型赋值时自动拷贝,原变量继续有效;非Copy类型赋值时发生所有权移动,移动后原变量失效,由编译器静态保证不会出现“使用已移动值“。
5.2 实验:test_function_move
#![allow(unused)]
fn main() {
fn take_ownership(s: String) {
println!("函数获得所有权: {}", s);
} // s 在这里被自动 drop
fn test_function_move() {
println!("\n=== 2. 函数传参移动所有权 ===");
let s = String::from("Rust");
take_ownership(s);
// println!("{}", s); // 编译错误:s 的所有权已转移
}
}
- 实验:将
String变量s作为参数传入take_ownership函数,函数返回后尝试使用s。 - 现象:函数内部正常打印;函数外部使用
s的代码若取消注释则编译报错use of moved value。 - 分析:函数参数也是变量,传参等价于赋值——
let s_param = s;的移动语义在此同样生效。s的所有权移入了函数参数,函数结束后参数离开作用域,String被drop。这展示了所有权原则三(所有者离开作用域自动释放)在函数边界的自然延伸:函数获得所有权 → 函数结束时释放,整个过程清晰、无泄漏。 - 结论:函数传参同样遵循移动语义。参数获得了值的所有权,调用者失去了所有权。如果调用者后续还需要使用该值,应改用借用(传引用)。
5.3 实验:test_return_ownership
#![allow(unused)]
fn main() {
fn give_ownership() -> String {
let inner = String::from("Created inside");
inner // 所有权移出(注意没有分号,是表达式返回)
}
fn test_return_ownership() {
println!("\n=== 3. 函数返回所有权 ===");
let s = give_ownership();
println!("得到所有权: {}", s);
// 现在 s 持有所有权,离开作用域时释放
}
}
- 实验:编写函数
give_ownership,内部创建String并返回,调用者接收返回值并继续使用。 - 现象:调用者正常获得并使用返回的
String,编译通过,无泄漏。 - 分析:所有权可以跨越函数边界“向外“流动。
inner在函数内部创建,它的所有权通过返回值转移给了调用者s。整个过程没有拷贝堆数据,移动的只是栈上的指针。这是 Rust 安全传递堆数据的核心机制——函数可以“生产“数据并把所有权交给调用者,调用者继续管理数据的生命周期。 - 结论:所有权可以沿函数返回值向外转移。配合移动语义,这是一种零成本的堆数据传递方式——数据在堆上不动,只有“所有权“(栈上的指针)在动。
5.4 实验:test_immutable_borrow
#![allow(unused)]
fn main() {
fn test_immutable_borrow() {
println!("\n=== 4. 不可变借用 ===");
let s = String::from("readonly");
let r1 = &s;
let r2 = &s;
println!("r1 = {}, r2 = {}", r1, r2);
// s.push_str("!"); // 编译错误:不能修改,因为有不可变借用
// r1 和 r2 离开作用域后,s 才可修改
}
}
- 实验:对
String创建两个不可变引用&s(r1、r2),同时打印它们,然后尝试修改原变量。 - 现象:
r1和r2均可正常打印;若取消注释s.push_str("!")则编译报错cannot borrow s as mutable because it is also borrowed as immutable。 - 分析:
r1和r2各自借出了 R 权限(只读),在此期间data的 W 权限被冻结。Rust 允许多个不可变引用共存,因为多人同时读不会产生数据竞争。但一旦有人持有不可变引用,任何修改操作都被禁止——这防止了“读的时候数据被改了“的问题(即迭代器失效的根源)。这正是借用规则二的前半部分:“可以有任意多个不可变引用。” - 结论:不可变引用允许共享读取,多个可共存;但在不可变引用存在期间,原变量被冻结,不能修改。
5.5 实验:test_mutable_borrow
#![allow(unused)]
fn main() {
fn test_mutable_borrow() {
println!("\n=== 5. 可变借用 ===");
let mut s = String::from("hello");
{
let r = &mut s;
r.push_str(", world"); // 通过可变引用修改
println!("内部修改后: {}", r);
} // r 离开作用域,可变借用结束
println!("外部原变量: {}", s); // 现在可以访问原变量
}
}
- 实验:对可变的
String在内部作用域中创建可变引用&mut s,通过引用修改内容,引用结束后再访问原变量。 - 现象:内部作用域中通过
r修改成功;r离开作用域后,原变量s可正常访问,内容已更新为修改后的值。 - 分析:
r借出了 R+W 权限(读写),O(释放权)始终在s手上。根据借用规则二的后半部分:“同一时刻只能有一个可变引用”,在r存活期间,其他任何对s的访问都被禁止。当r离开作用域,R+W 权限自动归还给s,原变量恢复完整的所有权。这种“作用域隔离“避免了写冲突。 - 结论:可变引用在作用域内独占读写权限;引用结束后权限自动归还,原变量恢复可用。通过作用域控制可变引用的存活范围是 Rust 的常用模式。
5.6 实验:test_illegal_borrow_combining(编译失败)
#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn test_illegal_borrow_combining() {
let mut s = String::from("conflict");
let r1 = &s; // 不可变借用
let r2 = &mut s; // 尝试创建可变借用 → 编译错误
println!("{}, {}", r1, r2);
}
}
- 实验:不取消注释,观察代码结构。尝试在已有不可变引用
r1的情况下创建可变引用r2。 - 现象:此函数无法通过编译,报错
cannot borrow s as mutable because it is also borrowed as immutable。 - 分析:这是借用规则二的最直接演示——不可变引用(读者)和可变引用(写者)不能同时存在。如果允许这种代码,
r1读到的数据可能被r2同时修改,产生数据竞争或逻辑错误。Rust 在编译期直接禁止了这种模式,不需要运行时锁。 - 结论:Rust 的借用规则阻止可变引用与不可变引用共存。这不是语言的“限制“,而是数据安全的“保证“。
5.7 实验:test_legal_borrow_scope
#![allow(unused)]
fn main() {
fn test_legal_borrow_scope() {
println!("\n=== 6. 借用作用域分离 ===");
let mut s = String::from("scope");
{
let r1 = &s;
println!("不可变借用: {}", r1);
} // r1 结束
let r2 = &mut s;
r2.push_str(" modified");
println!("可变借用后: {}", r2);
}
}
- 实验:先用独立作用域创建不可变引用并结束它,再创建可变引用修改数据。
- 现象:编译通过,先正常打印不可变引用,后成功修改字符串。
- 分析:借用检查器检查的是同一时刻存在的引用,而不是整个函数的引用历史。
r1在内部作用域结束时归还了 R 权限,之后s的 W 权限恢复,此时再创建r2不会有任何冲突。这个技巧极其常用:用{}创建一个临时作用域来“提前结束“某个引用的生命周期——不需要等到函数末尾。 - 结论:借用检查器只关注同时存在的引用;通过作用域分离,可以依次使用不可变和可变引用,互不干扰。
5.8 实验:dangling_reference(编译失败)
#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn dangling_reference() -> &String {
let s = String::from("I will die");
&s // 错误:返回局部变量的引用
}
}
- 实验:观察
dangling_reference函数——创建一个局部String,尝试返回它的引用&s。 - 现象:此函数无法通过编译,报错
cannot return reference to local variable s。 - 分析:函数返回时,局部变量
s会被销毁(调用drop),其堆内存被释放。如果允许返回&s,调用者将拿到一个指向已释放内存的悬垂指针。这正是借用规则一(引用的生命周期不能超过被引用值)在函数边界的体现——编译器推断出&s的生命周期长于s本身(因为引用要被返回出去),违反了规则,直接拒绝编译。 - 结论:Rust 编译期阻止悬垂引用的产生。要安全地从函数传出数据,应该返回所有权(下一个实验),而不是返回局部变量的引用。
5.9 实验:test_no_dangle
#![allow(unused)]
fn main() {
fn no_dangle() -> String {
let s = String::from("I live on");
s // 所有权移出
}
fn test_no_dangle() {
println!("\n=== 7. 避免悬垂引用:返回所有权 ===");
let s = no_dangle();
println!("{}", s);
}
}
- 实验:调用
no_dangle(),它内部创建String后直接返回(转移所有权),调用者接收并打印。 - 现象:编译通过,正常打印
"I live on"。 - 分析:与 5.8 形成对比——
no_dangle返回的是String本身(所有权),而非引用。函数内部的s将所有权移出到调用者,调用者的s成为新的所有者。堆上的数据没有移动,移动的只是“所有权令牌“。调用者负责管理接收到的数据的生命周期,一切责任明确。 - 结论:返回所有权是安全传递数据的正确方式。如果函数“生产“了数据,就应该把所有权交给调用者。如果只是想“借阅“数据,使用借用参数(下一个实验)。
5.10 实验:test_borrow_as_param
#![allow(unused)]
fn main() {
fn print_length(s: &String) {
println!("字符串长度: {}", s.len());
} // s 是引用,不会 drop 原数据
fn test_borrow_as_param() {
println!("\n=== 8. 函数参数借用 ===");
let s = String::from("Hello, world!");
print_length(&s); // 传递不可变引用
println!("原变量仍可用: {}", s);
}
}
- 实验:定义
print_length接受&String(不可变引用),调用后原变量继续使用。 - 现象:函数内成功读取字符串长度;函数外
s仍可用,打印正常。 - 分析:传引用(
&s)不转移所有权,print_length只是临时借用了 R 权限(读)。函数结束时引用离开作用域,R 权限归还给s,不会触发drop。这是 Rust 中最常见的参数传递方式——当你只需要“看一眼“数据而不想抢走所有权时,传&T。 - 结论:不可变引用作为函数参数是“借阅“数据的标准方式——调用者保留所有权,被调者临时读取,互不侵犯。
5.11 实验:test_mutable_borrow_param
#![allow(unused)]
fn main() {
fn add_suffix(s: &mut String, suffix: &str) {
s.push_str(suffix);
}
fn test_mutable_borrow_param() {
println!("\n=== 9. 可变引用作为参数 ===");
let mut s = String::from("Hello");
add_suffix(&mut s, "!!!");
println!("修改后: {}", s);
}
}
- 实验:定义
add_suffix接受&mut String,调用后观察原变量的变化。 - 现象:函数内成功追加了
"!!!";调用后原变量s的内容已变为"Hello!!!"。 - 分析:
&mut s将 R+W 权限临时借给函数,函数通过可变引用修改了堆上的字符串内容。函数返回后 R+W 权限归还给s,调用者继续持有完整所有权。整个过程没有拷贝数据,也没有转移所有权。这和 5.10 的不可变借用的区别仅在于权限——&mut允许修改,因此编译器要求s必须声明为mut,且在add_suffix(&mut s, ...)调用期间不能有其他引用存在。 - 结论:可变引用参数允许函数修改外部数据而不获取所有权。这是 Rust 中“让函数帮你改东西“的标准方式。
5.12 实验:test_string_slice
#![allow(unused)]
fn main() {
fn test_string_slice() {
println!("\n=== 10. 字符串切片引用 &str ===");
let s = String::from("Rust programming");
let hello = &s[0..4]; // &str 切片
println!("切片: {}", hello);
// 切片借用期间,原 String 不能修改
// s.clear(); // 编译错误:不能修改,因为还有切片引用存在
drop(hello); // 显式结束借用(通常不需要,作用域结束自动结束)
s.clear(); // 现在可以了
println!("清空后: '{}'", s);
}
}
- 实验:对
String取切片得到&str(通过&s[0..4]),在切片存活期间尝试修改原String,然后drop切片后再修改。 - 现象:切片正常打印;
s.clear()在切片存活期间若被取消注释则编译报错;显式drop(hello)提前结束切片借用后,s.clear()编译通过。 - 分析:
&str(字符串切片)本质是不可变引用——它不拥有数据,只是指向String内部某段字节范围的“窗口“。所以切片同样遵循借用规则二:存在不可变引用(切片)时,原变量不能修改。drop(hello)是 Rust 中显式提前结束引用生命周期的技巧,通常不需要——让作用域自然结束即可。 - 结论:切片是引用的一种,完全遵守借用规则。切片存在时原集合不能修改;切片结束后立即恢复。
5.13 实验:test_struct_ownership
#![allow(unused)]
fn main() {
struct Person {
name: String,
age: u32,
}
impl Person {
fn introduce(&self) {
println!("我叫 {},今年 {} 岁。", self.name, self.age);
}
fn have_birthday(&mut self) {
self.age += 1;
println!("生日快乐!现在 {} 岁了。", self.age);
}
fn destroy(self) {
println!("{} 被销毁了", self.name);
// self 在此函数结束时被 drop
}
}
fn test_struct_ownership() {
println!("\n=== 11. 结构体与所有权 ===");
let mut p = Person {
name: String::from("Alice"),
age: 30,
};
p.introduce(); // 不可变借用
p.have_birthday(); // 可变借用
println!("再次访问: {} 现在 {} 岁", p.name, p.age);
// p.destroy(); // 若调用,p 的所有权被消耗,之后 p 失效
}
}
- 实验:定义包含
String字段的结构体Person,提供&self、&mut self和self三种方法签名,依次调用。 - 现象:
introduce()(&self)可读字段;have_birthday()(&mut self)可修改age;两次调用后p仍可用。若取消注释p.destroy()(self签名),则p的所有权被消耗,后续不能再使用。 - 分析:三种方法签名对应三种所有权级别:
&self= 不可变借用,允许多个同时存在,只读不写,调用后实例仍可用。&mut self= 可变借用,独占读写权限,调用后实例仍可用(借用归还)。self= 获取所有权,调用后实例被消耗(move 进方法),方法结束时实例被drop。实际开发中极少使用。 结构体的字段各自独立遵循所有权规则——name: String不可 Copy,age: u32可 Copy。
- 结论:方法签名精确控制对结构体的权限级别。
&self和&mut self是惯用写法,self(消耗式)仅在少数场景(如构建器模式的build())中使用。
5.14 实验:test_mut_borrow_blocks_owner
#![allow(unused)]
fn main() {
fn test_mut_borrow_blocks_owner() {
println!("\n=== 12. 可变借用期间原变量被冻结 ===");
let mut data = 100;
{
let r = &mut data;
*r += 50;
// println!("原变量 = {}", data); // 编译错误:不能读取 data
println!("通过可变引用读取 = {}", r);
} // r 结束
println!("可变借用结束后,原变量 = {}", data);
}
}
- 实验:在可变引用
r存活期间,尝试通过原变量名data读取数据。 - 现象:通过
r读取和修改正常;若取消注释println!("原变量 = {}", data)则编译报错;r离开作用域后data恢复可访问,值已变为 150。 - 分析:可变借用期间,原变量的 R+W 权限被完全剥夺——不仅不能写,也不能读。这是 Rust 比许多语言的读写锁模型更严格的地方:写锁期间连读都被禁止。为什么?因为如果允许通过原变量读取,编译器就无法保证可变引用“独占“的语义——可能存在两条路径同时访问同一数据。这种“要么全部通过引用访问,要么就等引用结束“的强制约定,彻底消除了“以为只有一个引用在操作数据“的隐患。
- 结论:可变借用期间,原变量被完全冻结(不可读不可写);所有访问必须通过可变引用;引用结束后原变量自动恢复。这一机制保证了可变引用的独占性是绝对且可验证的。
5.15 实验:test_cannot_borrow_mut_from_immut(编译失败)
#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn test_cannot_borrow_mut_from_immut() {
let x = 5;
let y = &mut x; // 错误:不能对不可变变量创建可变引用
}
}
- 实验:观察此函数——对声明为不可变(
let x = 5,无mut)的变量尝试创建可变引用。 - 现象:编译失败,报错
cannot borrow x as mutable。 - 分析:可变引用意味着“我有权通过这个引用修改数据“。但
x本身被声明为不可变,这意味着“我不希望x的值被改变“。两者矛盾。Rust 不允许通过引用来绕过变量的不可变性声明——mut不仅是编译器的提示,更是安全承诺:如果变量没有mut,那么没有任何方式可以修改它的值。这保证了代码阅读者看到let x = ...(无mut)时,就能确信x在后续代码中不会发生变化。 - 结论:
mut声明是创建可变引用的前提。不可变变量 = 完全不可变,无法通过任何引用“走后门“修改。
5.16 实验:test_deref_mut
#![allow(unused)]
fn main() {
fn test_deref_mut() {
println!("\n=== 13. 解引用修改 ===");
let mut x = 10;
let r = &mut x;
*r = 20; // 解引用并赋值
println!("修改后 x = {}", x);
}
}
- 实验:创建可变引用
r指向x,使用*r = 20修改其指向的值,然后读取x。 - 现象:
*r = 20执行后,println输出修改后 x = 20,证明x确实被修改了。 - 分析:
r是&mut i32类型,它本身是一个引用(一个指向x的指针)。要修改引用指向的值,必须用*(解引用操作符)“穿透“引用到达目标。*r = 20等价于“找到r指向的那块内存,把 20 写进去”。在 Rust 中,对&mut T的解引用修改是唯一能改变引用目标值的方式。编译器确保了在r存活期间,x本身被冻结(见 5.14),所以*r = 20和后续println!("x = {}", x)之间没有冲突——后者在r已经离开作用域之后才执行。 - 结论:通过可变引用修改数据需要显式解引用(
*r)。解引用是“穿透引用操作目标“的语法,Rust 的借用检查器保证解引用操作的安全性。