Expand description
可共享的可变容器。
Rust 内存安全基于以下规则:给定一个对象 T
,它只能具有以下之一:
- 对对象具有多个不可变引用 (
&T
) (也称为别名)。 - 对对象有一个可变引用 (
&mut T
) (也称为可变性)。
这由 Rust 编译器强制执行。但是,在某些情况下,此规则不够灵活。有时需要对一个对象进行多次引用,然后对其进行可变的。
存在共享的可变容器以允许以受控的方式进行可变性,即使在出现混叠的情况下也是如此。
Cell<T>
、RefCell<T>
和 OnceCell<T>
允许以单线程方式实现这一点,但它们不实现 Sync
。
(如果您需要在多个线程之间进行别名和可变的,Mutex<T>
、RwLock<T>
、OnceLock<T>
或 atomic
类型是执行此操作的正确数据结构)。
Cell<T>
、RefCell<T>
和 OnceCell<T>
类型的值可以通过共享引用 (即常见的 &T
类型) 进行可变,而大多数 Rust 类型只能通过唯一的 (&mut T
) 引用进行可变。
我们说这些 cell 类型提供 内部可变性
(通过 &T
可变),与表现出 继承可变性
(仅通过 &mut T
可变) 的典型 Rust 类型形成对比。
Cell 类型分为三种类型: Cell<T>
、RefCell<T>
和 OnceCell<T>
。每个都提供了一种不同的方式来提供安全的内部可变性。
Cell<T>
Cell<T>
通过将值移入和移出 cell 来实现内部可变性。
也就是说,永远无法获取到内部值的 &mut T
,如果不将其替换为其他内容,则无法直接获取值本身。
这两条规则都确保永远不会有一个以上的引用指向内部值。
该类型提供了以下方法:
- 对于实现
Copy
的类型,get
方法通过复制它来检索当前内部值。 - 对于实现
Default
的类型,take
方法将当前内部值替换为Default::default()
,然后返回替换后的值。 - 所有类型都有:
replace
: 替换当前内部值并返回替换后的值。into_inner
: 此方法使用Cell<T>
并返回内部值。set
: 该方法替换内部值,丢弃替换后的值。
Cell<T>
通常用于更简单的类型,在这些类型中复制或移动值不会占用太多资源 (例如数字),并且在可能的情况下通常应优先于其他 cell 类型。
对于较大的非复制类型,RefCell
提供了一些优势。
RefCell<T>
RefCell<T>
使用 Rust 的生命周期来实现 “dynamic borrowing”,这是一个可以临时、独占、可更改访问内部值的过程。
RefCell<T>
s 的引用在运行时被跟踪,这与 Rust 的原生引用类型不同,后者在编译时完全静态地被跟踪。
可以使用 borrow
获取对 RefCell
的内部值 (&T
) 的不可更改引用,使用 borrow_mut
可以获取不可更改引用 (&mut T
)。
当调用这些函数时,它们首先验证是否满足 Rust 的借用规则: 允许任意数量的不可改变借用或允许单个不可改变借用,但绝不能同时使用。
如果尝试违反这些规则的借用,线程将崩溃。
RefCell<T>
对应的 Sync
版本为 RwLock<T>
。
OnceCell<T>
OnceCell<T>
在某种程度上是 Cell
和 RefCell
的混合体,适用于通常只需要设置一次的值。
这意味着无需移动或复制内部值 (与 Cell
不同) 也无需运行时检查 (与 RefCell
不同) 即可获得引用 &T
。
然而,它的值一旦设置也不能更新,除非您有一个可变引用到 OnceCell
。
OnceCell
提供了以下方法:
get
: 获取对内部值的引用set
: 如果未设置则设置内部值 (返回Result
)get_or_init
: 返回内部值,如果需要则初始化它get_mut
: 提供对内部值的可变引用,仅当您对 cell 本身具有可变引用时才可用。
OnceCell<T>
对应的 Sync
版本为 OnceLock<T>
。
何时选择内部可变性
更常见的继承的可变性 (其中必须具有对值的唯一访问权) 是使 Rust 能够强烈考虑指针别名的关键语言元素之一,从而可以静态地防止崩溃错误。 因此,首选继承的可变性,而内部可变性则是不得已而为之。 由于 cell 类型能够在不允许的情况下实现可变,所以有时内部可变性可能是合适的,或者甚至必须使用,例如
- 在不可变的内部引入可变性
- 逻辑上不可变的方法的实现细节。
Clone
的变异实现。
在不可变的内部引入可变性
许多共享的智能指针类型,包括 Rc<T>
和 Arc<T>
,都提供了可以在多方之间克隆和共享的容器。
由于所包含的值可能具有多重别名,因此只能使用 &
,而不能使用 &mut
来借用它们。
如果没有 cell,根本不可能改变这些智能指针内的数据。
然后,在共享指针类型中放置一个 RefCell<T>
来重新引入可变性是非常常见的:
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
fn main() {
let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
// 创建一个新块以限制动态借用的作用域
{
let mut map: RefMut<'_, _> = shared_map.borrow_mut();
map.insert("africa", 92388);
map.insert("kyoto", 11837);
map.insert("piccadilly", 11826);
map.insert("marbles", 38);
}
// 请注意,如果我们没有让缓存的上一次借用离开作用域,那么后续的借用将导致动态线程 panic。
//
// 这是使用 `RefCell` 的主要危险。
let total: i32 = shared_map.borrow().values().sum();
println!("{total}");
}
Run请注意,这个例子使用了 Rc<T>
而不是 Arc<T>
。RefCell<T>
s 适用于单线程场景。如果在多线程情况下需要共享可变性,可以考虑使用 RwLock<T>
或 Mutex<T>
。
逻辑上的不可变方法的实现细节
有时,可能希望不要在 API 中公开幕后发生了变异。
这可能是因为逻辑上该操作是不可变的,但是例如,缓存会强制实现执行变异; 或者是因为您必须使用突变来实现一个最初定义为接受 &self
的 trait 方法。
use std::cell::RefCell;
struct Graph {
edges: Vec<(i32, i32)>,
span_tree_cache: RefCell<Option<Vec<(i32, i32)>>>
}
impl Graph {
fn minimum_spanning_tree(&self) -> Vec<(i32, i32)> {
self.span_tree_cache.borrow_mut()
.get_or_insert_with(|| self.calc_span_tree())
.clone()
}
fn calc_span_tree(&self) -> Vec<(i32, i32)> {
// 昂贵的计算在这里
vec![]
}
}
RunClone
的变异实现
这只是一种特殊情况 - 但很常见 - 以前的情况:隐藏看起来不可变的操作的可变性。
clone
方法不会更改源值,并声明采用 &self
,而不是 &mut self
。
因此,在 clone
方法中发生的任何变异的都必须使用 cell 类型。
例如,Rc<T>
在 Cell<T>
中维护它的引用计数。
use std::cell::Cell;
use std::ptr::NonNull;
use std::process::abort;
use std::marker::PhantomData;
struct Rc<T: ?Sized> {
ptr: NonNull<RcBox<T>>,
phantom: PhantomData<RcBox<T>>,
}
struct RcBox<T: ?Sized> {
strong: Cell<usize>,
refcount: Cell<usize>,
value: T,
}
impl<T: ?Sized> Clone for Rc<T> {
fn clone(&self) -> Rc<T> {
self.inc_strong();
Rc {
ptr: self.ptr,
phantom: PhantomData,
}
}
}
trait RcBoxPtr<T: ?Sized> {
fn inner(&self) -> &RcBox<T>;
fn strong(&self) -> usize {
self.inner().strong.get()
}
fn inc_strong(&self) {
self.inner()
.strong
.set(self.strong()
.checked_add(1)
.unwrap_or_else(|| abort() ));
}
}
impl<T: ?Sized> RcBoxPtr<T> for Rc<T> {
fn inner(&self) -> &RcBox<T> {
unsafe {
self.ptr.as_ref()
}
}
}
RunStructs
- LazyCellExperimental在首次访问时初始化的值。
- SyncUnsafeCellExperimental
UnsafeCell
,但是Sync
。 RefCell::try_borrow
返回的错误。RefCell::try_borrow_mut
返回的错误。- 可变的内存位置。
- 一个 cell 只能写入一次。
- 在
RefCell
box 中将借用的引用括起来。 从RefCell<T>
不变借来的值的包装器类型。 - 具有动态检查借用规则的可变内存位置
- 从
RefCell<T>
可变借来的值的包装器类型。 - Rust 中内部可变性的核心原语。