当我可以使用Cell或RefCell时,应该选择哪一个?

62
std::cell文档中,我了解到Cell仅“与实现了Copy的类型兼容”。这意味着对于非Copy类型,我必须使用RefCell
当我确实有一个Copy类型时,是否有一种单元格类型优于另一种?我假设答案是“是”,否则两种类型都不存在!那么,使用其中一种类型的好处和权衡是什么?
下面是一个愚蠢的编造示例,它使用CellRefCell来完成相同的目标:
use std::cell::{Cell,RefCell};

struct ThingWithCell {
    counter: Cell<u8>,
}

impl ThingWithCell {
    fn new() -> ThingWithCell {
        ThingWithCell { counter: Cell::new(0) }
    }

    fn increment(&self) {
        self.counter.set(self.counter.get() + 1);
    }

    fn count(&self) -> u8 { self.counter.get() }
}

struct ThingWithRefCell {
    counter: RefCell<u8>,
}

impl ThingWithRefCell {
    fn new() -> ThingWithRefCell {
        ThingWithRefCell { counter: RefCell::new(0) }
    }

    fn increment(&self) {
        let mut counter = self.counter.borrow_mut();
        *counter = *counter + 1;
    }

    fn count(&self) -> u8 { *self.counter.borrow_mut() }
}


fn main() {
    let cell = ThingWithCell::new();
    cell.increment();
    println!("{}", cell.count());

    let cell = ThingWithRefCell::new();
    cell.increment();
    println!("{}", cell.count());
}
3个回答

65

我认为考虑CellRefCell之间的其他语义差异很重要:

  • Cell 提供值,而 RefCell 提供引用。
  • Cell 从不发生恐慌,RefCell 可能会导致恐慌。

让我们想象一种这些差异很重要的情况:

let cell = Cell::new(foo);
{
    let mut value = cell.get();
    // do some heavy processing on value
    cell.set(value);
}
在这种情况下,如果我们想象一些具有许多回调的复杂工作流程,并且cell是全局状态的一部分,那么"重型处理"的副作用可能会修改cell的内容,并且这些潜在的更改将在将value写回cell时丢失。
另一方面,使用RefCell的类似代码:
let cell = RefCell::new(foo);
{
    let mut_ref = cell.borrow_mut().unwrap();
    // do some heavy processing on mut_ref
}
在这种情况下,“重度处理”的任何对cell的修改都是被禁止的,否则将导致恐慌。因此,您可以确定在不使用mut_ref的情况下,cell的值不会改变。
我会根据它所持有的语义来决定使用哪一个,而不仅仅是Copy特质。如果两个都可以接受,则Cell比其他方式更轻便和安全,因此更可取。

2
你可以通过使用 RefCell 来获得值语义:克隆初始状态,对其进行操作,最后将修改后的状态写回单元格中。 - Matthieu M.
1
@MatthieuMпјҢдҪҶжҳҜжӮЁж— жі•дҪҝз”ЁCellиҺ·еҸ–еј•з”ЁиҜӯд№үпјҢеҜ№еҗ—пјҹ - Shepmaster
2
@Shepmaster:嗯,你可以有对Cell的引用。如果你想要一个值的引用,你可以使用它的UnsafeCell组成部分,而UnsafeCell可以给你一个指向引用值的指针...但这是不安全的(根据名称)。所以,不,Cell不是用来操作引用的;否则就不安全了,因为它会打破借用检查。 - Matthieu M.
3
@MatthieuM。确实,RefCell允许您在仅为Clone而不是Copy的数据上获得值语义,只要它没有被借用到其他地方。然而,它也允许您在Copy数据上强制引用语义,这正是我的观点,因为问题假设该类型是Copy - Levans

43

TL; DR: Cell适合需要频繁更改inner value的情况,RefCell则适合需要更灵活内部可变性的情况。


32

如果可以的话,您应该使用Cell

Cell根本不使用运行时检查。它所做的只是封装,禁止别名,并告诉编译器它是一个内部可变槽。在大多数情况下,它应该编译成与没有cell包装的类型完全相同的代码。

相比之下,RefCell使用简单的使用计数器来检查借用与可变借用,在运行时进行检查,如果您违反了可变借用的排他性,这个检查可能会导致运行时崩溃。可能的崩溃可能会影响优化。

至少还有一个区别。 Cell永远不会让您获得指向存储值本身的指针。因此,如果您需要这个,那么RefCell是唯一的选择。


4
它如何防止别名问题:它不允许你获取对“单元格(Cell)”中值的引用(因此根本没有指针)。最后一个问题很容易回答:是的,这取决于你自己。不安全块是一种显式的“信任程序员”的逃生口子。正如谚语所说,“unsafe”块不是用来破坏Rust的不变量的,而是要手动维护它们。 - bluss
1
这个问题只是我自己的感觉还是unsafe块的这个特定方面确实非常微妙? - Brian Cain
1
避免unsafe的许多原因。你要承担责任,另一个很好的理由是尚未完成对不安全代码块的要求规范化工作!这是一种信任程序员模式,在我们通常希望编译器检查我们工作的语言中。 - bluss
1
请注意,通常会通过一个指针进行写入,同时假定另一个指针不会更改指向的对象来推理别名。仅有指针相等性而没有任何写入操作并不是我们所考虑的。 - bluss
@bluss "至少还有一个区别。Cell 永远不会让你获取存储值本身的指针。因此,如果你需要这个,RefCell 是唯一的选择。"我认为这并不严格正确:Cell 上有一个 get_mut 方法,它返回一个 &mut T,但这取决于 Cell 本身是可变的,而这通常不是情况。RefCell 的文档强调了这一点,而 Cell 的文档没有。 - Eosis

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