使用Cell或RefCell的最佳选择情况

40
当您需要使用Cell或RefCell时是什么情况?似乎有很多其他类型的选择可以替代它们,而文档警告使用RefCell是一种"最后的手段"。
使用这些类型是一种"代码异味"吗?是否有人可以展示一个例子,说明使用这些类型比使用其他类型(如Rc甚至Box)更合适?

2
RcBox解决不同类别的问题:当对象的大小未知或太大无法内联存储时,它们被使用;而CellRefCell提供内部可变性,以便绕过继承的可变性。 - Francis Gagné
1
@FrancisGagné 我对“继承可变性”是什么意思或为什么它重要或是问题有点不清楚。你能澄清一下吗? - jocull
检查一下在一个好的代码库中它们被使用的所有地方,比如编译器本身,怎么样? - Chayim Friedman
我认为Rc<RefCell<T>>是一种代码异味,是的。我不会直接称RcRefCell本身有异味,但它们经常一起使用,而且我认为大多数情况下,有更好的方法可以替代它们的使用。如果你的程序状态不需要成为一个完整的图形结构,那么最好将其重构为一个简单的拥有关系树。但如果你的程序状态确实是一个图形结构,那么它可能包含循环引用,而Rc<RefCell<T>>会导致内存泄漏和恐慌。请参考https://jacko.io/object_soup.html。 - undefined
3个回答

53

询问何时应该使用CellRefCell而不是BoxRc并不完全正确,因为这些类型解决了不同的问题。实际上,更多时候RefCellRc一起使用,以提供共享所有权的可变性。因此,是的,CellRefCell的用例完全取决于代码中的可变性要求。

内部和外部可变性在官方Rust书籍中的指定可变性章节中非常好地解释了。外部可变性与所有权模型密切相关,大多数情况下,当我们说某个东西是可变的或不可变的时,我们指的是外部可变性。外部可变性的另一个名称是继承可变性,这可能更清楚地解释了这个概念:这种可变性由数据的所有者定义,并继承到您可以从所有者访问的所有内容。例如,如果您的结构类型变量是可变的,则变量中结构的所有字段也是可变的:

struct Point { x: u32, y: u32 }

// the variable is mutable...
let mut p = Point { x: 10, y: 20 };
// ...and so are fields reachable through this variable
p.x = 11;
p.y = 22;

let q = Point { x: 10, y: 20 };
q.x = 33;  // compilation error

继承的可变性还定义了您可以从值中获取哪些类型的引用:

{
    let px: &u32 = &p.x;  // okay
}
{
    let py: &mut u32 = &mut p.x;  // okay, because p is mut
}
{
    let qx: &u32 = &q.x;  // okay
}
{
    let qy: &mut u32 = &mut q.y;  // compilation error since q is not mut
}

有时候,继承的可变性是不够的。经典的例子是引用计数指针,在 Rust 中称为 Rc。以下代码完全有效:
{
    let x1: Rc<u32> = Rc::new(1);
    let x2: Rc<u32> = x1.clone();  // create another reference to the same data
    let x3: Rc<u32> = x2.clone();  // even another
}  // here all references are destroyed and the memory they were pointing at is deallocated

乍一看似乎不清楚可变性与此有何关系,但请记得引用计数指针被称为这样是因为它们包含一个内部的引用计数器,在引用被复制(在 Rust 中是 clone())和销毁(超出 Rust 的范围)时会发生修改。因此,即使存储在非 mut 变量中,Rc 也必须自我修改。

这是通过内部可变性实现的。标准库中有特殊类型,其中最基本的类型是 UnsafeCell,它允许我们绕过外部可变性规则并改变某些东西的值,即使它(间接地)存储在非 mut 变量中。

另一种表达具有内部可变性的方式是,可以通过 &-引用修改其内容 - 如果您有一个类型为 &T 的值,并且可以修改其指向的 T 的状态,则 T 具有内部可变性。

例如,Cell 可以包含 Copy 数据,即使它存储在非 mut 位置也可以进行改变:
let c: Cell<u32> = Cell::new(1);
c.set(2);
assert_eq!(c.get(), 2);

RefCell 可以包含非 Copy 数据,并且可以为其包含的值提供 &mut 指针,在运行时检查别名是否存在。这些都在它们的文档页面上详细解释。


