Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 语言完全没有回答:

  1. buffer 这块内存是谁的?—— 是 create_greeting 这个函数的?还是 main 的?语言层面没有定义。
  2. 谁负责释放 buffer—— 编译器会自动回收栈内存,但 main 里的 msg 并不知道这件事。它拿到了一个地址,然后理所当然地用了。
  3. 什么时候可以安全地使用 msg—— 没有规则告诉你这个指针还有没有效。你只能靠”脑子记”。

本质问题:C 语言只提供了 mallocfree 这两个操作,但没有建立一套 ”这块内存属于谁、由谁负责释放、什么时候释放”的清晰规则。程序员在脑子里各自维护一套隐式的、不统一的约定——这就是所有内存 bug 的温床。

1.3 什么是”所有权”?

从上面的分析中,我们可以自然地推导出”所有权”应该回答的三个问题:

  1. 谁创建了这块资源?(资源从哪来)
  2. 谁持有这块资源?(在使用期间,谁是它的主人)
  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 的做法是:

  1. 在语言层面定义了三条硬性的所有权规则(见第 3 节
  2. 通过**借用检查器(Borrow Checker)**在编译期验证所有代码是否遵守这些规则
  3. 违反规则的代码直接编译不通过——没有”不小心”,没有”下不为例”

从 C 的手动管理 → C++ 的建议式所有权 → Rust 的强制所有权,演进的脉络很清晰:把内存管理的正确做法从”程序员脑子里的约定”变成”编译器能检查的规则”


2. Rust 所有权核心三原则

Rust 的所有权系统建立在三条核心规则之上。每一条规则都不是随意设计的——它们各自解决了 C 语言内存管理中的一类具体问题。

原则一:每个值都有一个所有者

每个值在 Rust 中都有一个变量,称为其所有者。(一个值对应一个 owner)

为什么这么设计?——解决“谁负责释放“的问题。

回顾 C 语言的困境:一块 malloc 出来的内存,到底谁应该 free?没有规则,全靠约定。Rust 用这条规则给出了明确的答案:每个值都绑定到一个所有者变量上,这个变量负责该值的最终释放。没有人需要猜测“这块内存归谁管“——编译器帮你追踪,所有者一目了然。

原则二:同一时刻只能有一个所有者

同一时刻只能有一个所有者。(所有权可以转移,但无法共享——此处指默认的独占所有权;共享所有权通过 Rc/Arc 另行提供)

为什么这么设计?——解决“双重释放“和“数据竞争“的问题。

如果多个变量同时拥有同一块内存,每个变量离开作用域时都会尝试释放它——这就是双重释放。更隐蔽的是,如果两个所有者分别在不同的线程中修改这块内存,就产生了数据竞争。

Rust 的策略是:默认独占。一个值只有一个所有者,转移所有权后原变量立刻失效(编译器禁止你再使用它)。这从根本上杜绝了“两个人同时管一块内存“的混乱。对于确实需要共享的场景,Rust 提供了 Rc/Arc(引用计数)——这仍然是有规则的共享,而不是 C 语言那种“谁都能拿指针、谁都搞不清谁在管“的无序状态。

原则三:所有者离开作用域时自动释放

当所有者离开作用域,该值被自动丢弃。(自动调用 drop,释放资源)

为什么这么设计?——解决“忘记释放(内存泄漏)“的问题。

C 语言中,你必须在合适的时机手动调用 free。忘了?内存泄漏。在复杂的控制流(提前 returnbreak、异常路径)中,“合适的时机“往往很难找准。

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 的类型(StringVec、大多数自定义类型):赋值时发生移动——所有权转移,原变量失效。
#![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 各有一份堆数据的副本
}

这会带来两个问题:

  1. 性能陷阱:每次赋值都深拷贝堆数据,代价高昂。GC 语言靠运行时优化(copy-on-write 等)部分缓解,但 Rust 没有 GC。
  2. 语义混乱:两个变量持有两份”独立”的数据,修改一个不影响另一个——这真的是你想要的行为吗?在很多场景下,你只是想把数据传到另一个地方处理。

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
  • r1r2 各自借出 R(只读)。
  • 由于存在不可变借用,dataW 被冻结,直到所有不可变引用结束。
  • 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
  • 在此期间,原所有者 dataRW 完全被剥夺(即使它名义上还有 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);
}
}
  • 实验:分别对 i32String 类型变量赋值给新变量,然后尝试使用原变量。
  • 现象i32 赋值后原变量 x 仍可正常打印;String 赋值后,若取消注释 println!("{}", s1) 则编译报错 value used here after move
  • 分析i32 实现了 Copy trait——它只占 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 的所有权移入了函数参数,函数结束后参数离开作用域,Stringdrop。这展示了所有权原则三(所有者离开作用域自动释放)在函数边界的自然延伸:函数获得所有权 → 函数结束时释放,整个过程清晰、无泄漏。
  • 结论:函数传参同样遵循移动语义。参数获得了值的所有权,调用者失去了所有权。如果调用者后续还需要使用该值,应改用借用(传引用)。

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 创建两个不可变引用 &sr1r2),同时打印它们,然后尝试修改原变量。
  • 现象r1r2 均可正常打印;若取消注释 s.push_str("!") 则编译报错 cannot borrow s as mutable because it is also borrowed as immutable
  • 分析r1r2 各自借出了 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 的借用规则阻止可变引用与不可变引用共存。这不是语言的“限制“,而是数据安全的“保证“。
#![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 selfself 三种方法签名,依次调用。
  • 现象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 的借用检查器保证解引用操作的安全性。