Rust 最核心、最独特的特性就是所有权(Ownership);它是 Rust 无需垃圾回收(GC)就能保证内存安全的关键。
所有权
所有权不仅是一组规则,更是一种编译器管理资源的思维模型;其本质是RAII (Resource Acquisition Is Initialization) 的严格实现。
程序运行时会不断申请内存(栈、堆),如何释放则有:
-
手动管理(C/C++):
malloc后必须记得free。容易出现:- 忘记释放→内存泄漏:可用的系统内存会被逐渐耗尽,最终导致程序运行越来越慢,甚至直接崩溃;
- 重复释放→双重释放:破坏系统内部的内存管理结构(堆结构),通常会直接触发程序崩溃(Abort),或者造成极其隐蔽的数据损坏和安全漏洞;
- 指针留而不用→悬垂指针:不小心再次访问或修改它指向的内存,就会引发"未定义行为"------可能是读取到垃圾数据,也可能是直接导致程序段错误(Segmentation Fault)崩溃。
-
垃圾回收(GC,Java/Go/Python等):语言的运行时(Runtime)或虚拟机(如 JVM)会在后台默默工作,定期启动一个"垃圾回收器",进行"可达性分析":
- 运行时开销与卡顿(STW):扫描和清理内存时,往往需要暂停所有的业务线程(即著名的 Stop-The-World 现象);
- 额外的资源消耗:GC 本身运行需要消耗 CPU 和内存资源,且为了保证回收效率,通常会预留一部分内存,内存利用率不如手动管理极致;
- 无法管理非内存资源:GC 只能回收内存;像文件句柄、网络连接、数据库连接这类"非内存资源",依然需显式关闭。
-
所有权系统(Rust):每块内存都明确属于某个"所有者", 通过编译器追踪变量的作用域,在编译阶段就确定了内存释放的时机:
- 零成本抽象:所有内存释放时机都在编译时算好,运行时完全不需要 GC 参与,因此没有 STW 卡顿和额外的运行时内存/CPU 开销,性能直接对标 C/C++。
- 编译期杜绝内存错误:Rust 的编译器(借用检查器)极其严格;把内存 Bug 的排查从"线上运行时"提前到了"写代码时"。
- 极高的学习门槛:需要彻底转变思维,去理解"所有权"、"借用(Borrowing)"、"生命周期(Lifetimes)"等概念。
基本规则
Rust 的所有权基本规则------把内存管理的责任在编译阶段就通过严格的规则确定下来:
- 唯一所有者:Rust 中的每个值(比如一个字符串)在同一时刻只能有一个所有者(Owner)。
- 作用域决定生死:当所有者离开它的作用域(如函数、代码块结束)时,自动调用
drop函数释放内存。 - 所有权转移(Move):把一个值赋给另一个变量,所有权就"移交"了,原来的变量立刻失效。
所有权虽复杂、难掌握,但会带来如下红利:
-
内存占用可预测:确定性的资源释放
- 告别 GC 的"随机卡顿";内存回收的时机完全确定:变量一旦离开其作用域,内存就会被立即、自动地释放(调用
drop)。 - 极致的缓存友好:因为无需 GC扫描,Rust 程序的内存布局通常更加紧凑且连续;极大地提升了 CPU 缓存的命中率,让程序的运行速度更加平稳、可预测。
- 告别 GC 的"随机卡顿";内存回收的时机完全确定:变量一旦离开其作用域,内存就会被立即、自动地释放(调用
-
并发安全:把"数据竞争"扼杀在编译期
- 编译期拦截并发 Bug:借用检查器在编译阶段强制执行"共享不可变,可变不共享"的铁律;避免了多线程同时修改同一块数据(数据竞争)类的隐蔽 Bug;
- 无锁编程的可能:编译器排除了 90% 以上的并发冲突,很多场景下甚至不需要加锁就能写出安全的并发代码,极大地降低了并发编程的心智负担。
-
零成本抽象:跑得快还能写得爽
- 高级语法,底层速度:编译器会在编译阶段把高级抽象(如
iter().map().filter())直接展开并优化成最底层的机器码;所有的安全检查(如边界检查、所有权规则)也都发生在编译期,最终生成的二进制文件在运行时没有任何额外的"包袱",性能与手写的 C/C++ 代码几乎无异。
- 高级语法,底层速度:编译器会在编译阶段把高级抽象(如
drop
drop 机制是确保资源确定性释放(Deterministic Resource Cleanup)的核心:
- 编译器自动生成析构调用(
Droptrait):当一个值离开其所有者作用域时,编译器会自动插入对drop方法的调用。 - 显式所有权终结(
std::mem::drop)
| 特性 | Drop Trait | std::mem::drop |
|---|---|---|
| 角色 | 定义者(规定如何清理) | 执行者(强制立即清理) |
| 触发机制 | 编译器自动注入 | 开发者手动调用 |
| 主要目的 | 实现 RAII,防止内存泄露 | 管理生命周期,优化资源占用 |
| 底层实现 | 编译器元编程 | 简单的所有权转移 |
赋值
Rust 将类型分为两类 :
| 类型范畴 | 特征 | 行为 | 示例 |
|---|---|---|---|
| Copy 类型 | 数据完全存储在栈上,不持有外部资源。 | 按位拷贝,赋值后原变量依然有效。 | i32, bool, f64, char, 元组 (仅含 Copy 类型)。 |
| Non-Copy 类型 | 管理堆内存或其他资源。 | 移动语义,赋值后原变量失效。 | String, Vec<T>, Box<T>, 自定义结构体。 |
赋值时,所有权变化:
| 类型分类 | 典型代表 | 存储位置 | 赋值行为 | 原始变量状态 |
|---|---|---|---|---|
| 基础标量 | i32, bool |
栈 | Copy (按位复制) | 保持有效 |
| 基础组合 | [i32; 10], (f64, bool) |
栈 | Copy | 保持有效 |
| 智能指针 | Box<T>, Rc<T> |
栈 + 堆 | Move (或引用计数) | 失效 |
| 动态集合 | Vec<T>, String |
栈 + 堆 | Move | 失效 |
| 复合结构 | struct, enum |
取决于成员 | 默认 Move (除非手动派生 Copy) | 失效/部分有效 |
Move 语义
移动语义(Move Semantics) 是 Rust 内存管理的核心。替代了传统语言中的"深度拷贝"或"引用计数",通过在编译期追踪资源的"所有权"来保证内存安全。
当一个变量赋值给另一个变量或传递给函数时,如果该类型没有实现 Copy Trait,所有权就会发生移动(Move)。
- 所有权转移:原变量(Source)将资源(堆内存、文件句柄等)的所有权转交给新变量(Target)。
- 失效状态:一旦所有权移动,原变量进入"未初始化"状态,编译器将禁止再次访问它。
- 浅拷贝 + 废弃原指针:移动只是简单的栈上的元数据拷贝(如指针、长度、容量),不复制堆数据;关键在于原变量的生命周期被立即终止。
移动出发的场景:
- 变量赋值
- 函数参数与返回
- 闭包捕获: 使用
move关键字强制闭包获取环境变量的所有权
perl
//////////////////////////////////////////
// 函数转移场景
fn take_ownership(s: String) {
println!("{}", s);
} // s 在此处离开作用域并被释放 (Drop)
let s1 = String::from("Rust");
take_ownership(s1);
// println!("{}", s1); // 错误!s1 已被函数"吃掉"了
//////////////////////////////////////////
// 闭包转移场景
let data = vec![1, 2, 3];
let handle = std::thread::spawn(move || {
println!("{:?}", data); // data 所有权移入线程
});
Copy 语义
Copy Trait(隐式的"按位复制"): 告诉编译器,该类型的值可以通过简单的位拷贝(Bitwise Copy/memcpy)来复制,且不会产生任何资源管理问题(原变量依然有效)。
- 只有在栈上拥有固定大小且不管理外部资源(如堆内存、文件句柄)的类型才能实现
Copy。 - 如果一个类型实现了
DropTrait(自定义析构逻辑),则不允许实现Copy。
常见类型:所有的标量类型(i32, u64, f64, bool, char)以及只包含 Copy 类型的元组或固定长度数组。
Clone 语义
Clone Trait(显式的"深拷贝"):用于处理需要进行"深度复制"或者涉及复杂逻辑的场景。
需要两个变量同时拥有独立的数据副本时,使用 .clone()。
- 必须通过手动调用
.clone()来触发。执行开发者定义的逻辑,通常包括在堆上分配新内存并复制数据。 - 开销可能非常大(取决于数据规模)。
Copy类型必须同时实现Clone(因为按位拷贝也是一种特殊的克隆),但Clone类型不一定要实现Copy。
rust
#[derive(Debug, Clone, Copy)] // 允许 Copy
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone)] // 仅允许 Clone,因为包含 String(堆数据)
struct User {
id: u32,
name: String,
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // 自动 Copy,p1 依然可用
println!("p1: {:?}", p1);
let u1 = User { id: 1, name: String::from("Alice") };
// let u2 = u1; // 如果这样做,u1 将失效(Move)
let u2 = u1.clone(); // 显式克隆堆内存,u1 依然有效
println!("u1: {:?}", u1);
}
| 特性 | Copy | Clone |
|---|---|---|
| 触发方式 | 隐式(赋值、传参时自动发生) | 显式(必须手动调用 .clone()) |
| 底层实现 | 编译器执行 memcpy(极快) |
开发者定义逻辑(可能涉及堆分配,慢) |
| 所有权影响 | 复制值,原值保留 | 复制值,原值保留 |
| 适用场景 | 简单的栈数据(数值、布尔等) | 复杂的资源管理(String, Vec, RC 等) |
| 主要目标 | 提高便利性,消除 Move 语义的限制 | 提供一种显式复制复杂数据副本的手段 |
语义判断
复杂类型的语义取决于"组合法则(Composition)"以及"显式声明(Explicit Opt-in)":
-
元素 / 标量类型 (Primitives / Scalars) :大小在编译期完全固定,且全部存储在栈(Stack)上 ,且不涉及任何外部资源。
- 默认
Copy(隐式按位复制) : 赋值或传参时,直接执行底层的memcpy汇编指令,极度轻量 。
- 默认
-
数组 (Arrays
[T; N]) 与 元组 (Tuples(T, U)) : 分配在栈上的连续内存块;语义严格绑定于其包含的元素。继承性:- 所有元素都实现了
Copy(例如[i32; 4]),则为Copy的 ; - 只要有一个元素是
Move语义,则退化为Move语义
- 所有元素都实现了
-
结构体 (Structs)
-
默认
Move(必须显式 Opt-in 才能 Copy) : 即使结构体的所有字段都分配在栈上且都是Copy类型,结构体默认依然是Move语义; -
让结构体具备
Copy语义,必须满足两个条件:- 所有内部字段都实现了
Copy。 - 显式添加
#[derive(Copy, Clone)]宏。
- 所有内部字段都实现了
-
-
集合 (Collections:
Vec,String,HashMap等) : 典型的胖指针(Fat Pointer),栈上存储元数据(指针、长度、容量),堆(Heap)上存储实际变长数据- 绝对的
Move语义,深度克隆需显式Clone
- 绝对的
-
智能指针 (Smart Pointers) : 系统级编程的核心,它们改变了默认的所有权模型
-
Move语义,但Clone的行为各异 -
Box<T>(独占堆分配)- Move: 转移独占所有权。
- Clone: 触发堆上的深度拷贝(类似集合)。
-
Rc<T>/Arc<T>(引用计数 / 线程安全引用计数)- Move: 转移对当前计数的拥有权。
- Clone: 触发浅拷贝(Shallow Copy);仅增加引用计数,底层堆数据共享。
-

