一个值只能被一个变量所拥有,这个变量被称为所有者(Each value in Rust has a
variable that’s called its owner)。
一个值同一时刻只能有一个所有者(There can only be one owner at a time),也就
是说不能有两个变量拥有相同的值。所以对应刚才说的变量赋值、参数传递、函数返回
等行为,旧的所有者会把值的所有权转移给新的所有者,以便保证单一所有者的约束。
所有者离开作用域,其拥有的值被丢弃(When the owner goes out of scope, the value will be dropped) ,内存得到释放。
核心就是保证单一所有权。其中第二条规则讲的所有权转移是
Move 语义,Rust 从 C++ 那里学习和借鉴了这个概念。
第三条规则中的作用域(scope)是一个新概念,我简单说明一下,它指一个代码块 (block),在 Rust 中,一对花括号括起来的代码区就是一个作用域。举个例子,如果一 个变量被定义在 if {} 内,那么 if 语句结束,这个变量的作用域就结束了,其值会被丢弃; 同样的,函数里定义的变量,在离开函数时会被丢弃。
fn main() {
let data = vec![1, 2, 3, 4];
let data1 = data;
println!("sum of data1: {}", sum(data1));
println!("data1: {:?}", data1); // error1
println!("sum of data: {}", sum(data)); // error2
}
fn sum(data: Vec<u32>) -> u32 {
data.iter().fold(0, |acc, x| acc + x)
}
如果我们要在把 data1 传给 sum(),同时,还想让 main() 能够访问 data,该怎么办?
我们可以调用 data.clone() 把 data 复制一份出来给 data1,这样,在堆上就有 vec![1,2,3,4] 两个互补影响且可以独立释放的副本
1. 如果你不希望值的所有权被转移,在 Move 语义外,Rust 提供了 Copy 语义。
如果一个数据结构实现了 Copy trait,那么它就会使用 Copy 语义。这样,在你赋值或者传 参时,值会自动按位拷贝(浅拷贝)。
当你要移动一个值,如果值的类型实现了 Copy trait,就会自动使用 Copy 语 义进行拷贝,否则使用 Move 语义进行移动
* `原生类型,包括函数、不可变引用和裸指针实现了 Copy;`
* `数组和元组,如果其内部的数据结构实现了 Copy,那么它们也实现了 Copy;`
* `可变引用没有实现 Copy;`
* `非固定大小的数据结构,没有实现Copy;`
Copy (整数/栈) —— 影分身之术
对于栈上的简单数据(i32),它们极其便宜,可以无限复制。
-
场景:你有一张电子发票的 PDF(
a = 10)。 -
所有权:你拥有这个文件(
a)。 -
赋值 (
let b = a) :-
动作:你复制了一份完全一样的 PDF 发给了
b。 -
结果:
a依然拥有他那份 PDF(所有权还在)。b拥有了他那份新的 PDF(b获得了新数据的所有权)。
-
旧主人:
a依然活着,依然管着自己的文件。 -
收尸:
- 当
a离开作用域,a的 PDF 被销毁。 - 当
b离开作用域,b的 PDF 被销毁。 - 互不干扰。
- 当
-
小结:
所有权:一个值只能被一个变量所拥有,且同一时刻只能有一个所有者,当所有者离开 作用域,其拥有的值被丢弃,内存得到释放。
Move 语义:赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的 变量就不能访问。
Copy 语义:如果值实现了 Copy trait,那么赋值或传参会使用 Copy 语义,相应的 会被按位拷贝(浅拷贝),产生新的值。
2. 如果你不希望值的所有权被转移,又无法使用 Copy 语义,那你可以“借用”数据
Borrow 语义通过引用语法(& 或者 &mut)来实现
Rust 的借用(&)都是只读的,就好像住酒店,退房时要完好无损。但有些情
况下,我们也需要可变的借用(&mut),就像租房,可以对房屋进行必要的装饰,这一点待会详细
讲。
以 Java 为例,给函数传一个整数,这是传值,和 Rust 里的 Copy 语义一致;而给函数传
一个对象,或者任何堆上的数据结构,Java 都会自动隐式地传引用.
Rust 没有传引用的概念,Rust 所有的参数传递都是传值,不管是 Copy 还是 Move。
所以在 Rust 中,你必须显式地把某个数据的引用,传给另一个函数
Rust 的引用实现了 Copy trait,所以按照 Copy 语义,这个引用会被复制一份交给要调用
的函数。对这个函数来说,它并不拥有数据本身,数据只是临时借给它使用,所有权还在 原来的拥有者那里。
当你说“借用 (Borrowing) ”时,对于所有权 (Ownership) 只有一句话结论:
所有权完全不动,但是所有者被“冻结”了。
简单来说,借用就是在不触发 Move(不过户) 且 不触发 Copy(不复印) 的情况下,给别人一张临时的访问证。
我们分三个视角来看这个“所有权”的状态:
1. 法律视角(所有权归属)
在借用期间:
- 所有权 (Ownership) :依然稳稳地在原主 (
s1) 手里。 - 借用者 (
r1) :手中只有使用权(读或写),绝无处分权(不能卖、不能销毁)。
类比:你有一本书(
s1)。
- Move:你把书送给张三了。(书归张三,你手里空了)
- 借用:你把书借给李四看两天。书名义上还是你的,李四只有阅读权。
2. 行为视角(所有者的“冻结期”)
这是 Rust 与 C++ 最不一样的地方。在 C++ 里,你把指针给别人后,你依然可以把内存释放掉(导致别人的指针变成悬垂指针)。
但在 Rust 里,一旦你把数据“借”出去了,原主 (s1) 的能力就会被暂时封印(Frozen) 。
看看这个“封印”的过程:
Rust
fn main() {
let s1 = String::from("Hello"); // s1 是所有者
// 1. 开始借用
let r1 = &s1;
// 此时 s1 依然拥有所有权,但是!
// s1 被“冻结”了。
// ❌ 动作 A:试图 Move(过户)
// let s2 = s1;
// 报错!因为 s1 正在被 r1 借用。你不能把正在被别人用的书卖掉。
// ❌ 动作 B:试图修改(如果 r1 是不可变借用)
// s1.push_str(" World");
// 报错!既然你承诺让 r1 看“原版”,你就不能悄悄修改内容。
println!("r1 is: {}", r1); // r1 在这里使用
// 2. 借用结束
// r1 离开作用域(或者最后一次使用结束)
// ✅ 动作 C:解冻
// 现在 r1 不用书了,s1 恢复自由身。
let s2 = s1; // 现在可以 Move 了
}
怎么看借用时的所有权? 就像你去住酒店。
- 酒店老板(所有者
s1) :拥有房产证。 - 你(借用者
r1) :拥有房卡。 - 关键点:当你住在里面的时候,酒店老板不能把这间房卖了,也不能突然冲进来装修。他的所有权被你的“租约”给锁定了。
3. 内存视角(生命周期约束)
借用的核心规则是:借用者的寿命,绝不能长于所有者的寿命。
这就是传说中的 借用检查器 (Borrow Checker) 每天盯着的事情。
Rust
{
let r;
{
let s = String::from("hello"); // s 出生,持有堆内存
r = &s; // r 借用 s
} // <--- s 在这里死了!堆内存被释放(Drop)。
// println!("{}", r);
// ❌ 报错!如果你在这里用 r,这就叫“悬垂指针”。
// Rust 编译器会说:s 已经带着所有权入土了,r 这张借条作废。
}
总结:借用时的所有权图谱
假设 let r = &s;
-
对于
s(Owner) :- 地位:我是老大,地契在我手里。
- 状态:但我现在被套牢了。只要
r还没死(还没用完),我就不能跑(Move),也不能死(Drop),也不能改(Mutate)。 - 责任:等
r走了,我还要负责最后的清理工作。
-
对于
r(Borrower) :- 地位:我是过客。
- 状态:我拿着指向
s的指针。 - 限制:我必须在
s死之前死掉。
一句话回答你的问题: 借用就是所有权不动,但在借用存续期间,强行剥夺了所有者“销毁”和“转移”数据的权利,直到借用归还。
线索:当我们data被引用到函数中,该函数完事,一旦 data 离开了作用域被释放,如果还有引用指向 data,岂不是造成我们想极力避免的使用已释放内存(use after free)这样的内存安全问题?怎么办呢?
这就引出了借用的生命周期及其约束
所以,我们对值的引用也要有约束,这个约束是:借用不能超过(outlive)值的生存期。
fn main() {
let r = local_ref();
println!("r: {:p}", r);
}
fn local_ref<'a>() -> &'a i32 {
let a = 42;
&a
}
生命周期更长的 main() 函数变量 r ,引用了生命周期更短的 local_ref() 函数里的
局部变量,这违背了有关引用的约束,所以 Rust 不允许这样的代码编译通过
引用了生命周期更短的值
fn main() {
let mut data: Vec<&u32> = Vec::new();
let v = 42;
data.push(&v);
println!("data: {:?}", data);
}
编译通过
堆内存里引用栈内存,但生命周期是相
同的
fn main() {
let mut data: Vec<&u32> = Vec::new();
push_local_ref(&mut data);
println!("data: {:?}", data);
}
fn push_local_ref(data: &mut Vec<&u32>) {
let v = 42;
data.push(&v);
}
引用函数,短生命周期,编译失败
线索:可变引用
fn main() {
let mut data = vec![1, 2, 3];
for item in data.iter_mut() {
data.push(*item + 1);
}
}
解决办法
1. 第一个可变引用:for 循环
在第 4 行 for item in data.iter_mut():
data.iter_mut()方法会可变地借用 (mutably borrow)data。- 这个“借用”会持续到整个
for循环结束(即第 6 行的大括号}之后)。 - 为什么?因为
iter_mut()返回的迭代器需要持续“锁定”data,以确保它可以安全地逐个给出data内部元素的可变引用(即item,它的类型是&mut i32)。
简单来说:从第 4 行开始,data 已经被 for 循环“可变地借用”了。
2. 第二个可变引用:data.push()
在第 5 行 data.push(*item + 1):
data.push()方法也需要一个可变引用 (&mut self) 才能向data中添加元素。- 此时,Rust 编译器发现你正试图在
for循环(它已经持有data的一个可变引用)的内部,再次获取data的第二个可变引用。
如何修复这个问题?
你不能在迭代一个集合的同时修改该集合的结构(比如增删元素)。正确的做法是:先收集,后修改。
如果你想达到的效果是 [1, 2, 3, 2, 3, 4],你可以这样做:
Rust
fn main() {
let mut data = vec![1, 2, 3];
// 1. 先收集所有需要添加的新元素
// 这里我们使用 .iter() (不可变迭代),因为我们不需要修改现有元素
let mut items_to_add = Vec::new();
for item in data.iter() {
items_to_add.push(*item + 1);
}
// 2. 等到上一个循环(和对 data 的不可变借用)结束后,
// 我们现在可以安全地获取可变引用并添加所有新元素
data.extend(items_to_add);
println!("{:?}", data); // 输出: [1, 2, 3, 2, 3, 4]
}
n main() {
let mut data = vec![1, 2, 3];
let data1 = vec![&data[0]];
println!("data[0]: {:p}", &data[0]);
for i in 0..100 {
data.push(i);
}
println!("data[0]: {:p}", &data[0]);
println!("boxed: {:p}", &data1);
}
这里有内存不安全的潜在操作:如果继续添加元素,堆上的数 据预留的空间不够了,就会重新分配一片足够大的内存,把之前的值拷过来,然后释放旧 的内存。这样就会让 data1 中保存的 &data[0] 引用失效,导致内存安全问题
在一个作用域内,仅允许一个活跃的可变引用。所谓活跃,就是真正被使用来修改数据的可变引用,如果只是定义了,却没有使用或者当作只读引用使用,不算活跃。
在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在
意思就是同时的状态,只要有一个人在编辑,可变,那就只能有这么一个可变,连只读的都不能有, 第二个 同时的可以有多个只读的
-
独占写入(编辑模式):
- 只要有一个“编辑者”(可变引用
&mut T)存在。 - 那么此时,不能有任何其他人,无论是“查看者”(
&T)还是其他“编辑者” (&mut T)。 - 就像你在编辑文档时,不希望有人在你背后同时查看或同时编辑,以免造成混乱。
- 只要有一个“编辑者”(可变引用
-
共享读取(只读模式):
- 你可以同时有很多个“查看者”(
&T)。 - 但此时,不能有任何“编辑者”(
&mut T)。 - 只要文档的内容是“只读”的,那么多少人同时看都没关系,因为内容不会变。
- 你可以同时有很多个“查看者”(
这两个状态在同一时间是互斥的。你要么处于“共享读取”模式,要么处于“独占写入”模式,不能同时都占。
不可变借用 (&) 是“把你冻结,只许看不许改”; 那么 可变借用 (&mut) 就是 “把你关进小黑屋,外面的人(包括你自己)既不能看也不能改,只有持有钥匙的那唯一一个人能动。”
可变借用(Mutable Borrowing)在 Rust 里的学术名称叫 独占引用 (Exclusive Reference) 。
这也是所有权体系中最霸道的一条规则。
1. 此时的所有权状态:完全封锁
当你写下 let r = &mut s; 时:
-
所有权 (Ownership) :依然归原主 (
s) 所有。 -
原主 (
s) 的状态:完全瘫痪。- 在不可变借用中,原主
s起码还能被“读取”。 - 在可变借用中,原主
s连被读取的资格都没有了。
- 在不可变借用中,原主
代码演示(惨烈的现场):
Rust
fn main() {
let mut s = String::from("Hello");
// --- 借用开始 ---
let r = &mut s; // r 拿到了唯一的“施工许可证”
// ❌ 1. 原主想读?不行!
// println!("s is: {}", s);
// 报错:s 已经被借出去了,而且是可变借用。此时访问 s 可能会读到被改了一半的脏数据。
// ❌ 2. 别人想借(哪怕是只读)?不行!
// let r2 = &s;
// 报错:此时已经有“写锁”了,不能再有“读锁”。
// ✅ 3. 只有 r 能操作
r.push_str(" World"); // r 在修改数据
println!("r is: {}", r); // r 在读取数据
// --- 借用结束 ---
} // r 离开作用域
// ✅ 4. 现在 r 死了,s 终于刑满释放
println!("s is: {}", s); // 正常打印 "Hello World"
2. 为什么这么霸道?(数据竞争 Data Race)
作为 C++ 开发者,你一定知道多线程最怕什么:Data Race(数据竞争) 。 Rust 的伟大之处在于,它在单线程里也强行消灭了数据竞争的可能性。
假设 Rust 允许 s (原主) 和 r (可变引用) 同时访问:
r正在做r.push_str(...)。这可能会导致String的堆内存扩容(Reallocate)。- 扩容意味着:旧的堆内存被释放,数据搬到了新的更大的内存地址。
- 如果此时
s还能访问(或者有一个r2指向s),它手里拿的那个旧指针(Old Pointer)就指向了无效内存。 - Boom! 悬垂指针 / 读取垃圾数据。
所以 Rust 规定:只要有一个人在改,其他所有人(包括主人)都必须闭眼、把手拿开。
3. 借用的“金科玉律”
这是 Rust 借用检查器(Borrow Checker)的核心逻辑,请背诵:
在任意给定时间,对于同一个数据,你只能拥有以下二者之一(互斥):
-
任意数量 的 不可变引用 (
&T) —— (大家都能看,没人能改)- 类比:图书馆阅览室。一百个人可以同时看同一本书,但谁也不能拿笔涂改。
-
且仅有一个 可变引用 (
&mut T) —— (只有一人能改,其他人不能看)- 类比:外科手术室。只能有一个主刀医生在动手术,此时闲杂人等(包括病人家属)严禁入内观看。
4.“解引用”
对于可变借用,解引用 (*) 是修改值的必要手段。
Rust
fn main() {
let mut x = 100;
let r = &mut x; // r 是指针
// r = 200; // ❌ 错!你不能把整数赋值给指针变量。
*r = 200; // ✅ 对!顺着 r 找到 x 的内存,把里面的值换成 200。
}
总结:一张图对比原主 s 的处境
假设 s 是房子的主人。
| 场景 | 代码 | 原主 s 的权利 (Status) | 别人的权利 | s状态描述 |
|---|---|---|---|---|
| 自由身 | let mut s = ... | 读 ✅ 写 ✅ 移交 ✅ | 无 | 完全自由 |
| 不可变借用 | let r = &s; | 读 ✅ 写 ❌ 移交 ❌ | r 可以读 | 被冻结 (Frozen) 。 看着自己的房子,但不能装修,也不能卖。 |
| 可变借用 | let r = &mut s; | 读 ❌ (完全瞎了) 写 ❌ 移交 ❌ | r 可以读 + 写 | 被隔离 (Shadowed) 。 房子正在搞秘密装修,主人也被赶出去了,直到装修队走人。 |
线索: 进入动态运行时检查
编译时,处理大部分使用场景,保证安全性和效率;运行时,处理无法在编译时处理的场景,会牺牲一部分效率,提高灵活性。后续讲到静态分发和动态分发也会有体现
使用引用计数的智能指针:Rc(Reference counter) 和 Arc(Atomicreference counter)
场景
-
一个有向无环图(DAG)中,某个节点可能有两个以上的节点指向它,这个按照所有权 模型怎么表述?
-
多个线程要访问同一块共享内存,怎么办?
Rc
对某个数据结构 T,我们可以创建引用计数 Rc,使其有多个所有者。Rc 会把对应的数据结构创建在堆上,堆是唯一可以让动态创建的数据被到处使用的内存.
对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数同样的而当一个
Rc 结构离开作用域被 drop() 时,也只会减少其引用计数,直到引用计数为零,才会真正清除对应的内存。
fn clone(&self) - >Rc<T>{
//增加引用计数
self.inner().inc_strong();
//通过self.ptr生成一个新的Rc结构
Self::from_inner(self.ptr)
}
Rc 的 clone() 正如我们刚才说的,不复制实际的数据,只是一个引用计数的增加
而当一个Rc 结构离开作用域被 drop() 时,也只会减少其引用计数,直到引用计数为零,才会真正
清除对应的内存。
Rc 是一个只读的引用计数器
use std::rc::Rc
fn main(){
let a = Rc::new(1);
let b = a.clone();
let c = a.clone();
}
上面的代码我们创建了三个 Rc,分别是 a、b 和 c。它们共同指向堆上相同的数据,也就 是说,堆上的数据有了三个共享的所有者。在这段代码结束时,c 先 drop,引用计数变成 2,然后 b drop、a drop,引用计数归零,堆上内存被释放。
线索:你可能继续会疑惑:Rc 是怎么产生在堆上的?并且为什么这段堆内存不受栈内存生命周期的控制呢?
Box::leak() 机制
Rust提供的方式是 Box::leak()创建不受栈内存控制的堆内存,而绕过编译时的所有权规则,
Box 是 Rust 下的智能指针,它可以强制把任何数据结构创建在堆上,然后在栈上放一个指 针指向这个数据结构,但此时堆内存的生命周期仍然是受控的,跟栈上的指针一致。
Box::leak(),顾名思义,它创建的对象,从堆内存上泄漏出去,不受栈内存控制,是一个
自由的、生命周期可以大到和整个进程的生命周期一致的对象
当我们要改变用Rc创建的节点时,无法完成,因为Rc 是一个只读的引用计数器,所以就引出了RefCell
和 Rc 类似,RefCell 也绕过了 Rust 编译器的静态检查,允许我们在运行时,对某个只读 数据进行可变借用。这就涉及 Rust 另一个比较独特且有点难懂的概念:内部可变性
概念:内部可变形
当我们用 let mut 显式地声明一个可变的值,或者,用 &mut 声明一个可变引用时,编
译器可以在编译时进行严格地检查,保证只有可变的值或者可变的引用,才能修改值内部
的数据,这被称作外部可变性(exterior mutability),外部可变性通过 mut 关键字声 明。
在编译器的眼里,值是只读的,但是在运行时,
这个值可以得到可变借用,从而修改内部的数据,这就是 RefCell 的用武之地。
Rc 和 RefCell它们都只用于单线程环境
简单来说:
Rc<T>:实现共享所有权(Multiple Owners)。允许多个“所有者”指向堆上的同一个数据,但它只提供不可变访问。RefCell<T>:实现内部可变性(Interior Mutability)。允许你在持有不可变引用时,也能修改内部的数据,但它不允许多所有者。
当你需要 “既能共享所有权,又能修改数据” 时,就会使用它们的组合:Rc<RefCell<T>>。
线索:既然上面只能解决单线程,那么多线程我们可以用Arc 和 Mutex/RwLock
在基础 Rust 中,所有权是“独裁制”(一人拥有);而在 Rc/Arc 和 RefCell/Mutex 的世界里,所有权变成了 “股份制” 和 “托管制” 。
为了让你彻底理解,我们必须把这些类型看作是包裹数据的 “洋葱” 结构。每一层对“所有权”的处理方式都不同。
我们以最经典的组合 Arc<Mutex<T>> 为例,拆开来看。
1. 外层:Rc 和 Arc —— “股份制”所有权 (Shared Ownership)
核心理解:所有权被“虚化”了。
在普通 Rust 中,let a = s 意味着 s 死了,a 活了(独占)。 但在 Arc 中,let b = a.clone() 意味着 a 和 b同时拥有 这块数据。
-
所有权归谁?
- 表面上:
a和b都是老板。 - 实质上:它们都只是 “持股人” 。
- 真正的数据(T)以及计数器,存在于堆上的一个公共区域。
- 表面上:
-
生死大权(Drop) :
- 普通的 Move:主人一死,数据立刻陪葬。
- Arc/Rc 的 Move:只是转让了“股份”。
- 真正的销毁:当最后一个持股人离开(计数器归零)时,所有权才真正失效,数据才会被清理。
形象类比:联名银行账户 你和你老婆都有银行卡(
Arc指针)。 钱(数据)在金库里。 只有当你们俩都销户了(Drop),银行才会注销这个账户。 只要还有一个人拿着卡,账户就必须活着。
2. 内层:RefCell 和 Mutex/RwLock —— “托管制”所有权 (Custody)
核心理解:所有权被“关起来”了。
这些类型本身拥有内部数据 T 的绝对所有权。 但是,它们存在的目的,不是为了让你把 T 取走(Move out),而是为了给你提供 “临时的、受控的借用” 。
-
所有权归谁?
Mutex<T>拥有T。T就在Mutex的肚子里。- 如果你销毁了
Mutex,里面的T也随之销毁。
-
你能得到什么?
-
你永远无法直接拿到内部的
T的所有权(除非你把 Mutex 砸了,用into_inner())。 -
你只能申请到一个 “看守/卫兵” (Guard) 。
RefCell给你的叫Ref或RefMut。Mutex给你的叫MutexGuard。
-
-
神奇的 Guard:
- 这个 Guard 表现得像一个引用(通过
Deref)。 - 但它不仅仅是引用,它还是锁的钥匙。
- 当 Guard 离开作用域(Drop)时,它不仅不仅销毁自己,还负责把锁还回去(释放锁)。
- 这个 Guard 表现得像一个引用(通过
形象类比:超市储物柜
T是你的包。Mutex是那个储物柜。包的所有权暂时归柜子管。lock()动作是刷条码。MutexGuard是那个打开的柜门。 只要柜门开着(Guard 活着),你就能操作里面的包。 你关上门走人(Guard Drop),柜子就自动上锁,等待下一个人。
3. 组合拳:Arc<Mutex<T>> —— 层层分包
现在我们把它们套在一起,看看所有权是如何流动的。
Rust
// 一个存在堆上的整数
let data = Arc::new(Mutex::new(0));
// 线程 A 克隆了 Arc
let thread_a_ref = data.clone();
此时的所有权状态:
-
最外层 (
Arc) :data和thread_a_ref共同持有(Share)那个Mutex的所有权。- 只要还有一个线程拿着
Arc,Mutex就不会被销毁。
-
中间层 (
Mutex) :Mutex独占持有内部i32 (0)的所有权。- 它负责保证同一时间只有一个人能摸到这个
0。
-
当你操作时 (
lock) :Rust
{ let mut guard = thread_a_ref.lock().unwrap(); *guard += 1; } // guard 离开作用域lock():你向Mutex申请访问权。guard:你拿到了一个 “借用凭证” 。*guard:通过解引用,你间接修改了被Mutex包裹的i32。- 注意:你自始至终都没有拿到
i32的所有权,你只是在借用。
总结:所有权视角的变更
| 类型 | 标准 Rust (String) | Arc / Rc | Mutex / RefCell |
|---|---|---|---|
| 谁拥有数据 | 变量本身 (Owner) | 引用计数器背后的堆内存 | 容器本身 (Container) |
| 所有权逻辑 | 独裁 (朕即国家) | 共和 (所有持股人共同存亡) | 监管 (想动数据?先过安检) |
| 赋值行为 | Move (原来的死,新的活) | Clone (持股人+1,数据不动) | Move (通常配合 Arc 使用,不直接赋值) |
| 获取数据方式 | 直接使用 | * 解引用拿到内部数据 | 调用 lock() 或 borrow() 拿到临时的 Guard |
| 销毁时机 | 变量离开作用域 | 最后一个变量离开作用域 | 容器离开作用域 |
一句话总结:
Arc/Rc负责延长数据的寿命(通过共享所有权,让数据不那么容易死)。Mutex/RefCell负责管理数据的访问(尽管数据是共享的,但我保证你访问的时候是安全的)。- 把它们套在一起,就实现了: “大家都能一直活着(Arc),且大家都能安全修改(Mutex)” 的效果。
Arc / Rc 这里的赋值行为Clone解释下,和上面说到copy的区别,这里的Clone的是引用是吧?那为什么不用copy,Clone不是深拷贝,那不是引用和堆里的值都被复制了,那应该是多份啊,那为什么说是股份,不应该是一人一份?这和简单类型和复杂类型有什么关系么?
这触及到了 Rust 中 Clone 这个词的“歧义”,也是最容易让新手中招的地方。
一句话结论: 在 Rust 中,Clone 只是一个通用接口(Trait),意思是“复制”。但具体是“深拷贝(复印全部)”还是“浅拷贝(只复印指针)”,完全由这个类型自己决定!
对于 Arc 和 Rc 来说,Clone 只是复制了指针和计数器,并没有复制堆里的数据!
让我们一层层拆解你的疑惑。
1. 最大的误解:Clone = 深拷贝?
不一定。
-
对于
String/Vec:- 它的
clone()实现确实是 深拷贝。 - 它会重新申请堆内存,把数据全拷一遍。
- 结果:两份独立的数据。
- 它的
-
对于
Arc/Rc:-
它的
clone()实现是 特殊逻辑。 -
它只做了两件事:
- 把堆上的计数器 +1。
- 复制一份栈上的指针(指向同一个堆内存)。
-
结果:只有一份数据,但是有两个人拿着遥控器。
-
这就像“配钥匙”:
- String 的 Clone:是照着原来的房子,在隔壁盖了一座一模一样的房子(深拷贝)。
- Arc 的 Clone:只是给原来的房子新配了一把钥匙(浅拷贝+计数)。
所以,这就是为什么我们叫它“股份”——大家手里的钥匙(Arc)虽然多了,但房子(数据)还是那一套。
2. 既然只是复制指针,为什么不用 Copy?
你可能会问:“i32 复制也是复制位数据,Arc 复制也是复制指针位数据,而且都很快,为什么 i32 叫 Copy,Arc 非要叫 Clone?”
这是一个非常底层的技术限制,但逻辑很清晰:
Rust 的铁律:如果一个类型需要“善后处理”(实现了 Drop),它就绝不允许是 Copy。
我们来看看为什么:
场景 A:i32 (Copy)
- 复制:直接内存按位拷贝。
- 销毁:
i32没有任何牵挂,离开作用域时什么都不用做。 - 结论:可以用
Copy。
场景 B:Arc (必须是 Clone)
-
复制:我们假设它是 Copy(直接拷贝指针)。
-
销毁:问题来了!
Arc是有牵挂的。当一个Arc变量离开作用域时,它必须去堆上把计数器 -1。 -
如果它是 Copy:
- 编译器会认为“这只是个拷贝,不需要特殊处理”。
- 当你把
Arc传给别人(Copy 了一份)时,计数器没有 +1(因为 Copy 只是傻傻地复制 bit,不会运行+1的逻辑)。 - 但是当这两个
Arc销毁时,它们都会去执行 -1。 - 结果:计数器乱了!数据可能被提前释放,或者内存泄漏。
所以: 因为 Arc 在复制时需要执行一段特定的代码(计数器 +1),而 Copy 只是无脑复制 bit,无法执行代码。 因此,Arc 必须实现 Clone Trait,让你显式调用 .clone(),从而触发那段 count += 1 的逻辑。
3. 总结:简单类型 vs 复杂类型
回到你的问题: “这和简单类型和复杂类型有什么关系么?”
有关系。
-
简单类型 (Simple Types / POD) :
- 如
i32,bool,f64,char。 - 特点:不需要管理资源(堆、文件、锁)。
- 行为:使用
Copy。系统自动帮你复制,没有心理负担。
- 如
-
资源管理类型 (Resource Managing Types) :
- 如
String(管内存),File(管文件),MutexGuard(管锁)。 - 特点:有“生”也有“死”(Drop)。
- 行为:禁止 Copy。必须用 Move(移交所有权)或 Clone(显式复制)。
- 如
-
智能指针 (Smart Pointers) :
- 如
Rc,Arc。 - 特点:它们看起来很小(就一个指针),像简单类型。但它们背负着管理责任(引用计数)。
- 行为:因为要管理责任,所以禁止 Copy。但为了方便共享,它们的
Clone被设计得极快(只用来增加引用计数)。
- 如
终极对比表
| 概念 | i32 | String | Arc<T> |
|---|---|---|---|
| 复制方式 | Copy (隐式) | Clone (显式) | Clone (显式) |
| 底层操作 | memcpy (复制内存位) | Deep Copy (堆上申请新空间,拷贝所有字符) | Shallow Copy + Logic (复制指针,堆上计数器 +1) |
| 代价 | 极低 | 高 (数据越大越慢) | 低 (无论数据多大,都只是加法运算) |
| 数据份数 | 2 份独立数据 | 2 份独立数据 | 1 份共享数据 (2 个持有者) |
| 为什么这么做 | 不需要善后 | 必须独占堆内存 | 必须维护计数器逻辑 |
Export to Sheets
下次看到 Arc::clone(&data) 时,你的脑海里应该浮现: 这不是在盖新房,这只是在印一把新钥匙,顺便在门口的访客登记表上画了个 +1。