Caiwen的博客

Rust - 所有权与生命周期

2025-12-11 13:14

1. 所有权

当我们使用 let 定义变量的时候,我们应该视为创建了一个数据,然后把这个数据的“所有权”进行了绑定。你需要把变量和数据分开来看。

rust
1
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];

image-20251124170854973

Rust 中,= 不仅是赋值,还意味着所有权的转移:

rust
1
let t = s;

image-20251124171408457

上述代码会把 s 的数据的所有权交给 t,而 s 变成未初始化状态(没有拥有任何数据)

此时我们再写:

rust
1
let u = s;

image-20251124171908074

1.1 Drop

一个变量拥有某个数据的所有权,这意味着这个数据的内存由这个变量管理。如果一个变量是某个数据的所有者,当这个变量掉出作用域时,这个变量肯定是不会再使用到了,其拥有的数据就应该被释放。

rust
1
2
3
4
5
{ let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()]; // do something } // 这里 s 就被释放掉了 println!("{}", s); // s 已经被释放,这里不能再使用

image-20251124172429415

当然这些并不是理所当然的。Rust 在释放变量所拥有的数据的时候,会先判断其数据类型是否实现了 Drop trait,如果没有则仅释放其在栈上的数据,反之则调用其 drop 函数,然后再释放其在栈上的数据。

rust
1
2
3
trait Drop { fn drop(&mut self); }

可以手动使用 drop 函数来提前将某个变量释放。drop 函数的实现非常简单,只是把所有权转移走:

rust
1
fn drop<T>(_x: T) {}

Rust 在 drop 时,会先按定义顺序的反序来调用 drop,遇到嵌套时,先调用外层类型的 drop,再调用内部类型的 drop。drop 函数调用完毕之后再将内存释放掉。

rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Bomb(&'static str); impl Drop for Bomb { fn drop(&mut self) { println!("{}", self.0); } } struct Inner { a: Bomb, b: Bomb, } impl Drop for Inner { fn drop(&mut self) { println!("Inner"); } } struct Outer { x: Inner, y: Bomb, } impl Drop for Outer { fn drop(&mut self) { println!("Outer"); } } fn main() { let _o = Outer { x: Inner { a: Bomb("a"), b: Bomb("b") }, y: Bomb("y"), }; } // 打印:Outer → y → Inner → b → a

1.2 Clone

实现了 Clone trait 的类型可以被复制。可以使用 #[derive(Clone)] 来实现 Clone trait,但是Rust 默认的 Clone 只是复制了栈上数据。如果需要复制堆上内存,或是进行其他的操作,需要手动实现 Clone trait:

rust
1
2
3
4
5
6
trait Clone: Sized { fn clone(&self) -> Self; fn clone_from(&mut self, source: &Self) { *self = source.clone() } }

其中 clone_from 可以将当前的变量直接变为 source clone 过来的值。这看起来和 t = s.clone() 是一样的,但是对于某些类型来说是有优化作用,比如对于 String,直接 t = s.clone() 会先将 s 的堆上内存复制,然后释放 t 的堆上内存。而 t.clone_from(s) 则可以做到,如果 t 在堆上的内存足以存放 s 的数据,则可以直接把 s 的数据复制到 t 上,这会有一些性能优化。

1.3 Copy

有些数据类型,比如 i32 这种基本数据类型,他的复制是没成本的,而且也没什么特殊含义,可以随便复制。这种数据类型往往实现了 Copy trait,带有这种 trait 就意味着编译器可以在赋值时自动将其在栈上的数据原封不动地复制过去,而不再搞什么所有权的转移。

rust
1
2
3
let a = 114514; let b = a; let c = a;

Rust 要求一个类型在实现 Copy 之前必须先实现 Clone

rust
1
trait Copy: Clone {}

Copy 是一个 marker trait,可以直接 #derive[Copy]

Rust 还规定,如果一个类型实现了 Drop trait,那么这个类型就不能实现 Copy trait 了。

2. 引用/借用

rust
1
2
3
4
5
6
7
8
fn f(a: String, b: String) { // do something }// a 和 b 在这里被释放 let x = ... let y = ... f(x, y) // x 和 y 就不能够使用了

上面这个函数的参数直接拿走了数据的所有权,这导致调用完函数之后 xy 就不能继续使用了,但可能这个函数就只是想“借用”一下某个数据,那么我们可以这样写:

rust
1
2
3
4
5
6
7
8
9
10
fn f(a: &String, b: &String) { // do something } fn main() { let x = String::from("xxx"); let y = String::from("yyy"); f(&x, &y); // x 和 y 仍然可以在这里使用 }

& 在这里有两个含义,它可以用作类型,表示数据的引用,也可以用作运算符,表示引用数据。

从 C++ 的角度看,Rust 的引用就相当于是指针。& 用作运算符就相当于 C++ 中的取地址运算符,用作类型里面就相当于 C++ 中的指针。

& 产生的引用也叫做不可变引用:

rust
1
2
3
let s = String::from("hello world"); let r: &String = &s; (*r) = String::from("hello rust");

image-20251208141949292

Rust 还有一种可变引用:&mut。我们可以根据上面的报错提示将代码改写成下面这种:

rust
1
2
3
let s = String::from("hello world"); let r = &mut s; (*r) = String::from("hello rust");

但还是报错:

image-20251208142144426

这是因为 Rust 中,变量默认是不可变的。如果我们想要可变地借用,那么必须变量本身就是可变的:

rust
1
2
3
let mut s = String::from("hello world"); let r = &mut s; (*r) = String::from("hello rust");

2.1 解引用

C++ 中可以解引用,在 Rust 中也可以,我们上面例子就已经看到。需要注意的是解引用会获得其所有权,如果你再把所有权转移出去,就会报错:

rust
1
2
3
let x = String::from("xxx"); let r = &x; let y = *r;

我们明明是借用 x 的,后面又通过这个借用尝试获得其所有权,这显然是不行的:

image-20251208132748149

如果解引用不是为了转移所有权,那还是允许的:

rust
1
2
3
4
5
6
7
8
struct Point { x: i32, y: i32, s: String, } let p = Point { x: 10, y: 20, s: String::from("hello") }; let r = &p; let s = &(*r).s;

2.2 自动引用/解引用

考虑到在 Rust 中,引用比较常见,所以 . 这个东西还有些额外作用。首先他可以自动解引用:

rust
1
2
3
4
5
6
7
8
9
10
struct Point { x: i32, y: i32, s: String, } let p = Point { x: 10, y: 20, s: String::from("hello") }; let r = &p; let x = r.x; // ---- 等价于 ---- let x = (*r).x;

Rust 中引用也是一个“实体”,比如上面,r 相当于拥有了某个对 p 的不可变引用的所有权。我们可以对引用进行引用:

rust
1
2
3
4
5
6
let rr = &r; let rrr = &rr; let rrrr = &rrr; let x = rrrr.x; // ---- 等价于 ---- let x = (*(*(*rrrr))).x;

. 调用函数时,如果需要引用,那么会自动引用:

rust
1
2
3
4
let s = String::from("a b c"); let arr = s.split(" "); // ---- 等价于 ---- let arr = (&s).split(" ");

image-20251208141309177

又比如:

rust
1
2
3
4
let mut v = vec![1, 2, 3, 4, 5]; v.sort(); // ---- 等价于 ---- (&mut v).sort()

image-20251208143332796

2.3 引用结构体

如果要借用某个变量,那么这个变量必须是完整的。

rust
1
2
3
4
5
6
7
8
9
10
11
struct S { x: String, y: String, } let s = S { x: String::from("long string is long"), y: String::from("xyz"), }; let x = s.x; // 此时 s 不再拥有 x 的所有权,s 不再完整 let r = &s; let y = s.y; // 但我可以继续从残缺的 s 中再把 y 取出来

image-20251208171218039

Rust 的这种设计带来了一个好处,我们可以很清楚的从函数的签名中大概知道函数想要做什么。

  • 如果函数要求传递所有权,那么这个函数就表示我想“消耗”掉这个数据,数据被消耗掉之后就不应该被继续使用了。
  • 如果函数要求传递不可变引用,那么这个函数就表示我就是想“借用”一下这个数据,而且不会对这个数据进行任何修改。
  • 如果函数要求传递可变引用,那么这个函数不仅想“借用”一下这个数据,还会对这个数据进行修改。

2.4 Sharing Versus Mutation

当共享和可变之间结合时可能会出问题,因此有一些规则限制引用的使用:

引用期间不能够移动

rust
1
2
3
4
let v = vec![1, 2, 3, 4]; let r = &v; let aside = v; r[0];

如果我们借用了某个变量,那么在整个借用期间,这个变量指向的数据不能被移动到别的地方去,不然我们的引用就变成悬垂引用了。

image-20251208172152505

这样就可以了,确保移动时不存在其他的引用。注意,尽管我们之前可能一直在说函数结束之后某个变量才会被释放,但是 Rust 的编译器会足够聪明,如果从某个地方开始你就不再使用一个变量了,那么 Rust 会认为这个变量的生命周期就持续到那里。

rust
1
2
3
4
5
6
{ let v = vec![1, 2, 3, 4]; let r = &v; r[0]; let aside = v; }

而在 C++ 中,我们很可能将某个指针指向的数据释放掉了,但忘了这个事,继续用这个指针。而 Rust 中你想释放内存肯定要有所有权,而引用期间所有权不会发生转移,所以不会出现这种情况。

又比如:

rust
1
2
3
4
let mut x = 10; let r1 = &mut x; x += 10; // 处在引用期间,不能够被移动 println!("{}", r1);

不可变引用期间不能够有可变引用

从直觉上来说,如果不可变引用期间,我们把数据改变了,但是后面又忘了这个事,可能就容易出 bug(类似于变量默认不可变的设计思想),但有时可能没这么简单:

rust
1
2
3
4
let mut v = vec![1, 2, 3, 4]; let r = &v[0]; v.push(5); println!("r: {}", r);

image-20251208174732360

.push 虽然可能看起来只会改变数组最后面的部分,并不会影响到我对 v[0] 的引用。但是你注意,.push 接受可变引用就意味着他可能改变整个 Vector 的任何部分。一种情况是,如果我们 push 时发现 vector 容量不够了,需要扩容,那么我们将会重新再堆上分配一个空间,把数据复制过去,然后再释放掉原来的数据,此时之前对 v[0] 的引用就失效了。

C++ 的 vector 也是类似的。而 C++ 并不会去检查这些,一旦出现这种问题将会非常棘手。

可变引用期间不能够有其他引用

其中的道理和上面是差不多的。

rust
1
2
3
4
let mut x = 10; let r1 = &mut x; let r2 = &x; (*r1) = 12;

image-20251208175551838

更多

Rust 中,每个数据有且仅有一个拥有其所有权的变量,因此这可以形成一个树形结构:

image-20251208180312171

当我们不可变地借用某个变量时,其所拥有的变量(即图中子树部分)是只读的(毕竟你只是借用,甚至还是不可变的借用)。其父节点一直到根节点这条链上的点也都是只读的(否则的话就可以在祖先节点上将数据修改掉,制造出悬垂引用)。

当我们可变地借用某个变量时,其所拥有的变量是可读可写的,也就是整个子树部分都相当于是进行了一个可变引用。如果直接引用其子节点,Rust 会认为你在可变引用的基础上又进行了一个不可变引用。但你可以通过这个可变引用来引用:

rust
1
2
3
4
5
6
7
8
9
#[derive(Debug)] struct S { x: i32, y: i32 } let mut s = S { x: 10, y: 20 }; let t_mut = &mut s; let r1 = &s.x; println!("{:#?}", t_mut);

image-20251208181336379

rust
1
2
3
4
5
6
7
8
9
#[derive(Debug)] struct S { x: i32, y: i32 } let mut s = S { x: 10, y: 20 }; let t_mut = &mut s; let r1 = &t_mut.x; println!("{:#?}", t_mut);

同时,产生可变引用的祖先节点不仅是不能够写,甚至连引用都不行了,因为有可能顺着祖先节点的引用来引用到可变引用部分,此时就违背了可变引用单独存在的规则。

2.5 生命周期

引用的生命周期是其保持有效的范围。如果我们引用了某个东西,那么我们需要保证在引用的这段时间,被引用的数据没有被释放:

rust
1
2
3
4
5
6
7
8
{ let r; { let x = 1; r = &x; } assert_eq!(*r, 1); }

image-20251208144538892

image-20251208144713692

蓝色表示 x 的生命周期,绿色表示 r 的生命周期,我们可以看到,在 x 生命周期已经结束,被释放后,r 的生命周期还没结束。Rust 会阻止这样的代码通过编译。

如果我们改成这样就可以了:

image-20251208145626337

而我们在 C++ 中很容易写出这样的代码:

image-20251208173518262

使用结构体也会有类似的问题:

rust
1
2
3
4
5
6
7
8
9
struct S { r: &i32 } let s; { let x = 10; s = S { r: &x }; } assert_eq!(*s.r, 10);

image-20251208153020329

Rust 还是发现了这个问题。但是即使我们改成下面这样,不再尝试引用已经被释放掉的变量了,Rust 还是会报错:

rust
1
2
3
4
5
6
7
8
9
struct S { r: &i32 } let x = 10; { let s; s = S { r: &x }; assert_eq!(*s.r, 10); }

2.5.1 手动标注

这是因为 Rust 要求结构体中的引用必须手动标注生命周期。

rust
1
2
3
struct S<'a> { // <> 里写结构体内所有手动标注的生命周期 r: &'a i32 }

其中 'a 中的 a 是可以自己命名的,我们一般用 abc... 这样的字母来手动标注。在 Rust 中,生命周期也是数据类型的一部分,起到约束的作用。只不过在大多数情况下,Rust 会自动推导生命周期,无需我们手动标注。在结构体中,结构体的生命周期显然取各个引用成员中生命周期最短的那个就好了,但是各个成员之间的生命周期 Rust 无法决定。上面只有一个成员还看不出什么问题来,但如果有多个,比如,对于有两个成员变量:

写法一

rust
1
2
3
4
struct S<'a> { x: &'a i32, y: &'a i32 }

写法二

rust
1
2
3
4
struct S<'a, 'b> { x: &'a i32, y: &'b i32 }

有两种写法,并且表示的含义完全不同,Rust 无法确定。这就是为什么你需要手动标注生命周期。

写法一表示,结构体内的 x 和结构体内的 y 和结构体本身,三者的生命周期一致。注意,这并不意味着创建结构体的时候必须是两个活得一样长的变量才能够赋值进去。Rust 会自动将 'a 推导成带有 'a 的成员中较短的生命周期。

rust
1
2
3
4
5
6
7
8
9
10
let x = 10; let r; { let y = 20; { let s = S { x: &x, y: &y }; r = s.x; } } println!("{}", r);

理论上来说,上面的代码是不会产生悬垂引用的,Rust 没理由报错,但是还是报错了:

image-20251208165848267

这是因为 Rust 会将 'a 推导为 y 的生命周期。于是结构体中的 s.x 的生命周期就和 y 相当了,尽管 x 的生命周期是比较长的。

而写法二则不同。写法二则表示 xy 的生命周期没有关联。这样的话上面的代码就正确了,s.x 就会和 x 的生命周期一致,s.y 就会和 y 的生命周期一致,而 s 的生命周期会取 'a'b 中较小的那个,也就是会和 y 生命周期一致(尽管实际上 s 会先比 y 释放掉)。这样即使结构体释放掉了,其中较长生命周期的那个引用还能够接着用。

又比如:

rust
1
2
3
4
5
struct S<'a, 'b> { x: &'a i32, y: &'b i32, z: &'b i32 }

这就表示 xyz 的生命周期没有关联,但是 yz 生命周期是一样的。也就是比如结构体被实例化为变量 sy 生命周期较大而 z 较小,当 s.z 生命结束,s.y 也跟着结束,尽管 y 还没结束生命。

总的来说,给结构体手动标注生命周期相当于对成员之间生命周期关系做出了约束。

当函数需要返回引用的时候也会遇到这种问题,比如:

rust
1
2
3
4
5
6
7
8
9
10
fn longest(x: &String, y: &String) -> &String { if x.len() > y.len() { x } else { y } } fn main() { longest(&String::from("hello"), &String::from("world!")); }

此时 Rust 不清楚返回值的生命周期是什么,有可能生命周期和 x 是一样的,有可能和 y 是一样的,有可能取 xy 中存活最短的那个,也有可能和 xy 没有关系(生命周期为 'static)。此时我们分析一下我们究竟要干什么,我们想要返回两个字符串之间最长的那个的引用,那么我们的返回值的生命周期应该和 xy 之间较长的那个是一致的。但是我们只有运行的时候才知道 xy 哪个存活时间更久,而 Rust 作为静态类型语言,需要在编译期间就知道所有类型的情况。我们可以保守一点,令返回值的生命周期为两者之中较小的那个:

rust
1
2
3
4
5
6
7
fn longest<'a>(x: &'a String, y: &'a String) -> &'a String { if x.len() > y.len() { x } else { y } }
rust
1
2
3
4
5
6
7
8
9
fn main() { let y = String::from("world!"); let r; { let x = String::from("hello"); r = longest(&x, &y); } println!("The longest string is {}", r); }

给函数标注生命周期相当于约束了返回值之间,返回值和参数之间生命周期的关系。

2.5.2 高阶生命周期约束

高阶生命周期约束指的是表明某个含有生命周期的类型,可以接受任意的生命周期。主要用在回调的场景下,比如:

rust
1
2
3
4
5
6
7
fn apply<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str, { let local = String::from("x"); f(&local); }

即表明,apply 函数接受的这个 F 闭包,需要能够处理传入的任何生命周期 'a。这样做是因为闭包实际上是在 apply 函数内部进行调用的,其生命周期应该和函数内部那个 local 的生命周期一致。而编译器只能根据函数的参数来在编译期推导生命周期。

rust
1
apply(|s| s); // 这里看不出、也不该由这里决定 apply 内部会用多短的借用

2.5.3 生命周期提升

Rust 貌似正在考虑将这一部分的内容进行改进,所以这里只举几个简单的例子,并且这里这些内容还有可能在将来失效。

rust
1
f(&String::from('🦀'));

引用的临时 String 会在函数 f 返回时就被 drop 掉。

这就导致下面的代码是编译不过去的(假设 f 返回值的生命周期和参数一致):

rust
1
2
let a = f(&String::from("Hello, world!")); println!("{}", a);

所以事实上这么写之后,返回值是永远用不了的。

rust
1
g(f(&String::from('🦀')));

引用的临时 String 会在最外层的函数 g 返回时才被 drop 掉。这意味着语句中的临时值生命周期会被延长到整个语句,当然也仅限于整个语句。

rust
1
2
3
if f(&String::from('🦀')) { … }

如果 f 作为条件的话,那么其返回值应该是一个布尔值,和临时的 String 的生命周期无关,所以里面这个临时的 String 会在 f 返回时就被 drop 掉。

rust
1
2
3
4
5
fn example(m: &Mutex<String>) { if m.lock().unwrap().is_empty() { println!("the string is empty!"); } }

同上,这意味着 if{} 内不会持有锁。

但是对于 if let 来说是相反的:

rust
1
2
3
if let … = f(&String::from('🦀')) { … }

有可能模式匹配出来的值和 f 的参数的生命周期有关,所以 String 的生命周期被延长到了整个 {}

即使事实上模式匹配出来的值和参数的生命周期无关,生命周期也会被延长:

rust
1
2
3
4
5
if let Some(x) = vec.lock().unwrap().pop() { // `Mutex`在这里仍然被锁着 :( // 这是不必要的,因为我们并没有从`Vec`中借用任何东西。(`x`是一个`T`) println!("popped item from the vec: {x}"); }

直接对一个临时值取引用,然后绑在一个变量上面,也会带来生命周期提升:

rust
1
2
3
let a = &String::from("Hello, world!"); f(a); f(a);

这样的话是不行的:

rust
1
2
3
4
// error! let a = &f(String::from("Hello, world!")); f(a); f(a);

对于常量,Rust 会选择直接将其生命周期提升为 'static,尽管可能没必要:

rust
1
2
let x = f(&3); // 这里的&3是 'static 的,不论对 `f()` 来说是否有必要 ···

3. 智能指针

3.1 Box

如果我创建了一个结构体,那么其大概是在栈上的:

rust
1
2
3
4
5
struct S { x: u32, y: u32 } let s = S { x: 1, y: 2 };

我可以使用 Box 这个东西将其搞到堆上面:

rust
1
2
3
4
let s = Box::new(S { x: 1, y: 2 }); // s 的类型是 Box<S> (*s).x s.x

涉及到堆的话我们就要讨论一下内存安全问题。Box 会将其包裹的数据放到堆上面,然后 Box 本身相当于一个指针,指向堆上的数据。

rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// https://doc.rust-lang.org/src/alloc/boxed.rs.html#231-234 pub struct Box< T: ?Sized, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global, >(Unique<T>, A); impl<T> Box<T> { #[cfg(not(no_global_oom_handling))] #[inline(always)] #[stable(feature = "rust1", since = "1.0.0")] #[must_use] #[rustc_diagnostic_item = "box_new"] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces pub fn new(x: T) -> Self { return box_new(x); } } // https://docs.rs/unique/latest/src/unique/lib.rs.html#47 /// Constructs a `Box<T>` by calling the `exchange_malloc` lang item and moving the argument into /// the newly allocated memory. This is an intrinsic to avoid unnecessary copies. /// /// This is the surface syntax for `box <expr>` expressions. #[rustc_intrinsic] #[unstable(feature = "liballoc_internals", issue = "none")] pub fn box_new<T>(x: T) -> Box<T>; #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> { #[inline] fn drop(&mut self) { // the T in the Box is dropped by the compiler before the destructor is run let ptr = self.0; unsafe { let layout = Layout::for_value_raw(ptr.as_ptr()); if layout.size() != 0 { self.1.deallocate(From::from(ptr.cast()), layout); } } } }

上面这个例子一眼看去感觉没什么实际意义,无非就是换了个地方放数据。但有这样一种情形需要考虑:

比如我们想实现一个类似链表的东西:

rust
1
2
3
4
5
struct S { x: u32, y: u32, next: Option<S> }

image-20251208190224551

Rust 要求在编译期间知道每个类型的大小,但是上面这种“自我引用”的类型,会让 Rust 无法计算。我们需要一种固定大小的东西,还能通过这个东西间接访问到数据。那么这个东西就是指针了。然后考虑一下指针大概指向上或是堆上。

如果我们把数据放到栈上,那么这个指针就相当于是引用,我们可以这么写:

rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct S<'a> { x: u32, y: u32, next: Option<&'a S<'a>> } let node = S { x: 5, y: 6, next: None }; let root = S { x: 1, y: 2, next: Some(&node) };

不过这样的话,这里的 node 的所有权是在 root 的外面的,换句话说就是链表中节点的所有权在链表外,这多少看起来有点不妥,比如我想把一个链表的所有权转移出去,我可能要把所有节点的所有权也跟着一个个转走。而且这后面可能会涉及到生命周期的问题。

如果把数据放到堆上,就可以使用 Box 了:

rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct S { x: u32, y: u32, next: Option<Box<S>> } let root = S { x: 1, y: 2, next: Some(Box::new(S { x: 3, y: 4, next: None })) };

这样我们就可以认为链表内节点的所有权都在链表内部,看起来更舒服一点。

3.2 Rc / Arc

目前 Rust 的所有权机制使得每个数据只能由一个所有者,这样一个数据的生命周期就比较清晰。但是很多时候一个数据的生命周期是不好说的。比如我有多个结构体都需要引用一个数据,那么这个数据的所有者是不好说的,因为这个数据的生命周期就不好确定,其被释放的最好时机应该是这些结构体都被释放了,也就没人引用这个数据了,那么这个数据就应该被释放了。但是单纯依赖单一所有者机制是不好实现这个东西。

Rc 就帮助我们解决了这一点,其基本原理是引用计数,大概如图所示。

image-20251208192002688

但是 Rc 的引用计数不是原子操作,所以 Rc 不是线程安全的。于是 Rc 是 !Send + !Sync。Arc 则使用原子操作来维护引用计数,是线程安全的。

4. 内部可变性

这里只讨论单线程下的内部可变性。对于多线程,则需要原子操作或是锁来确保线程安全。

单线程的内部可变性可能在如下的场景中有用:

  • 一些图/树型的数据结构,需要配合 Rc 来建立数据结构,并配合内部可变性来做到能够修改数据:

    rust
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type NodeRef = Rc<RefCell<Node>>; struct Node { children: Vec<NodeRef>, parent: Option<Weak<RefCell<Node>>>, text: String, } fn rename(node: &NodeRef, s: String) { node.borrow_mut().text = s; // 只有 &NodeRef,没有唯一 &mut }
  • 接口对外暴露的是查询,只需要 &self,但实际上内部需要维护一些对外部透明的数据(比如缓存)

    rust
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct Parser { source: String, cache: RefCell<HashMap<Span, Ast>>, } impl Parser { fn parse_expr(&self, span: Span) -> Ast { if let Some(ast) = self.cache.borrow().get(&span) { return ast.clone(); } let ast = /* 真正解析 */; self.cache.borrow_mut().insert(span, ast.clone()); ast } }

4.1 Cell

Cell<T> 支持如下操作:

  • Cell::new(value):创建一个 Cell,并把 value 丢进去。
  • cell.get():仅 T 为 Copy 时才可以,将 Cell 内的值取出。
  • cell.set(value):这个 .set 只需要 Cell 的不可变引用,将 value 插入到 Cell 中。
  • cell.replace(value):如果 T 不是 Copy 的话可以用这个办法,需要用一个新的 value 去和 Cell 内部的值进行替换。

4.2 RefCell

直接用 Cell 的话会有限制:要么只能得到内部的值的 copy,要么直接就把内部的值的所有权取出来。而 RefCell 则可以做到直接取内部的值的可变引用,但是 RefCell 会维护当前的可变引用和不可变引用的计数,在运行时进行引用规则的检查:

  • RefCell::new(value):创建一个 RefCell,并把 value 丢进去。
  • ref_cell.borrow():对 RefCell 内的值进行一个不可变引用。如果当前已经有了可变引用了,则直接 panic。反之则返回一个 Ref<T>,在 Ref<T> drop 时归还不可变引用计数。
  • ref_cell.borrow_mut():和上面那个类似,但是是可变引用。
  • ref_cell.try_borrow()ref_cell.try_borrow_mut():不会 panic 的版本,会返回 Result。