Skip to content

所有权延申

Rust 所有权机制详解

在 Rust 中,所有权(Ownership)是其最独特的特性,它让 Rust 无需垃圾回收(GC)就能保证内存安全.

一、谁可以拥有所有权?

在 Rust 的代码世界里,主要有三类实体可以充当"所有者"的角色:

1.变量(Variables)

这是最常见的形态.当你声明 let s = String::from("hello"); 时,变量 s 就直接拥有了该字符串的所有权.

2.数据结构(Data Structures)

结构体(Structs)、枚举(Enums)或容器(如 VecHashMap)可以拥有其内部成员的所有权.例如,一个结构体字段如果是 String 类型,那么该结构体实例就拥有这个字符串.

3.函数(Functions)

当数据作为参数传递给函数(且不是引用传递)时,该函数的作用域会接管该数据的所有权.

二、怎么才算"拥有"了所有权?

拥有所有权意味着对某个值(Value)拥有最终决定权,遵循三条核心规则:

  • 唯一性: 每个值在任一时刻有且只有一个所有者
  • 负责清理: 当所有者离开其作用域(Scope)时,它拥有的值会被自动清理(调用 drop 释放内存)
  • 可转移性(Move): 将值赋值给新变量或传给函数时,所有权会从旧所有者"移动"到新所有者,旧变量立即失效

三、"获得"所有权背后的底层机制

这些实体之所以能获得并维持所有权,并非靠魔法,而是因为 Rust 的编译器(Compiler)在背后做了一套严密的帐本记录.

本质: 编译器在编译阶段会为每个变量、数据结构和函数参数分配一个独特的内存地址,并将这个地址登记在一个内部帐本上.当你声明一个变量或传递一个值时,编译器会更新这个帐本,确保所有权规则得到遵守.

关键点: 所有权不是运行时的某种实体,而是编译器在编译时对内存管理责任的"接力赛"记录.

1.内存布局: 栈(Stack)管理堆(Heap)

String 为例:

  • 堆上: 存真实数据(如 "hello")
  • 栈上: 存一个包含"地址指针、长度、容量"的数据块

获得所有权的本质: 某个变量(或字段)在栈上持有了那个指向堆内存的唯一指针.

2.赋值即"移动"(Move)

在其他语言中,赋值通常是复制指针(导致两个变量指向同一块内存).而在 Rust 中:

  • 执行 let b = a; 时,编译器将 a 的指针信息拷贝给 b
  • 关键动作: 编译器在内部帐本上将 a 标记为无效(Invalid),而 b 成为新的所有者

通过这种"废除前任"的方式,确保了"现任"所有者的唯一性.

3.作用域绑定(Scope Binding)

编译器在编译阶段会严格追踪每个变量的生命周期:

  • 登记: 声明变量时,编译器记录该内存地址归此变量管理
  • 注销: 运行到作用域结束(})时,编译器自动在此处插入 drop 指令

因为编译器确保了只有一个变量能活到最后去执行这个清理动作,所以所有权显得极其稳固且安全.

四、引用互斥(借用检查)的底层逻辑

1.核心矛盾: 内存重分配(Reallocation)

  • 物理现象: 当 StringVec 等集合在堆(Heap)上增长且空间不足时,Rust 会申请更大的新内存,将数据从旧地址搬移到新地址,更新变量的指针信息并释放旧内存
  • 后果: 搬家后,所有指向旧地址的指针都会变成野指针(Dangling Pointer)

2.为什么"可变引用"与"不可变引用"互斥?

  • 场景: 假设允许 let r1 = &s;(不可变)和 let r2 = &mut s;(可变)同时存在
  • 冲突: 如果通过 r2 进行了扩容操作(导致搬家),那么 r1 手里持有的内存地址瞬间失效
  • 原因: r2 扩容后更新了 s 的指针信息,但 r1 仍然指向旧地址,导致访问时会崩溃
  • 安全隐患: 此时通过 r1 读取数据,程序会访问已被释放或分配给他人的内存,导致崩溃或安全漏洞
  • 底层结论: 不可变引用要求数据"原地不动"以便安全读取,而可变引用拥有"让数据搬家"的权力,两者在物理逻辑上无法并存

3.为什么"多个可变引用"互斥?

  • 数据竞争(Data Race): 如果两个 &mut 同时修改同一块内存,且其中一个触发了搬家,另一个 &mut 指向的地址会立即失效
  • 一致性失效: 即使不搬家,两个入口同时修改同一份数据也会导致不可预测的逻辑错误
  • 底层结论: 确保同一时间只有一个"修改入口",是为了保证修改操作对内存状态的绝对控制

4.编译器的"守护"流程

  1. 锁定状态: 当你借出一个 &mut 时,原变量在栈(Stack)上的 ptrlencap 信息处于被锁定的读写禁区
  2. 原子更新: 修改完成后,通过可变引用直接回写更新原变量在栈上的三元组信息(指针、长度、容量)
  3. 释放锁定: 只有当可变引用生命周期结束,原变量才重新解锁,确保外界永远不会拿到"过时的"栈信息

总结

所有者类型获得方式关键特性
变量声明唯一性、自动清理
数据结构包含关系字段生命周期绑定
函数参数压栈作用域接管

核心原则: 谁手里拿着那张唯一的"内存地契"(指针),且没被编译器失效,谁就是所有者.

借用检查: 引用的互斥规则是为了防止"内存搬家"导致的指针失效,确保任何引用在存续期间,其指向的内存地址永远真实有效.

本质认识: 所有权和借用规则就是编译器的"静态逻辑检查",这一点和 TypeScript 类型检查系统类似.

基于 MIT 协议发布