所有权

53 阅读26分钟

一个值只能被一个变量所拥有,这个变量被称为所有者(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),它们极其便宜,可以无限复制。

  • 场景:你有一张电子发票的 PDFa = 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;

  1. 对于 s (Owner)

    • 地位:我是老大,地契在我手里。
    • 状态:但我现在被套牢了。只要 r 还没死(还没用完),我就不能跑(Move),也不能死(Drop),也不能改(Mutate)。
    • 责任:等 r 走了,我还要负责最后的清理工作。
  2. 对于 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);
}
引用函数,短生命周期,编译失败

image.png

线索:可变引用

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] 引用失效,导致内存安全问题

 在一个作用域内,仅允许一个活跃的可变引用。所谓活跃,就是真正被使用来修改数据的可变引用,如果只是定义了,却没有使用或者当作只读引用使用,不算活跃。

 在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在

意思就是同时的状态,只要有一个人在编辑,可变,那就只能有这么一个可变,连只读的都不能有, 第二个 同时的可以有多个只读的

  1. 独占写入(编辑模式):

    • 只要有一个“编辑者”(可变引用 &mut T)存在。
    • 那么此时,不能任何其他人,无论是“查看者”(&T)还是其他“编辑者”&mut T)。
    • 就像你在编辑文档时,不希望有人在你背后同时查看或同时编辑,以免造成混乱。
  2. 共享读取(只读模式):

    • 你可以同时有很多个“查看者”(&T)。
    • 但此时,不能任何编辑者”(&mut T)。
    • 只要文档的内容是“只读”的,那么多少人同时看都没关系,因为内容不会变。

这两个状态在同一时间互斥的。你要么处于“共享读取”模式,要么处于“独占写入”模式,不能同时都占。

不可变借用 (&)  是“把你冻结,只许看不许改”; 那么 可变借用 (&mut)  就是  “把你关进小黑屋,外面的人(包括你自己)既不能看也不能改,只有持有钥匙的那唯一一个人能动。”

可变借用(Mutable Borrowing)在 Rust 里的学术名称叫 独占引用 (Exclusive Reference)

这也是所有权体系中最霸道的一条规则。


1. 此时的所有权状态:完全封锁

