【问题标题】:When I can use either Cell or RefCell, which should I choose?当我可以使用 Cell 或 RefCell 时,我应该选择哪个?
【发布时间】:2015-05-16 13:01:53
【问题描述】:

std::cell documentation,我看到Cell“仅与实现Copy 的类型兼容”。这意味着我必须将RefCell 用于非Copy 类型。

当我确实拥有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());
}

【问题讨论】:

    标签: rust


    【解决方案1】:

    我认为考虑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 的修改作为“繁重处理”的副作用都是被禁止的,并且会导致恐慌。因此,您可以确定cell 的值不会在不使用mut_ref 的情况下发生变化

    我会根据它所持有的值的语义来决定使用哪个,而不是简单地使用 Copy 特征。如果两者都可以接受,那么Cell 比另一个更轻、更安全,因此更可取。

    【讨论】:

    • 您可以使用RefCell 获得值语义:克隆初始状态,对其进行处理,然后将修改后的状态写回到最后的单元格中。
    • @MatthieuM。但是您无法使用Cell 获得参考语义,对吗?
    • @Shepmaster:嗯,你可以参考Cell。如果你想引用该值,你可以使用它的UnsafeCell 成分,UnsafeCell 可以给你一个指向引用值的指针......但它是不安全的(根据名称)。所以,不,Cell 不是用来操纵引用的;否则就不安全了,因为它会破坏借款支票。
    • @MatthieuM。实际上,RefCell 允许您获取只有 Clone 而不是 Copy 的数据的值语义,只要它没有在其他地方借用。但是,它还允许您在 Copy 数据上强制引用语义,这是我的观点,因为问题是假设类型是 Copy
    【解决方案2】:

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

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

    相比之下,RefCell 在运行时使用一个简单的使用计数器来检查借用与可变借用,如果您违反了可变借用的排他性,该检查可能会在运行时导致恐慌。可能的恐慌可能是优化的障碍。

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

    【讨论】:

    • 在哪里可以了解有关它如何禁止别名的更多信息?它可以用它来声明它为noalias吗? github.com/rust-lang/rust/issues/31681 似乎暗示了来自 &amp;mut 指针的相关功能。但是任何unsafe 都不能阻止潜在的别名吗?如果我写了一个unsafe 块,我是否要确保我不创建别名引用或取消引用任何在某处为Cell 别名的东西?
    • 它如何禁止别名:它不允许您获得对单元格 in 值的引用(因此根本没有指针)。最后一个问题很容易回答:是的,这取决于你,不安全的块是一个明确的“信任程序员”逃生口。俗话说unsafe块不是用来打破Rust的不变量,而是手动维护它们。
    • 是我自己还是 unsafe 块的责任的特定方面真的很微妙?
    • 不惜一切代价避免unsafe的原因有很多。您将自己置于对其中一个负责的位置,另一个很好的理由是,对不安全代码块的要求正式化的工作尚未完成!这是对程序员模式的信任,在我们通常希望编译器检查我们的工作的语言中。
    • 请注意,别名的原因通常是通过一个指针写入,而另一个指针假设指针对象不变。只是没有任何写入的指针相等不是我们所想的。
    【解决方案3】:

    TL; DRCell 尽可能。


    长答案CellRefCell 具有相似的名称,因为它们都允许内部可变性,但它们的用途不同:

    Cell

    它是T 的包装器,禁止一次多次共享它:您不能一成不变地借用内部数据。这个包装器没有任何开销,但是由于这个限制,你只能做以下操作:

    • 设置内部值,
    • 用其他东西交换内部值,
    • 复制内部值(仅当TCopyable 时)。

    由于其限制,Cell 的行为类似于独占借用,aka &amp;mut T。因此,更改内部值始终是安全的。总结一下:

    • 优势:没有开销
    • 优点:总是可变的
    • 限制:有些操作是不可能的

     RefCell

    它是T 的包装器,它“删除”了编译时借用检查:修改内部值的操作将&amp;self 共享引用到RefCell。通常,这是不安全的,但每个修改操作首先验证该值之前没有被借用。可变借用的排他性在运行时进行验证。

    总结一下:

    • 限制:非常小的开销
    • 限制:并不总是可变的,如果它以前可变地借用(请注意,在这种情况下某些操作可能会出现恐慌)
    • 优势:你可以做的操作不受限制

    你应该选择什么?

    优势和局限是互为镜像的。您的问题的答案是:如果Cell 的限制不打扰您,请使用它,因为除此之外,它只有优点。但是,如果您想要更灵活的内部可变性,请使用 RefCell

    【讨论】:

      猜你喜欢
      • 2019-01-15
      • 2015-08-30
      • 2010-10-24
      • 1970-01-01
      • 1970-01-01
      • 2010-11-24
      • 2019-07-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多