第 7 章:上一章的痛苦体验过了吗?

所有权系统逼着你转移来转移去,烦死了是不是?

别慌,Rust 不是变态,它是完美主义者。既然给了你世界上最严格的内存管理,自然也要给你最优雅的解决方案。

这就是借用系统——让你在不失去所有权的前提下,优雅地使用数据。

是时候让你见识真正的 Rust 魔法了。

从笨拙到优雅:一个华丽转身

之前的笨拙做法

// 上一章的垃圾代码
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)  // 还要把所有权还回去,麻烦死了
}

这种代码写起来想死是不是?

Rust 的优雅解法

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);  // 借用,不转移所有权

    println!("The length of '{}' is {}.", s1, len);  // s1 依然有效!
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 离开作用域,但什么都不会发生,因为它只是借用

看到差别了吗?这就是文明与野蛮的区别。

一个 & 符号,解决了所有痛苦。

引用:借用的实现机制

引用就是一个指针,但比指针更安全、更智能。

  • Java 的引用:什么都能做,没有约束,运行时才知道出错
  • C++ 的指针:什么都能做,还能搞出悬垂指针杀死程序
  • Rust 的引用:编译期检查,保证安全,零运行时成本

不可变引用:只读的力量

fn main() {
    let s = String::from("hello");

    let r1 = &s;  // 创建一个不可变引用
    let r2 = &s;  // 可以有多个不可变引用

    println!("{} and {}", r1, r2);  // 完全没问题

    // r1.push_str("world");  // 编译错误!只读的,不能改
}

这就是默认的安全模式:你可以看,但不能动。

可变引用:写入的授权

想要修改?那就要明确申请"写入权限":

fn main() {
    let mut s = String::from("hello");  // 变量本身必须可变

    let r = &mut s;  // 创建可变引用
    r.push_str(", world");  // 现在可以修改了

    println!("{}", r);
}

注意这里的双重约束:

  1. 变量本身必须 mut - 数据的所有者同意修改
  2. 引用必须 &mut - 明确申请修改权限

这种明确性不是负担,是保护。

借用规则:编译器的铁律

现在来学习最重要的规则。这些规则看似严格,实际上是在拯救你的程序。

规则一:独占 vs 共享

在任何时刻,你只能拥有以下之一:

  • 一个可变引用
  • 任意数量的不可变引用
let mut s = String::from("hello");

let r1 = &s;      // 不可变引用
let r2 = &s;      // 更多不可变引用,OK
let r3 = &mut s;  // 编译错误!不能混合

println!("{}, {}, {}", r1, r2, r3);

为什么这么严格?

想象一下多线程环境:如果一个线程在读数据,另一个线程在写数据,会发生什么?数据竞争!程序崩溃!

Rust 在编译期就杜绝了这种可能。没有数据竞争,不是因为运气,是因为规则。

规则二:多个可变引用?想都别想

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;  // 编译错误!

println!("{}, {}", r1, r2);

两个写手同时修改一个文档?那还不乱套了?

作用域的智能判断

但 Rust 编译器不是死板的。它知道引用的实际使用范围:

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);  // r1 和 r2 到此结束生命

let r3 = &mut s;  // 现在可以了!
r3.push_str(", world");
println!("{}", r3);

编译器比你想象的更聪明。它知道引用什么时候真正"死掉"。

悬垂引用:编译器的最后一道防线

最恶心的 bug 是什么?指向无效内存的指针。

在 C/C++ 里,这种 bug 能让你调试到崩溃:

// C++ 的噩梦
char* get_string() {
    char local[] = "hello";
    return local;  // 返回栈上局部变量的地址,程序等死
}

Rust 表示:这种垃圾代码,想都别想编译通过。

fn main() {
    let reference_to_nothing = dangle();  // 编译错误!
}

fn dangle() -> &String {
    let s = String::from("hello");  // 局部变量
    &s  // 返回局部变量的引用?做梦!
}  // s 在这里死掉,引用指向无效内存

编译器直接拒绝:

error[E0106]: missing lifetime specifier
  --> src/main.rs:5:16
   |
5  | fn dangle() -> &String {
   |                ^ expected lifetime parameter

正确的做法?转移所有权:

fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 把所有权移出去,完美解决
}

性能对比:借用 vs 克隆

Java 的隐性成本

// Java 伪代码
public int calculate(List<String> data) {
    // 看起来没有拷贝,但实际上传递的是引用
    // 如果需要防御性拷贝,成本很高
    List<String> copy = new ArrayList<>(data);  // 整个列表都拷贝了
    return copy.size();
}

Rust 的零成本抽象

fn calculate(data: &Vec<String>) -> usize {
    data.len()  // 零拷贝,纯指针操作
}

let big_list = vec!["item"; 1000000];
let size = calculate(&big_list);  // 零成本!

借用只传递一个指针,无论数据多大,成本都是常数。

内存安全的数学证明

Rust 的借用规则不是拍脑袋想出来的,是有数学基础的。

如果有 N 个线程访问同一块数据:

  • 全是读操作:√ 安全,不会有冲突
  • 有一个写操作:√ 如果只有一个线程,安全
  • 多个写操作同时进行:✗ 数据竞争,危险

Rust 的规则:

  • 多个 &T:对应"全是读操作"
  • 一个 &mut T:对应"只有一个写操作"
  • 禁止混合:杜绝"多个写操作"

这就是类型系统保证并发安全的数学原理。

写在最后:从痛苦到优雅

还记得学所有权时的痛苦吗?那些痛苦是值得的。

借用系统给你的不只是语法糖,而是:

  • 内存安全 - 永远不会有悬垂指针
  • 并发安全 - 编译期杜绝数据竞争
  • 零运行时成本 - 引用就是原始指针,无额外开销
  • 表达力强 - 类型系统就是文档,一眼看懂函数的行为

当你掌握了借用,你就掌握了 Rust 的精髓。

但还有一个更深层的话题等着你:生命周期

当你的引用关系变得复杂,编译器有时无法自动推断引用的有效期。此时你需要手动标注生命周期,告诉编译器"这些引用之间的存活关系"。

准备好面对 Rust 最复杂的概念了吗?掌握了生命周期,你就是真正的 Rust 专家。