事实证明,在绝大多数情况下,您只需要使用外部可变性即可。Rust 中现有的大多数高级代码都是这样编写的。然而,有时不可避免地需要使用内部可变性或者使用它可以使代码更加清晰。一个例子是上面已经描述过的 Rc 实现。另一个例子是当您需要共享可变所有权(也就是说,您需要从代码的不同部分访问和修改相同的值)时,通常会使用 Rc>,因为仅使用引用无法完成此操作。甚至另一个例子是 Arc>,其中 Mutex 是另一种用于内部可变性的类型,也可以安全地跨线程使用。
因此,正如您所看到的,Cell 和 RefCell 并不是 Rc 或 Box 的替代品;它们解决了在默认情况下不允许可变性的情况下提供可变性的任务。您可以完全不使用它们编写代码;如果您陷入需要使用它们的情况,您将会知道。

CellRefCell并不是代码异味; 它们被描述为“最后的手段”的唯一原因是它们将检查可变性和别名规则的任务从编译器移动到运行时代码中,正如RefCell的情况:你不能同时有两个指向相同数据的&mut,这是由编译器静态强制执行的,但是对于RefCell,你可以要求同一个RefCell给你尽可能多的&mut - 除非你这样做超过一次,否则它会在运行时产生panic,并强制执行别名规则。Panic比编译错误更糟糕,因为你只能在运行时而不是在编译时找到导致它们的错误。然而,有时编译器中的静态分析器过于严格,确实需要“解决”它。


10
重温可变性章节对此很有帮助。需要从中汲取的重要部分是 Cell / RefCell 允许您“模拟字段级可变性”。这类似于将结构体的字段标记为 mut,如果可能的话。感谢提供详细答案、示例和相关文档链接! - jocull

15
不,CellRefCell不是“代码异味”。通常情况下,可变性是继承的,也就是说,只有当你拥有整个数据结构的独占访问权时,才能对字段或数据结构的一部分进行突变,因此你可以使用mut在该级别上选择可变性(即foo.xfoo继承其可变性或缺乏可变性)。这是一个非常强大的模式,应该在适当的时候使用它(这种情况出现得惊人地频繁)。但它并不足以表达所有的代码。 BoxRc与此无关。像几乎所有其他类型一样,它们尊重继承的可变性:如果你拥有Box的独占可变访问权限(因为这意味着你也拥有内容的独占访问权限),那么你可以突变Box的内容。相反,你永远无法获得指向Rc内容的&mut,因为按其本质Rc是共享的(即可以有多个Rc引用相同的数据)。

CellRefCell 的一个常见用例是您需要在几个地方之间共享可变数据。通常不允许同时拥有两个对同一数据的 &mut 引用(这是有很好的原因的!)。然而,有时候你需要它,并且 cell 类型可以安全地实现它。

这可以通过常见的组合 Rc<RefCell<T>> 来实现,该组合允许数据在任何人使用它时保留,并允许每个人(但仅限一个人)对其进行更改。或者它可能像 &Cell<i32> 一样简单(即使 cell 包装在更有意义的类型中)。后者也常用于像引用计数这样的内部、私有、可变状态。

文档实际上有几个例子,说明何时使用CellRefCell。一个很好的例子实际上是Rc本身。创建新的Rc时,必须增加引用计数,但引用计数在所有Rc之间共享,因此通过继承可变性,这不可能起作用。Rc实际上必须使用Cell

一个好的指导方针是尽可能多地编写代码,而不使用cell类型,但在没有它们时会感到痛苦时使用它们。在某些情况下,有一种好的解决方案可以不使用cells,并且通过经验,您将能够找到以前错过的那些,但总会有一些事情是不可能没有它们的。


14
假设你想要或者需要创建某种自定义类型的对象,并将其转储到一个 Rc 中。
let x = Rc::new(5i32);

现在,您可以轻松地创建另一个指向完全相同对象和内存位置的Rc

let y = x.clone();
let yval: i32 = *y;

在Rust中,对于任何存在其他引用的内存位置,你都不能拥有可变引用,因此这些 Rc 容器将永远无法再次被修改。

那么,如果您想要能够修改这些对象并且有多个指向同一个对象的 Rc 怎么办?

这就是 CellRefCell 解决的问题。解决方案称为“内部可变性”,这意味着Rust的别名规则在运行时而不是编译时执行。

回到我们最初的例子:

let x = Rc::new(RefCell::new(5i32));
let y = x.clone();
为了获得您的类型的可变引用,您可以在RefCell上使用borrow_mut。
let yval = x.borrow_mut();
*yval = 45;

如果您已经以可变或不可变的方式借用了Rc指向的值,则borrow_mut函数将会出现panic,并因此强制执行Rust的别名规则。

Rc<RefCell<T>>只是RefCell的一个示例,还有许多其他合法的用途。但是文档是正确的。如果有其他方式,请使用其他方式,因为编译器无法帮助您推理RefCell


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接