引用与借用
引用(References) 允许"借用"值而不取得其所有权;本质上是在编译期执行的动态锁策略,旨在彻底消除数据竞争(Data Races)和挂起指针(Dangling Pointers):
-
引用:不拥有所有权的指针,用于临时访问数据。
-
借用(Borrowing):通过引用访问值的行为,相当于所有权的临时租约;借用检查器(Borrow Checker)会静态分析每一个引用的作用域,确保:
- 没有悬垂引用
- 同一时间不存在对同一数据的可变与不可变借用
- 同一时间最多只有一个可变借用
| 存在的引用 | 允许再创建 &T |
允许再创建 &mut T |
|---|---|---|
| 无引用 | ✅ | ✅ |
只有 &T |
✅ | ❌ (不能打破只读) |
只有 &mut T |
❌ | ❌ (不能同时存在) |
共享借用 &T
共享借用通过不可变引用 &T 实现;可同时存在多个不可变引用,所有人都只能读取,不能修改:
- 多个
&T可以共存,因为它们只读,互不干扰:只要不改变数据,多个人同时看是安全的。 - 在任意一个不可变引用存在的期间,原值不能被移动或修改,但可以通过这些引用读取。
- 引用作为函数参数,既能让函数访问数据,又不会夺走所有权。
独占借用 &mut T
可变引用 &mut T是独占的;在同一时刻,只能存在一个可变引用;且一旦有了可变引用,就不能再有任何其他引用(无论是可变还是不可变)。
这种"排他性"是为了消除数据竞争(data race):如果一个可变引用在修改数据,同时又有别的引用在读,结果就是未定义行为。Rust 在编译期直接禁止这种可能。