Skip to content

Rust 深入理解 Ownership(所有权)与 Borrowing(借用)

如果说 Rust 有一道“新手门槛”,那一定是:

Ownership(所有权) 和 Borrowing(借用)

它们不是语法技巧,而是 Rust 整个内存模型的核心。 一旦理解,你会发现 Rust 并不反人类,反而非常严谨。


一、为什么 Rust 一定要有「所有权」?

在 Java / JS 中:

  • Java:GC 自动回收
  • JS:引擎自动管理内存

而 Rust 选择了一条更“硬核”的路:

不用 GC,也不让你手动 free,而是在编译期“证明”代码是安全的

所有权,就是 Rust 用来完成这件事的工具。


二、Ownership(所有权)到底是什么?

1️⃣ 所有权的直觉理解

你可以把 所有权 想象成:

“这块内存的唯一负责人”

  • 谁拥有它
  • 谁负责释放它
  • 谁说了算

Rust 的规则非常严格:

一块资源,只允许一个负责人


2️⃣ 所有权的三条铁律(必须牢记)

text
1. 每个值都有一个所有者
2. 同一时间只能有一个所有者
3. 所有者离开作用域,值会被释放

这三条规则,贯穿你写的每一行 Rust 代码。


三、Move(所有权转移)——新手最容易踩坑的地方

示例 1:String 的 move

rust
let s1 = String::from("Rust");
let s2 = s1;

// println!("{}", s1); // ❌ 编译错误

发生了什么?

  • String 数据在 堆上
  • let s2 = s1 并不会复制字符串
  • 而是 转移所有权(move)

结果是:

变量是否还能用
s1❌ 不能
s2✅ 可以

👉 Rust 直接禁止你再用 s1, 从根源上杜绝 释放后使用(use after free)


示例 2:函数参数中的 move

rust
fn print_str(s: String) {
    println!("{}", s);
}

print_str(s1); // s1 被 move

let s = String::from("Hello");
print_str(s);
// println!("{}", s); // ❌

函数参数默认会获取所有权

你可以把它理解为:

把变量“交给函数去处理”, 函数用完就会负责释放。


四、Copy vs Move:为什么 i32 不会出问题?

rust
let x = 5;
let y = x;

println!("{}", x); // ✅

原因:i32 是 Copy 类型

特性Copy 类型
存储位置
拷贝成本极低
行为按位复制

Copy 类型特点:

  • 存在 栈上
  • 复制成本极低
  • 赋值时直接复制一份值

Rust 中常见的 Copy 类型:

  • i32
  • u64
  • bool
  • char
  • (i32, i32)

而下面这些不是 Copy:

  • String
  • Vec<T>
  • HashMap<K, V>

String / Vec / HashMap 都是 Move 类型


五、Clone:显式的“我知道我在复制”

rust
let s2 = s1.clone();

clone() 表示:

“我明确知道要复制一份堆数据,并愿意承担性能成本”

这是 Rust 的设计哲学之一:

让昂贵操作显式出现


六、Borrowing(借用):不拿走所有权,也能用数据

如果只有所有权,Rust 会非常难用。 所以 Rust 引入了 Borrowing(借用)


1️⃣ 什么是借用?

rust
let s = String::from("Rust");
let r = &s;

&s 表示:

  • 不获取所有权
  • 只是“借来看一眼”

你可以把借用理解成:

“临时访问权”


2️⃣ 不可变借用(&T

rust
fn print_string(s: &String) {
    println!("{}", s);
}

let s = String::from("Rust");
print_string(&s);
println!("{}", s); // ✅ 仍然可用

特点:

  • 只读
  • 可以同时存在多个
  • 不影响所有者
rust
let r1 = &s;
let r2 = &s;

3️⃣ 可变借用(&mut T

rust
fn add_to_string(s: &mut String) {
    s.push_str(" + 追加");
}

let mut s = String::from("Rust");
add_to_string(&mut s);

特点:

  • 可以修改数据
  • 同一时间只能有一个
  • 不能和不可变借用共存

七、借用规则(Rust 安全性的核心)

同一时间:

  • ✅ 多个不可变借用
  • ❌ 不可变借用 + 可变借用
  • ❌ 多个可变借用
  • 错误示例
rust
let mut s = String::from("Rust");

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

Rust 在编译期就阻止了“数据竞争”。


八、什么时候该用 &&mut,什么时候该 move(重点)

这是新手最容易迷糊、但最重要的一节

你可以用下面这套 决策模型 来判断。


1️⃣ 什么时候用 &T(不可变借用)

只读,不修改,不负责释放

rust
fn print_string(s: &String) {}

适用场景:

  • 打印
  • 校验
  • 计算
  • 查看状态

一句话总结:

“我只是用一下,不碰、不拿、不背锅”


2️⃣ 什么时候用 &mut T(可变借用)

我要修改原数据,但不想拥有它

rust
fn add_to_string2(s: &mut String) {
    s.push_str(" + 追加");
}

这里:

  • 所有权 ❌ 没有转移
  • 修改的是原变量
  • 修改权是临时的

一句话总结:

“我改一下,用完就还你”


3️⃣ 什么时候该 move(直接用 T

函数要“接管”这份数据

rust
fn consume(s: String) {}

适用场景:

  • 函数内部创建并返回
  • 需要长期保存
  • 明确语义上的“消费”

一句话总结:

“交给我,我负责到底”

4️⃣ 一张终极判断表((强烈推荐)请记住)

问自己三个问题:

  1. 我只是读取吗?&T

  2. 我需要修改,但不想负责释放吗?&mut T

  3. 我需要长期持有 / 存储 / 返回 / 转移责任吗?T(move)

需求选择
只读&T
修改原数据&mut T
接管 / 返回 / 保存T(move)
简单值(i32 等)直接传

九、可变借用(&mut) ≠ 所有权转移(非常重要)(新手常见误解)

rust
fn add_to_string2(s: &mut String) {
    s.push_str(" + 追加");
}
  • 这里发生的是:
  • s 始终属于调用者
  • 函数只是暂时借用“修改权限”
  • 没有释放责任

👉 可变借用 ≠ 拿走所有权

这是很多新手的误区。


十、为什么不能返回局部变量的引用?

rust
fn generate_str() -> &String {
    let s = String::from("字符串");
    &s // ❌
}

原因一句话就够:

你在返回一个指向“即将被释放内存”的引用

s 在函数结束时就被释放了, Rust 通过生命周期检查直接禁止这种行为。


十一、终极总结(新手必记)

  • 所有权:唯一负责释放资源
  • move:责任转移
  • copy:值复制
  • borrow:临时访问
  • &:只读借用
  • &mut:可写借用

如果你能在写代码时下意识回答一句: “现在谁是 owner?” 那你已经真正入门 Rust 了。

结语

Rust 的所有权与借用,本质是在回答三件事:

  • 谁负责释放?
  • 谁能访问?
  • 能不能修改?

Rust 不靠运行时兜底,而是在编译期把所有问题解决掉

一旦你建立了这套心智模型, 后面的生命周期、并发、异步,都会变得自然。