当你写下 let r = &mut s; 时:

  1. 所有权 (Ownership) :依然归原主 (s) 所有。

  2. 原主 (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 (可变引用) 同时访问:

  1. r 正在做 r.push_str(...)。这可能会导致 String 的堆内存扩容(Reallocate)。
  2. 扩容意味着:旧的堆内存被释放,数据搬到了新的更大的内存地址。
  3. 如果此时 s 还能访问(或者有一个 r2 指向 s),它手里拿的那个旧指针(Old Pointer)就指向了无效内存。
  4. Boom!  悬垂指针 / 读取垃圾数据。

所以 Rust 规定:只要有一个人在改,其他所有人(包括主人)都必须闭眼、把手拿开。


3. 借用的“金科玉律”

这是 Rust 借用检查器(Borrow Checker)的核心逻辑,请背诵:

在任意给定时间,对于同一个数据,你只能拥有以下二者之一(互斥):

  1. 任意数量 的 不可变引用 (&T) —— (大家都能看,没人能改)

    • 类比:图书馆阅览室。一百个人可以同时看同一本书,但谁也不能拿笔涂改。
  2. 且仅有一个 可变引用 (&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,引用计数归零,堆上内存被释放。

image.png

线索:你可能继续会疑惑:Rc 是怎么产生在堆上的?并且为什么这段堆内存不受栈内存生命周期的控制呢?

Box::leak() 机制

Rust提供的方式是 Box::leak()创建不受栈内存控制的堆内存,而绕过编译时的所有权规则,

Box 是 Rust 下的智能指针,它可以强制任何数据结构创建在堆上,然后在栈上放一个指 针指向这个数据结构,但此时堆内存生命周期仍然是受控的,跟栈上的指针一致

Box::leak(),顾名思义,它创建的对象,从堆内存上泄漏出去,不受栈内存控制,是一个 自由的、生命周期可以大到和整个进程的生命周期一致的对象

当我们要改变用Rc创建的节点时,无法完成,因为Rc 是一个只读的引用计数器,所以就引出了RefCell

和 Rc 类似,RefCell 也绕过了 Rust 编译器的静态检查,允许我们在运行时,对某个只读 数据进行可变借用。这就涉及 Rust 另一个比较独特且有点难懂的概念:内部可变性

概念:内部可变形

当我们用 let mut 显式地声明一个可变的值,或者,用 &mut 声明一个可变引用时,编 译器可以在编译时进行严格地检查,保证只有可变的值或者可变的引用,才能修改值内部 的数据,这被称作外部可变性(exterior mutability),外部可变性通过 mut 关键字声 明

在编译器的眼里,值是只读的,但是在运行时, 这个可以得到可变借用,从而修改内部的数据,这就是 RefCell 的用武之地。

image.png

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> 拥有 TT 就在 Mutex 的肚子里。
    • 如果你销毁了 Mutex,里面的 T 也随之销毁。
  • 你能得到什么?

    • 永远无法直接拿到内部的 T 的所有权(除非你把 Mutex 砸了,用 into_inner())。

    • 你只能申请到一个  “看守/卫兵” (Guard)

      • RefCell 给你的叫 Ref 或 RefMut
      • Mutex 给你的叫 MutexGuard
  • 神奇的 Guard

    • 这个 Guard 表现得像一个引用(通过 Deref)。
    • 但它不仅仅是引用,它还是锁的钥匙
    • 当 Guard 离开作用域(Drop)时,它不仅不仅销毁自己,还负责把锁还回去(释放锁)。

形象类比:超市储物柜 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(); 

此时的所有权状态:

  1. 最外层 (Arc)

    • data 和 thread_a_ref 共同持有(Share)那个 Mutex 的所有权。
    • 只要还有一个线程拿着 ArcMutex 就不会被销毁。
  2. 中间层 (Mutex)

    • Mutex 独占持有内部 i32 (0) 的所有权。
    • 它负责保证同一时间只有一个人能摸到这个 0
  3. 当你操作时 (lock)

    Rust

    {
        let mut guard = thread_a_ref.lock().unwrap();
        *guard += 1;
    } // guard 离开作用域
    
    • lock():你向 Mutex 申请访问权。
    • guard:你拿到了一个  “借用凭证”
    • *guard:通过解引用,你间接修改了被 Mutex 包裹的 i32
    • 注意:你自始至终都没有拿到 i32 的所有权,你只是在借用。

总结:所有权视角的变更

类型标准 Rust (String)Arc / RcMutex / 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. 把堆上的计数器 +1
      2. 复制一份栈上的指针(指向同一个堆内存)。
    • 结果只有一份数据,但是有两个人拿着遥控器。

这就像“配钥匙”:

  • String 的 Clone:是照着原来的房子,在隔壁盖了一座一模一样的房子(深拷贝)。
  • Arc 的 Clone:只是给原来的房子新配了一把钥匙(浅拷贝+计数)。

所以,这就是为什么我们叫它“股份”——大家手里的钥匙(Arc)虽然多了,但房子(数据)还是那一套。


2. 既然只是复制指针,为什么不用 Copy

你可能会问:“i32 复制也是复制位数据,Arc 复制也是复制指针位数据,而且都很快,为什么 i32 叫 CopyArc 非要叫 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 复杂类型

回到你的问题: “这和简单类型和复杂类型有什么关系么?”

有关系。

  1. 简单类型 (Simple Types / POD)

    • 如 i32boolf64char
    • 特点:不需要管理资源(堆、文件、锁)。
    • 行为:使用 Copy。系统自动帮你复制,没有心理负担。
  2. 资源管理类型 (Resource Managing Types)

    • 如 String (管内存), File (管文件), MutexGuard (管锁)。
    • 特点:有“生”也有“死”(Drop)。
    • 行为禁止 Copy。必须用 Move(移交所有权)或 Clone(显式复制)。
  3. 智能指针 (Smart Pointers)

    • 如 RcArc
    • 特点:它们看起来很小(就一个指针),像简单类型。但它们背负着管理责任(引用计数)。
    • 行为:因为要管理责任,所以禁止 Copy。但为了方便共享,它们的 Clone 被设计得极快(只用来增加引用计数)。

终极对比表

概念i32StringArc<T>
复制方式Copy (隐式)Clone (显式)Clone (显式)
底层操作memcpy (复制内存位)Deep Copy (堆上申请新空间,拷贝所有字符)Shallow Copy + Logic (复制指针,堆上计数器 +1)
代价极低 (数据越大越慢) (无论数据多大,都只是加法运算)
数据份数2 份独立数据2 份独立数据1 份共享数据 (2 个持有者)
为什么这么做不需要善后必须独占堆内存必须维护计数器逻辑

Export to Sheets

下次看到 Arc::clone(&data) 时,你的脑海里应该浮现:  这不是在盖新房,这只是在印一把新钥匙,顺便在门口的访客登记表上画了个  +1