





















Rust 所有权 Move 语义 借用规则 Copy / Clone 多语言对比
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- 所有权三规则:每个值有唯一所有者、值随所有者超出作用域而 drop、所有权可转移(Move)
- Move 语义:堆上数据的所有权转移,而非浅拷贝;转移后原变量不可用
- Copy trait:栈上数据自动按位复制,无所有权转移;Clone 是显式深拷贝
- 借用规则:同一时刻可变借用或多个不可变借用,编译期消除数据竞争
次重点(理解即可)
- 智能指针 Box<T>、Rc<T>、Arc<T> 与所有权的交互
- 所有权与其他语言 GC/手动管理的本质区别
- Unsafe Rust 中对所有权规则的绕过
CONTENTS
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,建议在通读全文后作为复习使用
所有权是 Rust 用来管理堆内存的一套规则体系——每个值有且仅有一个"所有者"(Owner),所有者负责在值超出作用域时调用 Drop 释放资源。这使得 Rust 不需要垃圾回收器(GC)就能自动管理内存:所有权的转移(Move)、复制(Copy)和释放(Drop)都在编译期确定,没有运行时开销。简单说,所有权 = "谁负责清理这块内存"。
规则一:Rust 中的每个值都有一个所有者(Owner);规则二:同一时刻每个值只有一个所有者;规则三:当所有者超出作用域(scope)时,值将被删除(drop)。这三条规则完全由编译器强制执行,违反任何一条都会导致编译错误。这三条规则是 Rust 内存安全体系的基石——第一条定义了"谁来负责",第二条定义了"唯一性保证",第三条定义了"何时清理"。
Move 语义是指将值的所有权从源变量转移给目标变量,转移后源变量失效,堆内存不发生复制。对于 String 这样的堆上数据,赋值操作 let s2 = s1; 会将 s1 持有的堆上数据指针的所有权转移给 s2,s1 的内部指针被设为空,s2 成为唯一持有者。当 s1 和 s2 超出作用域时,不会出现"双重释放"(double free)——因为 s1 已经不再拥有那块内存。
let s1 = String::from("hello"); // s1 拥有堆数据的所有权
let s2 = s1; // Move: s1 将所有权转移给 s2
// println!("{}", s1); // error: s1 的所有权已经转移,不再有效
println!("{}", s2); // OK: s2 是新的所有者
Copy 是隐式的、按位复制的、无额外操作的;Clone 是显式的、可以任意复杂复制的、需要手动调用。实现了 Copy trait 的类型(如 i32、f64、bool)在赋值时自动按位复制,原变量仍然有效——因为这些类型足够简单,复制成本可忽略不计且不存在资源管理问题。而 Clone 是 Copy 的超集(Clone: Copy),任何能 Copy 的类型也能 Clone,但反之不一定。
// Copy:隐式,零成本
let a: i32 = 42;
let b = a; // Copy: a 和 b 各持有一个独立的 i32 值
println!("{} {}", a, b); // OK: 两个都有效
// Clone:显式,可能有成本
let s1 = String::from("hello");
let s2 = s1.clone(); // Clone: 深拷贝,s1 和 s2 都有效
println!("{} {}", s1, s2); // OK: "hello hello"
因为对于拥有堆上资源(如堆内存、文件句柄、网络连接)的类型,自动按位复制会导致多个所有者指向同一资源,从而引发双重释放或数据竞争。编译器通过 Copy trait 的设计来强制开发者对"昂贵复制"和"资源管理"做出显式决策——如果一个类型持有堆数据,它就不能是 Copy(因为 Move 比 Copy 更安全);如果想复制,必须调用 .clone(),这是开发者的有意识决策。
会。当把值作为参数传入函数时,所有权会转移给函数参数(如果类型是 Move 类型);函数也可以将所有权通过返回值转移回来。这是 Rust 最优雅的设计之一——函数通过参数"接收"所有权,通过返回值"归还"所有权。如果不想转移所有权,可以借用(&)或使用智能指针(Rc/Arc)。
fn take_ownership(s: String) { // s 获得字符串的所有权
println!("{}", s);
} // s 超出作用域,字符串被 drop
fn main() {
let s = String::from("hello");
take_ownership(s); // s 的所有权转移到函数参数
// println!("{}", s); // error: s 已经不再拥有这个字符串
}
fn restore_ownership() -> String { // 通过返回值恢复所有权
let s = String::from("world");
s // s 的所有权转移给调用者
}
结构体的所有权取决于它的字段——只要有一个字段不是 Copy,整个结构体就不能是 Copy。这是 Rust 的"零成本抽象"原则的体现:如果结构体包含 String、Vec<T>、Box<T> 等堆上数据类型,这些字段拥有资源管理职责,无法按位复制,因此结构体本身也不能是 Copy。赋值给新变量时,整个结构体会 Move(所有权转移)。
struct Person {
name: String, // 堆数据,不是 Copy
age: i32, // 栈数据,是 Copy
}
let p1 = Person { name: String::from("Alice"), age: 30 };
let p2 = p1; // Move: p1 的所有权转移给 p2
// println!("{}", p1.name); // error: p1 不再拥有这个 Person
// 如果希望 name 也能复制,需要 Clone:
let p3 = Person {
name: p1.name.clone(), // 深拷贝
age: p1.age, // Copy: i32 自动复制
};
println!("{} {}", p3.name, p3.age);
Move 转移了值的所有权,借用(&)只是临时"借阅"值的所有权,原所有者不变。Move 后原变量失效,后续无法使用;借用后原变量仍然有效,借用期结束后自动归还。借用本质上是在不转移所有权的前提下访问数据,是 Rust 中最常用的模式——它让函数可以"看"数据而不"拿走"数据。
let s1 = String::from("hello");
// Move: s1 失效
let s2 = s1;
// s1 不再可用
// Borrow: s1 仍然有效
let s3 = &s1;
println!("{} {}", s1, s3); // s1 和 s3 都有效
可变借用(&mut T)允许修改借来的数据,但同一时刻只能存在一个可变借用(不能和其他任何借用共存);不可变借用(&T)只能读取数据,但可以同时存在多个。这是 Rust"独占访问"(Exclusive Access)原则的具体体现——要么多个人一起读书(多个不可变借用),要么一个人单独编辑(一个可变借用),不能同时有编辑者又有阅读者。这条规则由借用检查器在编译期强制执行。
let mut v = vec![1, 2, 3];
let r1 = &v; // 不可变借用:OK
let r2 = &v; // 多个不可变借用:OK
println!("{:?}", r1);
// 可变借用:在不可变借用之后创建可变借用(NLL 之后合法)
let rm = &mut v;
rm.push(4);
// rm 在这里不再使用
let r3 = &v; // OK: rm 已经结束
println!("{:?}", r3);
所有权系统在编译期确定何时释放内存,无运行时开销且行为可预测;GC 在运行时追踪引用并异步回收内存,有额外开销且释放时机不确定。GC 语言(Java、Go、Python)通过追踪"还有哪些引用指向这块内存"来判断是否需要保留这块内存。Rust 则通过"谁拥有这块内存"来判断——所有者超出作用域时立即回收,没有 GC 的"Stop the World"暂停,没有引用计数(除非显式使用 Rc/Arc)。所有权系统让 Rust 获得了 C/C++ 的性能,同时获得了内存安全的保证。
双重释放是指同一块内存被释放两次,导致堆元数据损坏或安全问题,是 C/C++ 中最常见的安全漏洞之一。Rust 通过 Move 语义防止双重释放:当 String 被赋值给另一个变量时,原变量的内部指针被设为空(null),原变量不再持有任何堆内存的所有权。当两个变量各自持有不同的堆内存时,各自析构时释放各自的内存——不会冲突。
// C++ 中的双重释放(危险):
// char* s1 = new char[6]; strcpy(s1, "hello");
// char* s2 = s1; // 浅拷贝,两个指针指向同一块内存
// delete[] s1; delete[] s2; // 双重释放!UB!
// Rust 中不会有这个问题:
let s1 = String::from("hello");
let s2 = s1; // Move: s1 的所有权转移,s1 的内部指针被清空
// Rust 保证同一时刻只有一个所有者
// s1 和 s2 各自持有不同的内存(或 s1 完全失效)
Rc<T> 通过"共享所有权"(Shared Ownership)的概念绕过单一所有者限制——堆数据的所有权归 Rc 包装器持有,多个变量可以持有指向同一个 Rc 的引用,引用计数跟踪活跃的所有者数量,最后一个 Rc 超出作用域时才 drop 数据。但 Rc<T> 是单线程的(不提供原子性);对于多线程场景,需要使用 Arc<T>(Atomic Rc),它的引用计数操作是原子的,线程安全的。
use std::rc::Rc;
let s1 = Rc::new(String::from("hello"));
let s2 = Rc::clone(&s1); // 共享所有权,引用计数 +1
println!("引用计数: {}", Rc::strong_count(&s1)); // 2
{
let s3 = Rc::clone(&s1);
println!("引用计数: {}", Rc::strong_count(&s1)); // 3
} // s3 超出作用域,引用计数 -1
println!("引用计数: {}", Rc::strong_count(&s1)); // 2
Rc<T> 内部使用非原子引用计数(Rc),多个克隆共享同一个计数,如果允许 &mut T 可变借用,就可能在多线程场景中产生数据竞争——这与 Rust 的安全目标冲突。对于需要内部可变的共享所有权场景,Rust 提供了 RefCell<T>(单线程)和 Mutex<T>/RwLock<T>(多线程)作为解决方案——它们在运行时执行借用规则检查,而不是编译期。
Rust 的借用规则和所有权系统从语言层面保证了数据竞争的安全——同一时刻只有一个可变引用(独占写)或多个不可变引用(并发读),两者不能同时存在。在单线程中这是编译期检查;在多线程中,Arc<T>(原子引用计数)配合 Mutex<T>(互斥锁)或 RwLock<T>(读写锁)实现线程安全。Send 和 Sync trait 是 Rust 标记类型是否可以在线程间传递的机制——编译器自动推断大多数类型的这两个 trait,进一步减少数据竞争风险。
单线程场景用 Rc<RefCell<T>>(引用计数 + 内部可变性),多线程场景用 Arc<Mutex<T>>(原子引用计数 + 互斥锁)或 Arc<RwLock<T>>(读写锁)。这两组组合分别被称为"内部可变性模式"——它们在不违反所有权规则的前提下,提供了共享可修改状态的能力。Rc<RefCell<T>> 是编译期安全检查的"运行时补偿",Arc<Mutex<T>> 是多线程版的等效方案。
C++ 的 RAII 只负责"在析构时释放资源",但允许指针自由复制(浅拷贝),没有所有权的概念;Rust 的所有权系统在 RAII 基础上增加了"单一所有者 + Move 语义 + 借用检查",彻底消灭了悬垂指针和野指针。在 C++ 中,std::unique_ptr 最接近 Rust 的单一所有者模型,但 C++ 允许绕过 unique_ptr 直接操作裸指针,而 Rust 不允许——裸指针操作必须在 unsafe 块中进行,且 unsafe 块不会改变借用规则。
Go 的 GC 提供便捷的内存管理,但有"Stop the World"暂停风险和额外的运行时开销;Rust 的所有权系统在编译期完成内存管理,性能与 C/C++ 持平,但要求开发者显式处理所有权的转移和借用。Go 的 defer 提供了类似 RAII 的资源清理能力,但 defer 不能处理堆内存(交给 GC),而 Rust 的 Drop 自动处理所有实现了 Drop 的类型(堆内存、文件句柄、网络连接、锁等)。
Box<T> 将数据强制分配到堆上,并将这个堆数据的所有权包装为一个值——赋值时 Move 整个 Box,即转移堆数据指针的所有权,而非复制堆数据。Box<T> 实现了 Deref 和 Drop,让堆数据可以像栈数据一样用 * 解引用,并在超出作用域时自动 drop。它是 Rust 中"将堆数据作为值使用"的标准方式,与 C++ 的 std::unique_ptr 类似。
当函数需要"消耗"(consume)数据时用 Move(如 String 的 into_bytes());当函数只需要"读取"或"临时修改"数据时用借用(& 或 &mut)。经验法则:优先用不可变借用(&T),因为它最灵活(允许多个同时存在);只有需要修改数据时才用可变借用(&mut T);只有需要转移所有权时才用 Move。Rust 的类型系统通过 Deref trait 让 Box<T> 的借用和解引用操作完全透明。
Rust 的所有权系统在无运行时(No Stdlib / No Alloc)的嵌入式环境中完全可用——不需要 GC、不需要堆分配器,只需要编译器。这使得 Rust 成为第一个能系统编程(OS 内核、驱动、嵌入式固件)而不损失内存安全保证的语言。在 no_std 环境中,Rust 的所有权系统仍然有效,所有权检查在编译期完成,不需要任何运行时支持——这是 Rust 相比所有 GC 语言和大多数手动管理语言的独特优势。
全篇必记总纲
所有权是 Rust 内存安全体系的灵魂:单一所有者确保"谁负责清理",Move 语义确保"清理不会重复",借用规则确保"读写访问有秩序",Drop trait 确保"退出作用域即释放"——四条规则协同,在编译期彻底消灭了 GC 时代才能防住的内存安全问题,同时实现了与 C/C++ 相当的运行时性能。
所有权是 Rust 最核心的概念,是理解借用、生命周期和所有智能指针的基础。建议按以下顺序学习:
What 是什么 三规则、Move / Copy / Clone、借用与所有权的关系
→
Why 为什么 消灭 double free、悬垂指针、数据竞争的性能与安全收益
→
How 怎么做到的 Drop trait、借用检查器、生命周期、NLL 的实现机制
所有权(Ownership)是 Rust 独创的一套内存管理模型。在 Rust 中,每个值(value)都有一个"所有者"(owner),所有者是一个变量。当这个变量超出作用域时,Rust 会自动调用该值的 Drop trait,释放它所占用的资源——堆内存、文件句柄、网络连接等。这套机制完全在编译期实现,没有垃圾回收器的运行时开销。
三个核心术语
Rust 官方文档明确列出了所有权的三条规则,所有 Rust 编译器强制执行这三条规则:
// Rust 所有权三条规则(编译期强制执行)
// 规则一:每个值有一个所有者(每个值只能有一个变量"负责"它)
// 规则二:同一时刻每个值只有一个所有者(不能两个人同时"负责")
// 规则三:所有者超出作用域时,值被删除(内存自动释放)
fn main() {
// 规则一演示:s 是字符串的所有者
let s = String::from("hello");
// 规则二演示:Move 之后,s2 是新的唯一所有者
let s2 = s; // Move: s 将所有权转移给 s2
// 规则三演示:s2 超出作用域时,字符串被自动 drop
}
// main 函数结束时,所有本地变量按 FILO 顺序析构
Move 是 Rust 中最核心的语义之一。对于拥有堆上数据的类型(如 String、Vec<T>、Box<T>),赋值操作会触发 Move——原变量的内部指针被"移出",变为空(逻辑上相当于被设为 None),新变量继承堆数据的所有权。
所有没有实现 Copy trait的类型,在赋值、函数传参、函数返回值时都会 Move:
// 会 Move 的类型(堆数据或复杂资源管理)
String, Vec<T>, Box<T>, HashMap<K, V>, TcpStream, File, Mutex<T>, ...
// 不会 Move 的类型(实现了 Copy,在栈上按位复制)
i32, f64, bool, char, (i32, i32), [u8; 4], 所有固定大小的元组和数组
let s1 = String::from("hello");
// 内部:s1 是一个结构体 { ptr: 0x1234, len: 5, capacity: 5 }
// 堆地址 0x1234 上存储着 "hello" 这 5 个字节
let s2 = s1;
// Move: s1 的 ptr 被"移出",s2 的 ptr = 0x1234(同一个堆地址)
// s1 的 ptr 被设为 0x0(空指针),s1 不再拥有堆内存
// s1 仍然是有效的变量,但它的值是"空 String"(ptr=null, len=0)
// println!("{}", s1); // error: s1 的字符串已经被移走了
设计精髓:Rust 为什么用 Move 而不是像 C++ 那样默认浅拷贝?
Rust 认为"隐式复制你不知道大小的数据"是危险的——C++ 的默认拷贝对于 std::vector 这样的类型会导致高昂的深拷贝成本,开发者可能无意间写出 O(n) 的赋值操作。Rust 默认 Move,让开发者明确意识到"这个数据的所有权被转移了",如果真的需要复制,显式调用 .clone()——这是 Rust"零成本抽象"和"显式优于隐式"哲学的体现。
Copy 是 Rust 中一个特殊的 trait,标记"可以在赋值时按位复制的类型"。实现了 Copy 的类型不涉及堆内存管理,不会有双重释放风险——所以编译器允许它们在赋值时自动复制。
// Copy 类型示例:所有字段都是 Copy 的结构体
#[derive(Debug, Clone, Copy)]
struct Point { x: i32, y: i32 }
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy: p1 和 p2 各持有一个独立的 Point
println!("{:?}", p1); // OK: p1 仍然有效
Clone 是显式深拷贝,可以对任意类型进行完整的复制(如果是堆数据,则复制堆上的内容)。调用 .clone() 是开发者的有意识决策。
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝:堆上 "hello" 被复制一份
println!("{} {}", s1, s2); // 两个都有效
// Clone 是 Copy 的超集:能 Copy 的类型必定能 Clone
// 但反过来不一定成立(Clone 可能很贵,Copy 应该是零成本)
trait Copy: Clone { } // Copy 依赖 Clone,语义上 Copy 是"隐式 Clone"
// 标准库实现规范:
// Copy = "按位复制,零成本,无额外逻辑"
// Clone = "完整复制,可能有成本,需要显式调用"
| 类型 | 是否 Copy | 说明 |
|---|---|---|
| i32, i64, u32, f32, f64 | Copy | 固定大小的基本数值类型 |
| bool, char | Copy | 固定大小的单值类型 |
| [T; N](固定大小数组) | Copy(当 T 是 Copy 时) | 编译期已知大小的数组 |
| (T, U)(元组) | Copy(当 T 和 U 都是 Copy 时) | 元组字段递归判断 |
| String | Move(非 Copy) | 堆上数据,所有权转移 |
| Vec<T> | Move(非 Copy) | 堆上数据,所有权转移 |
| Box<T> | Move(非 Copy) | 堆指针,所有权转移 |
函数参数接收所有权,返回值交还所有权——这是 Rust 函数式编程风格的核心。
fn process(s: String) -> String {
// s 拥有字符串的所有权
let transformed = format!("processed: {}", s);
transformed // 所有权转移给调用者
}
fn main() {
let original = String::from("data");
let result = process(original); // original 的所有权转移给 process 的参数 s
// original 在这里不再可用
println!("{}", result); // OK: result 持有字符串所有权
}
思考记忆:所有权的"入"与"出"
在 C/C++ 中,当两个指针指向同一块堆内存,手动释放两次会导致堆元数据损坏,引发崩溃或安全漏洞。Rust 的 Move 语义彻底消除了这个问题——赋值操作转移所有权,而不是复制指针。
// C 语言:double free 危险
char* s1 = (char*)malloc(6);
strcpy(s1, "hello");
char* s2 = s1; // 浅拷贝:s1 和 s2 指向同一块内存
free(s1); // 第一次释放:内存归还系统
free(s2); // 第二次释放:危险!未定义行为,可能被利用为安全漏洞
// Rust:Move 语义天然防止双重释放
let s1 = String::from("hello");
let s2 = s1; // Move: s1 的所有权转移给 s2,s1 的内部指针被清空
// Rust 保证:同一时刻只有一个所有者
// 当 s1 和 s2 超出作用域时,各自只释放自己持有的数据
在 C/C++ 中,返回函数内局部变量的指针会导致悬垂指针(指向已释放的栈帧)。Rust 的生命周期系统确保返回引用时,引用不能指向局部变量(局部变量在函数退出时被 drop,返回引用将成为悬垂引用)。
// C 语言:返回栈变量的地址(危险)
int* create_int() {
int x = 42;
return &x; // 危险!x 在函数返回时被弹出栈,返回的指针悬垂
}
// Rust:生命周期注解保证引用不指向已销毁的数据
// 错误:编译期直接拒绝
fn dangle() -> &String { // error: missing lifetime specifier
let s = String::from("hello");
&s // s 在函数末尾被 drop,返回的引用悬垂!
}
// 正确:返回所有权而非引用
fn no_dangle() -> String {
let s = String::from("hello");
s // s 的所有权被转移给调用者,s 不会被 drop
}
数据竞争发生在多个线程同时读写同一块内存时,且至少有一个是写操作。Rust 的借用规则(同一时刻:多个不可变引用 OR 一个可变引用)从语言层面消除了数据竞争。
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;
fn main() {
// Arc<T>: 原子引用计数,允许多线程共享所有权
// Mutex<T>: 互斥锁,保证同一时刻只有一个线程能修改数据
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles { handle.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 总是 10
}
注意:Arc<Mutex<T>> 的工作原理
Arc(原子引用计数)允许多个线程持有同一个值的所有权副本,引用计数保证最后一个持有者负责 drop。Mutex(互斥锁)包装内部数据,只有成功获取锁的线程才能修改数据——这正是 Rust 用编译期检查(Send/Sync)+ 运行时锁实现的"安全并发"。
在 C++ 中,遍历 vector 时如果修改了 vector,迭代器会失效(iterator invalidation)。Rust 的借用规则保证:修改 vector 期间不能持有 vector 的迭代器,编译期就阻止了这类错误。
let mut v = vec![1, 2, 3];
for val in &v { // 不可变借用开始
// v.push(4); // error: 无法在持有不可变借用时创建可变借用
println!("{}", val);
} // 不可变借用结束
v.push(4); // OK: 迭代器已经结束,现在可以修改
| 语言 | 内存管理方案 | 双重释放防护 | 悬垂指针防护 | 数据竞争防护 | 性能开销 |
|---|---|---|---|---|---|
| Rust | 所有权系统(编译期分析) | Move 语义,编译期保证 | 生命周期注解,编译期保证 | 借用规则,编译期保证 | 零运行时开销 |
| C | 手动(malloc/free) | 无(程序员责任) | 无 | 无 | 最低 |
| C++ | RAII + 智能指针(手动选择) | unique_ptr 自动防护 | 无(裸指针无防护) | 无(需 std::mutex) | 接近零 |
| Python | GC(引用计数 + 标记清除) | GC 保护 | GC 保护 | GIL 限制 | GC 暂停开销 |
| Go | GC(并发三色标记) | GC 保护 | GC 保护 | channel 约定(无编译期保证) | GC 暂停开销 |
| Java | GC(分代收集) | GC 保护 | GC 保护(无指针) | 无(需 synchronized) | GC 暂停开销 |
| JS | GC(标记清除) | GC 保护 | GC 保护(无指针) | Event Loop 单线程 | GC 暂停开销 |
对比记忆口诀
当值超出作用域时,Rust 自动调用 Drop::drop(&mut self) 方法。这使得资源释放是确定性的——与 C++ RAII 完全相同,但 Rust 的 Drop 比 C++ 更安全(不需要手动管理)。
struct HasDrop {
data: String,
}
impl Drop for HasDrop {
fn drop(&mut self) {
println!("Dropping: {}", self.data);
}
}
fn main() {
let a = HasDrop { data: String::from("first") };
{
let b = HasDrop { data: String::from("second") };
println!("内层块结束,b 将被 drop");
} // b.drop() 被调用
println!("a 仍然存活");
} // a.drop() 被调用(FILO 顺序)
Drop 的析构顺序:变量按照与构造顺序相反的顺序被 drop(FILO,First In Last Out)。栈上的局部变量从最后一个声明的变量开始析构。内层块的变量先于外层块被 drop。
借用检查器(Borrow Checker)是 Rust 编译器(rustc)的一个组件,它在编译期追踪每个引用的活跃区间(Live Range),验证借用规则不被违反。
Rust 2018 引入 NLL 后,借用的作用域不再是大括号边界,而是"最后一次使用"的位置。这大大减少了"假性冲突"——借用检查器的行为更符合开发者的直觉。
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可变借用:first 的借用作用域到 println! 为止
println!("{}", first); // 最后使用 first
v.push(4); // NLL: first 的借用已结束,可变借用 OK!
// Rust 2018 之前:first 的借用作用域到函数末尾,这里 push 会编译失败
// Rust 所有权转移的几种方式
// 方式一:赋值 Move
let s1 = String::from("hello");
let s2 = s1; // Move: s1 -> s2,s1 失效
// 方式二:函数参数 Move
fn consume(s: String) { } // 参数 s 获得所有权
consume(s2); // s2 的所有权转移给 consume
// 方式三:函数返回值恢复所有权
fn produce() -> String { String::from("world") }
let s3 = produce(); // produce 返回值的所有权转移给 s3
// 方式四:Clone(显式深拷贝,不转移所有权)
let s4 = String::from("clone");
let s5 = s4.clone(); // s4 仍然有效,s5 是新的独立副本
// 方式五:Copy(栈上类型,不涉及 Move)
let a = 42;
let b = a; // Copy: a 和 b 都有效,都是独立的 i32
智能指针是实现了 Deref 和 Drop 的类型,它们以"指针"的形式包装数据,同时承担资源管理的职责。
// Box<T> 将数据分配到堆上,整个 Box 作为值在栈上传递
let b1 = Box::new(String::from("heap data"));
let b2 = b1; // Move: b1 的所有权转移给 b2,堆数据不复制
// b1 不再可用
println!("{}", b2); // OK
// Box<T> 实现了 Deref,可以像普通引用一样使用
fn print_box(s: &str) { println!("{}", s); }
print_box(&b2); // 自动解引用:&Box<String> -> &String -> &str
use std::rc::Rc;
let s1 = Rc::new(String::from("shared"));
let s2 = Rc::clone(&s1); // 引用计数 +1,s1 和 s2 共享所有权
let s3 = Rc::clone(&s1); // 引用计数 +1
println!("引用计数: {}", Rc::strong_count(&s1)); // 3
// Rc<T> 只能进行不可变借用(&T),不能可变借用(&mut T)
// 因为 Rc<T> 内部是非线程安全的引用计数
// 如果需要可变借用,用 Rc<RefCell<T>>
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data); // 原子引用计数 +1
let handle = thread::spawn(move || {
println!("子线程: {:?}", data2);
});
println!("主线程: {:?}", data);
handle.join().unwrap();
// Arc 的引用计数在所有线程结束后减为 0,vec 被 drop
Rust 支持用 let 解构来"拆分"一个值,同时转移或复制其字段的所有权:
let (x, y) = (String::from("hello"), 42);
// x 是 String,Move 语义:x 被绑定到第一个元素
// y 是 i32,Copy 语义:y 是第二个元素的按位复制
// 结构体解构
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
let Point { x, y } = p; // x 和 y 各获得一个字段(x: Copy, y: Copy)
println!("{} {}", x, y);
// C:手动内存管理 + 悬垂指针风险
char* create_string() {
char* s = (char*)malloc(6);
strcpy(s, "hello");
return s; // OK(堆上分配),但忘记 free 就内存泄漏
}
// C++:RAII,但默认浅拷贝导致双重释放风险
std::string s1 = "hello";
std::string s2 = s1; // 默认拷贝构造函数,深拷贝(安全但昂贵)
// unique_ptr 的 move 语义最接近 Rust:
std::unique_ptr<std::string> p1 = std::make_unique<std::string>("hello");
std::unique_ptr<std::string> p2 = std::move(p1); // Move: p1 失效,p2 持有资源
// p1 变成空 unique_ptr,不会 delete 任何东西
C++ std::unique_ptr vs Rust 单一所有者
std::unique_ptr 是 C++11 引入的最接近 Rust 所有权的概念——两者都实现了"独占所有权 + Move 语义 + 自动析构"。但关键区别是:C++ 的 unique_ptr 可以通过 .release() 放弃所有权并返回裸指针(危险操作),而 Rust 的所有权转移是单向的、更安全的。
# Python:所有东西都是引用,GC 自动处理
s1 = "hello"
s2 = s1 # 两个变量引用同一个字符串对象
s2 = "world" # s2 现在引用新字符串,s1 仍然引用 "hello"
# GC 追踪引用计数,最后一个引用消失时触发析构
# 问题:循环引用(a -> b -> a)导致内存泄漏(Python 用标记清除处理)
class Node:
def __init__(self, val):
self.val = val
self.next = None
a = Node(1)
b = Node(2)
a.next = b # a 持有 b
b.next = a # b 持有 a —— 循环引用,引用计数无法回收
# Python 用分代 GC 标记清除来处理循环引用
// Rust:所有权系统 + 借用规则,循环引用被编译期拒绝
struct Node { val: i32, next: Option<Box<Node>> }
fn main() {
let a = Box::new(Node { val: 1, next: None });
let b = Box::new(Node { val: 2, next: None });
a.next = Some(b); // a 拥有 b
// b.next = Some(a); // error: 不能在 a 已经拥有 b 时让 b 拥有 a(循环所有权)
// 编译器检查:A 拥有 B 意味着 A 不能同时被 B 拥有(会形成循环)
}
Rust 如何处理需要循环引用的场景?
如果业务逻辑确实需要循环引用(如链表、树),Rust 提供了两种方案:Rc<T>(弱引用计数,允许循环)+ Weak<T>(弱引用,不参与引用计数)。Rc<T> 配合循环引用会引发内存泄漏(因为引用计数永远不会到 0),但这比 GC 语言中的循环引用更容易被检测和修复。
// Go:GC 自动回收,无需所有权概念
func consume(s []byte) {
// s 是切片(引用类型),底层指向数组
// GC 追踪 s 引用的数组,函数结束后如果无其他引用则回收
}
func main() {
s := []byte{'h','e','l','l','o'}
consume(s)
// s 仍然有效(Go 的切片是引用传递,不会 Move)
}
// Rust:所有权是显式的,借用必须显式声明
fn consume(s: &[u8]) { // 借用切片(不转移所有权)
println!("{:?}", s);
}
fn main() {
let s = vec![1, 2, 3, 4, 5];
consume(&s); // 显式借用
// s 仍然有效(所有权未转移)
}
// Java:一切皆引用(除基本类型),GC 管理生命周期
public class Demo {
static void process(StringBuilder sb) {
sb.append(" processed"); // 直接修改引用的对象
// sb 超出作用域,但引用的堆对象不会被 GC 回收
// (main 函数中还有引用)
}
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
process(sb);
System.out.println(sb.toString()); // "hello processed"
}
}
// Java 的引用是共享的,没有"独占所有权"的概念
// 多个变量可以指向同一个对象,没有任何编译期检查
// Rust:借用是唯一安全的共享方式,且有独占检查
fn process(sb: &mut String) {
sb.push_str(" processed");
}
fn main() {
let mut sb = String::from("hello");
process(&mut sb); // 可变借用
println!("{}", sb); // "hello processed"
// Rust 的借用检查器保证同一时刻只有一个可变借用
// 编译期就阻止了多线程数据竞争
}
我的理解:Rust 所有权 vs 其他语言的"共享"模型
Java/JS/Python 的"共享"模型是"任何地方都可以持有指向同一对象的引用"——方便但危险(无编译期检查)。Rust 的"共享"模型是"要么独占(Move/Box),要么借用(&/&mut),要么计数共享(Rc/Arc)"。三条路泾渭分明,每条路都有编译期安全保证。
本专题核心要点
| 维度 | Rust 特色 | 其他语言对比 |
|---|---|---|
| 内存释放时机 | 作用域末尾确定析构(RAII) | GC 语言不确定(异步回收);C 手动 |
| 堆数据赋值 | 默认 Move(单一所有者) | C++ 默认拷贝;JS/Go 引用共享 |
| 所有权转移 | Move 语义(编译期验证) | C++ std::move(手动);Java/JS 无此概念 |
| 共享数据 | 借用(&/&mut)或计数共享(Rc/Arc) | JS/Python 无限制共享(GC 保护);Go 无限制指针共享 |
| 循环引用 | 编译器拒绝(除非用 Rc + Weak) | GC 语言可以(标记清除处理) |
| 数据竞争防护 | 借用规则 + Send/Sync trait | 所有语言依赖运行时锁(无编译期检查) |
学完本文后,下一步学什么?
作者:左扬 | 发表于 2026 年 | 水平有限,欢迎指正
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。