Cell 或 RefCell 是最佳选择的情况
Posted
技术标签:
【中文标题】Cell 或 RefCell 是最佳选择的情况【英文标题】:Situations where Cell or RefCell is the best choice 【发布时间】:2015-08-30 02:44:57 【问题描述】:您什么时候需要使用Cell or RefCell?似乎有许多其他类型选择可以代替这些,并且文档警告说使用 RefCell
有点“不得已”。
使用这些类型是“code smell”吗?谁能举例说明使用这些类型比使用其他类型更有意义,例如Rc
甚至Box
?
【问题讨论】:
Rc
和 Box
解决不同类别的问题:当对象的大小未知或太大而无法内联存储时使用它们,而 Cell
和 RefCell
提供内部可变性, 为了解决继承的可变性。
@FrancisGagné 我有点不清楚“继承的可变性”是什么意思,或者为什么它很重要或有问题。你能澄清一下吗?
【参考方案1】:
询问何时应该使用Cell
或RefCell
而不是Box
和Rc
并不完全正确,因为这些类型解决了不同的问题。事实上,RefCell
经常与Rc
一起一起使用,以提供具有共享所有权的可变性。所以是的,Cell
和 RefCell
的用例完全取决于代码中的可变性要求。
Rust 官方书籍designated chapter on mutability 很好地解释了内部和外部可变性。外部可变性与所有权模型密切相关,当我们说某事物是可变的或不可变的时,我们通常指的是外部可变性。外部可变性的另一个名称是inherited可变性,这可能更清楚地解释了这个概念:这种可变性由数据的所有者定义,并继承到您可以从所有者那里获得的所有内容。例如,如果您的结构类型变量是可变的,那么变量中结构的所有字段也是可变的:
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
的范围)。因此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
是另一种内部可变性类型,也可以安全地跨线程使用。
因此,如您所见,Cell
和 RefCell
不是 Rc
或 Box
的替代品;他们解决了在默认情况下不允许的地方为您提供可变性的任务。您可以完全不使用它们来编写代码;如果您遇到需要它们的情况,您会知道的。
Cell
s 和 RefCell
s 不是代码气味;将它们描述为“最后手段”的唯一原因是它们将检查可变性和别名规则的任务从编译器转移到运行时代码,例如 RefCell
:你不能有两个 &mut
s同时指向相同的数据,这是由编译器静态强制执行的,但是使用RefCell
s,您可以要求相同的RefCell
给您尽可能多的&mut
s - 除非您这样做它不止一次会惊慌失措,在运行时强制执行别名规则。恐慌可能比编译错误更糟糕,因为您只能在运行时而不是在编译时找到导致它们的错误。然而,有时编译器中的静态分析器过于严格,您确实需要“解决”它。
【讨论】:
关于可变性的章节是一件值得重新审视的好东西。从中汲取的重要部分是Cell
/ RefCell
允许您“模拟字段级可变性”。如果可能的话,这类似于将结构的字段标记为mut
。感谢您提供详细的答案、示例和相关文档链接!【参考方案2】:
不,Cell
和 RefCell
不是“代码气味”。通常,可变性是继承的,也就是说,当且仅当您对整个数据结构具有独占访问权时,您才能改变一个字段或数据结构的一部分,因此您可以在以下位置选择可变性与mut
处于同一水平(即foo.x
继承它的可变性或缺乏foo
)。这是一个非常强大的模式,只要它运行良好就应该使用(这令人惊讶地经常出现)。但它对所有代码的表现力都不够。
Box
和 Rc
与此无关。像几乎所有其他类型一样,它们尊重继承的可变性:如果您对 Box
具有独占、可变的访问权限,则可以更改 Box
的内容(因为这意味着您也可以独占访问内容)。相反,您永远无法将&mut
获取到Rc
的内容,因为Rc
本质上是共享的(即可以有多个Rc
s 引用相同的数据)。
Cell
或RefCell
的一个常见情况是您需要在多个位置之间共享可变数据。通常不允许有两个&mut
引用相同的数据(这是有充分理由的!)。但是,有时您需要它,并且细胞类型可以安全地进行。
这可以通过Rc<RefCell<T>>
的通用组合来完成,只要有人使用它,数据就会一直存在,并允许每个人(但一次只能一个人!)对其进行变异。或者它可以像&Cell<i32>
一样简单(即使单元格被包装在更有意义的类型中)。后者也常用于内部、私有、可变状态,如引用计数。
文档实际上有几个示例,说明您可以在哪里使用 Cell
或 RefCell
。一个很好的例子实际上是Rc
本身。在创建新的Rc
时,必须增加引用计数,但引用计数在所有Rc
s 之间共享,因此,通过继承的可变性,这不可能工作。 Rc
实际上已经使用Cell
。
一个好的指导方针是尝试编写尽可能多的代码而不使用单元格类型,但如果没有单元格类型会造成太大的伤害,请使用它们。在某些情况下,没有细胞也有很好的解决方案,并且根据经验,当您以前错过它们时,您将能够找到它们,但总会有一些事情没有它们是不可能的。
【讨论】:
【参考方案3】:假设您想要或需要创建一些您选择的类型的对象并将其转储到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();
要获得对您的类型的可变引用,请在 RefCell
上使用 borrow_mut
。
let yval = x.borrow_mut();
*yval = 45;
如果您已经借用了 Rc
s 指向的可变或非可变值,borrow_mut
函数将出现恐慌,因此会强制执行 Rust 的别名规则。
Rc<RefCell<T>>
只是RefCell
的一个例子,还有许多其他合法用途。但是文档是正确的。如果有其他方法,请使用它,因为编译器无法帮助您推理RefCell
s。
【讨论】:
以上是关于Cell 或 RefCell 是最佳选择的情况的主要内容,如果未能解决你的问题,请参考以下文章
如何在不破坏封装的情况下返回对 RefCell 内某些内容的引用?
Rayon 如何防止线程之间使用 RefCell<T>、Cell<T> 和 Rc<T>?
是否有替代方法或方法让 Rc<RefCell<X>> 限制 X 的可变性?