Rust中的Rc<T>和RefCell<T>深入解析
立即解锁
发布时间: 2025-08-16 02:20:31 阅读量: 1 订阅数: 9 


Rust编程实战:从入门到构建多线程Web服务器
### Rust 中的 Rc<T> 和 RefCell<T> 深入解析
#### 1. Rc<T> 实现多所有权
在 Rust 中,`Rc<T>` 允许一个值拥有多个所有者。以下是一个使用 `Rc<T>` 的示例代码:
```rust
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
```
由于 `Rc<T>` 不在 Rust 的预导入模块中,因此需要使用 `use std::rc::Rc;` 将其引入作用域。在 `main` 函数中,我们创建了一个包含 5 和 10 的列表,并将其存储在 `Rc<List>` 类型的变量 `a` 中。然后,在创建 `b` 和 `c` 时,我们调用了 `Rc::clone` 函数,并将 `a` 的引用作为参数传递。
这里需要注意的是,虽然我们也可以调用 `a.clone()`,但 Rust 的惯例是在这种情况下使用 `Rc::clone`。`Rc::clone` 不会像大多数类型的 `clone` 方法那样对所有数据进行深拷贝,它只是增加引用计数,这不会花费太多时间。而数据的深拷贝可能会花费大量时间。通过使用 `Rc::clone` 进行引用计数,我们可以直观地区分深拷贝和增加引用计数的克隆操作。在查找代码中的性能问题时,我们只需要考虑深拷贝的克隆操作,而可以忽略对 `Rc::clone` 的调用。
#### 2. 克隆 Rc<T> 增加引用计数
为了观察引用计数的变化,我们对上述示例进行修改:
```rust
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!(
"count after creating a = {}",
Rc::strong_count(&a)
);
let b = Cons(3, Rc::clone(&a));
println!(
"count after creating b = {}",
Rc::strong_count(&a)
);
{
let c = Cons(4, Rc::clone(&a));
println!(
"count after creating c = {}",
Rc::strong_count(&a)
);
}
println!(
"count after c goes out of scope = {}",
Rc::strong_count(&a)
);
}
```
在程序中引用计数发生变化的每个点,我们通过调用 `Rc::strong_count` 函数来打印引用计数。该函数之所以命名为 `strong_count` 而不是 `count`,是因为 `Rc<T>` 类型还有一个 `weak_count`。
这段代码的输出如下:
```plaintext
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
```
从输出结果可以看出,`a` 中的 `Rc<List>` 的初始引用计数为 1,每次调用 `clone` 时,计数增加 1。当 `c` 超出作用域时,计数减少 1。我们不需要像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少引用计数,因为 `Rc<T>` 类型实现了 `Drop` 特征,当 `Rc<T>` 值超出作用域时,引用计数会自动减少。
需要注意的是,在这个示例中我们看不到的是,当 `b` 和 `a` 在 `main` 函数结束时超出作用域,引用计数将变为 0,`Rc<List>` 将被完全清理。使用 `Rc<T>` 允许单个值有多个所有者,并且引用计数确保只要任何一个所有者仍然存在,该值就仍然有效。
`Rc<T>` 通过不可变引用允许你在程序的多个部分之间共享只读数据。如果 `Rc<T>` 也允许有多个可变引用,那么可能会违反 Rust 的借用规则,即对同一位置的多个可变借用会导致数据竞争和不一致。然而,能够修改数据是非常有用的!接下来,我们将讨论内部可变性模式和 `RefCell<T>` 类型,它可以与 `Rc<T>` 结合使用,以解决这种不可变性限制。
#### 3. RefCell<T> 和内部可变性模式
内部可变性是 Rust 中的一种设计模式,它允许你在有不可变引用指向数据时仍然可以修改该数据。通常,这种操作是被借用规则所禁止的。为了修改数据,该模式在数据结构内部使用不安全代码来绕过 Rust 通常的可变和借用规则。不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器为我们检查。
我们只能在确保在运行时遵守借用规则的情况下使用遵循内部可变性模式的类型,即使编译器无法保证这一点。涉及的不安全代码会被包装在一个安全的 API 中,而外部类型仍然是不可变的。
下面我们通过 `RefCell<T>` 类型来探索内部可变性模式。
#### 4. 在运行时强制执行借用规则的 RefCell<T>
与 `Rc<T>` 不同,`RefCell<T>` 类型表示对其所持数据的单一所有权。那么 `RefCell<T>` 与 `Box<T>` 等类型有什么不同呢?回顾一下借用规则:
- 在任何给定时间,你可以有一个可变引用或任意数量的不可变引用(但不能同时有)。
- 引用必须始终有效。
对于普通引用和 `Box<T>`,借用规则的不变性在编译时强制执行。而对于 `RefCell<T>`,这些不变性在运行时强制执行。如果违反了借用规则,使用普通引用会得到编译器错误,而使用 `RefCell<T>` 程序会在运行时发生恐慌并退出。
在编译时检查借用规则的优点是,错误会在开发过程中更早地被捕获,并且由于所有分析都在事先完成,对运行时性能没有影响。因此,在大多数情况下,编译时检查借用规则是最佳选择,这也是 Rust 的默认行为。
在运行时检查借用规则的优点是,某些内存安全的场景是允许的,而这些场景在编译时检查中可能会被禁止。静态分析(如 Rust 编译器)本质上是保守的,有些代码属性是无法通过分析代码来检测的,最著名的例子是停机问题。
由于有些分析是不可能的,如果 Rust 编译器不能确定代码是否符合所有权规则,它可能会拒绝一个正确的程序。如果 Rust 接受一个错误的程序,用户将无法信任 Rust 所做的保证。然而,如果 Rust 拒绝一个正确的程序,程序员会感到不便,但不会发生灾难性的事情。当你确定你的代码遵循借用规则,但编译器无法理解和保证时,`RefCell<T>` 类型就很有用。
与 `Rc<T>` 类似,`RefCell<T>` 仅适用于单线程场景,如果你尝试在多线程上下文中使用它,会得到编译时错误。
下面是选择 `Box<T>`、`Rc<T>` 或 `RefCell<T>` 的原因总结:
| 类型 | 所有权 | 借用检查时间 | 可变借用 |
| ---- | ---- | ---- | ---- |
| `Rc<T>` | 多个所有者 | 编译时 | 不允许 |
| `Box<T>` | 单个所有者 | 编译时 | 允许 |
| `RefCell<T>` | 单个所有者 | 运行时 | 允许 |
由于 `RefCell<T>` 允许在运行时进行可变借用检查,因此即使 `RefCell<T>` 本身是不可变的,你也可以修改其内部的值。在不可变值内部进行修改就是内部可变性模式。接下来,我们将探讨一个内部可变性有用的实际场景,并研究它是如何实现的。
#### 5. 内部可变性:对不可变值的可变借用
借用规则的一个结果是,当你有一个不可变值时,你不能对其进行可变借用。例如,以下代码无法编译:
```rust
fn main() {
let x = 5;
let y = &mut x;
}
```
如果你尝试编译这段代码,会得到以下错误:
```plaintext
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
```
然而,在某些情况下,一个值在其方法中修改自身,但对其他代码表现为不可变是很有用的。值的方法外部的代码将无法修改该值。使用 `RefCell<T>` 是实现内部可变性的一种方法,但 `RefCell<T>` 并不能完全绕过借用规则:编译器的借用检查器允许这种内部可变性,而借用规则在运行时进行检查。如果你违反了规则,程序会发生恐慌而不是得到编译器错误。
下面我们通过一个实际例子来看看如何使用 `RefCell<T>` 修改不可变值,以及为什么这很有用。
#### 6. 内部可变性的用例:模拟对象
在测试过程中,程序员有时会使用一个类型来代替另一个类型,以便观察特定行为并断言其实现是否正确。这个占位类型被称为测试替身。可以将其类比为电影制作中的特技替身,在拍摄特别棘手的场景时,一个人会代替演员完成任务。测试替身会在我们运行测试时代替其他类型。模拟对象是一种特定类型的测试替身,它会记录测试期间发生的事情,以便你可以断言正确的操作是否发生。
Rust 不像其他语言那样有对象,并且 Rust 的标准库中也没有像其他语言那样内置模拟对象功能。然而,你可以创建一个结构体来实现与模拟对象相同的目的。
我们要测试的场景是:创建一个库,该库跟踪一个值与最大值的接近程度,并根据当前值与最大值的接近程度发送消息。例如,这个库可以用于跟踪用户被允许进行的 API 调用次数配额。
我们的库只提供跟踪值与最大值接近程度以及在何时应该发送什么消息的功能。使用我们库的应用程序需要提供发送消息的机制,应用程序可以在应用程序中显示消息、发送电子邮件、发送短信或执行其他操作。库不需要知道这些细节,它只需要一个实现了我们提供的 `Messenger` 特征的类型。以下是库的代码:
```rust
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct L
```
0
0
复制全文
相关推荐










