为什么在Rust中,Cell只能用于Copy类型而不能用于Clone类型?

17
Rust标准库文档指出,`Cell`只能用于`Copy`类型,并且在其他情况下应使用`RefCell`,但未说明为什么。通过研究`Cell`和`RefCell`的文档和代码,唯一看起来重要的地方是`Cell`的`get`函数。如果这个值是一个`Copy`类型,那么可以直接返回这样一个复制值。但是,为什么克隆不足够好呢?可以直接在`RefCell`上实现`set`函数。
fn set<T>(r: &RefCell<T>, v: T) {
    *r.borrow_mut() = v
}

这仅在没有其他人持有该值的引用时才有效。但如果该值可以被克隆,那么就可以这样做:
fn get<T: Clone>(r: &RefCell<T>) -> T {
    r.borrow().clone()
}

如果像Cell这样的类型与Clone类型一起使用,可以避免运行时借用检查的开销。我有什么遗漏吗?


1
可能是因为Clone涉及实际运行代码,并且在运行时可能会出现错误。 - DK.
3个回答

12

这是不可靠的。DK的评论是正确的,但你甚至不需要恐慌就可以造成混乱。一个有问题的场景如下:

  1. Cells(连同Option)允许创建循环,即自引用类型
  2. Clone实现获得&self引用
  3. 在存在循环的情况下,Clone实现可能访问正在被克隆的单元格
  4. 因此,在对象被克隆时,它可以覆盖自身,而同时具有对自身的普通借用(即&self
  5. 在借用时进行覆盖是不可靠的,因为它允许任意类型的切换和其他不良行为。例如,假设有一个最初为Ok(T)Result<T, E>字段,获取其中的T的引用并将Result覆盖为Err(R)。然后&T突然指向一个E值。
这个例子的功劳归功于Huon Wilson,详见用户.rust-lang.org线程为什么Cell需要Copy而不是Clone?。他的文章更深入地解释了限制的结构原因,并包含完整的代码示例。

啊,是的,我没有考虑到克隆访问单元本身的问题。而且我上面的set和get实现避免了这个问题,因为在get返回之前不能调用set。 - dth
3
直觉应该很简单。Rust 不允许同时共享和变异。 Cell 通过不提供对实际值的引用(无共享)来解决这个问题,但 .clone() 将取一个引用作为参数。 - bluss

2
这是我的观点,但我无法直接将其与存在这样限制的真正原因联系起来。
我认为复制是“廉价”的(例如复制少量位),而克隆则是“昂贵”的(例如进行函数调用或更改数据)。如果这样的单元使用Clone,那么每次使用(cell.get())都必须复制底层值。例如,使用CloneCell<Vec<T>>意味着每个cell.get()都需要调用内存分配器。那不是一个好主意。
因此,限制为Copy类型可能是引导人们远离自己伤害的一种方式。

是的,克隆访问几乎不可能比动态检查的借用更快,但我正在寻找一个设置,在那里事情确实会发生错误。 - dth

2
接受的答案仍然是完全正确的(并且很有趣),但我想提到一些额外的工具,这些工具在Rust 1.17中得到了Cell,不需要内容是Copy
  • Cell::swap 交换两个单元格的内容。
  • Cell::replace 将新值放入单元格并返回旧值。
  • Cell::take 类似于replace,使用Default::default()的值。

请注意,在这里与mem::swapmem::replacemem::take之间存在紧密的并行关系。(尽管实际上最后一个直到Rust 1.40才稳定下来。) Cell方法实际上执行相同的操作,但它们通过共享引用工作,而不需要可变引用。

对于实现Default的类型,我们可以使用Cell::take来实现与.clone()非常相似的功能,只需要多做几步:

fn clone_from_cell<T>(cell: &Cell<T>) -> T
where
    T: Clone + Default,
{
    let val: T = cell.take();
    let clone: T = val.clone();
    cell.set(val);
    clone
}

对于实现了Clone但没有实现Default的类型(这种情况比较少见,例如NonZeroU32),请注意Option<T>无论T是什么都实现了Default,因此可以使用该函数对任何TCell<Option<T>>进行“克隆”。


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