28

什么时候需要使用Cell 或 RefCell?似乎有许多其他类型的选择可以代替这些,并且文档警告说使用RefCell是一个“最后的手段”。

使用这些类型是“代码气味”吗?谁能举一个例子,说明使用这些类型比使用其他类型更有意义,例如Rcor Box

4

3 回答 3

40

询问何时CellRefCell应该过度使用并不完全正确BoxRc因为这些类型解决了不同的问题。实际上,为了提供具有共享所有权的可变性,通常与一起RefCell使用。所以是的,用例完全取决于代码中的可变性要求。RcCellRefCell

内部和外部的可变性在官方的 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
}

然而,有时,继承的可变性是不够的。典型的例子是引用计数指针,Rc在 Rust 中调用。以下代码完全有效:

{
    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

乍一看,并不清楚可变性与此有何关系,但回想一下,引用计数指针之所以如此调用,是因为它们包含一个内部引用计数器,当引用被复制(clone()在 Rust 中)和销毁(退出范围Rust)。因此,即使它存储在非变量中,Rc 也必须修改自身。mut

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

另一种说某事物具有内部可变性的方式是,该事物可以通过&-reference 进行修改——也就是说,如果你有一个类型的值&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<RefCell<T>>,因为它不能单独使用引用来完成。甚至另一个例子是Arc<Mutex<T>>Mutex作为内部可变性的另一种类型,它也可以安全地跨线程使用。

因此,如您所见,CellandRefCell不是Rcor的替代品Box;他们解决了在默认情况下不允许的地方为您提供可变性的任务。您可以完全不使用它们来编写代码;如果您遇到需要它们的情况,您会知道的。

Cells 和RefCells 不是代码气味;将它们描述为“最后的手段”的唯一原因是它们将检查可变性和别名规则的任务从编译器转移到运行时代码,例如RefCell:你不能有两个&muts 指向相同的数据同时,这是由编译器静态强制执行的,但是使用RefCells 你可以要求它RefCell给你尽可能多&mut的 s - 除了如果你不止一次这样做,它会在你面前恐慌,在运行时强制执行别名规则. 恐慌可能比编译错误更糟糕,因为您只能在运行时而不是在编译时找到导致它们的错误。然而,有时编译器中的静态分析器过于严格,您确实需要“解决”它。

于 2015-06-14T16:46:53.007 回答
13

不,Cell也不RefCell是“代码气味”。通常,可变性是继承的,也就是说,当且仅当您对整个数据结构具有独占访问权时,您才能改变字段或数据结构的一部分,因此您可以选择在该级别使用可变性mut(即,foo.x 继承它的可变性或缺乏来自foo)。这是一个非常强大的模式,只要它运行良好就应该使用(令人惊讶的是经常如此)。但是对于所有地方的所有代码来说,它的表现力还不够。

BoxRc此无关。像几乎所有其他类型一样,它们尊重继承的可变性:Box如果您对 a 具有独占的、可变的访问权限,则可以改变 a 的内容Box(因为这意味着您也拥有对内容的独占访问权限)。相反,您永远无法获得&mutan 的内容,Rc因为其本质Rc上是共享的(即可以有多个Rcs 引用相同的数据)。

Cellor的一种常见情况RefCell是您需要在多个地方之间共享可变数据。&mut通常不允许对相同数据进行两次引用(这是有充分理由的!)。但是,有时您需要它,并且细胞类型可以安全地进行。

这可以通过 的常见组合来完成Rc<RefCell<T>>,只要有人使用它,数据就会一直存在,并允许每个人(但一次只能一个人!)对其进行变异。或者它可以很简单&Cell<i32>(即使单元格被包装在更有意义的类型中)。后者也常用于内部、私有、可变状态,如引用计数。

该文档实际上有几个示例说明您将在何处使用CellRefCell. 一个很好的例子实际上Rc就是它自己。创建新Rc的时,引用计数必须增加,但引用计数在所有Rcs 之间共享,因此,通过继承的可变性,这不可能工作。Rc实际上必须使用Cell.

一个好的指导方针是尝试在没有单元类型的情况下编写尽可能多的代码,但在没有它们的情况下使用它们会造成太大的伤害。在某些情况下,没有细胞也有很好的解决方案,并且根据经验,当您以前错过它们时,您将能够找到它们,但总会有一些事情没有它们是不可能的。

于 2015-06-14T16:36:40.790 回答
12

假设您想要或需要创建一些您选择的类型的对象并将其转储到Rc.

let x = Rc::new(5i32);

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

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

由于在 Rust 中,您可能永远不会对存在任何其他引用的内存位置进行可变引用,因此这些Rc容器永远不会被再次修改。

那么,如果您希望能够修改这些对象让多个对象Rc指向同一个对象,该怎么办?

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

回到我们原来的例子:

let x = Rc::new(RefCell::new(5i32));
let y = x.clone();

要获得对您的类型的可变引用,请borrow_mut使用RefCell.

let yval = x.borrow_mut();
*yval = 45;

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

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

于 2015-06-14T16:44:35.230 回答