RefCell
是一种"最后的手段"。使用这些类型是一种"代码异味"吗?是否有人可以展示一个例子,说明使用这些类型比使用其他类型(如
Rc
甚至Box
)更合适?RefCell
是一种"最后的手段"。Rc
甚至Box
)更合适?询问何时应该使用Cell
或RefCell
而不是Box
和Rc
并不完全正确,因为这些类型解决了不同的问题。实际上,更多时候RefCell
与Rc
一起使用,以提供共享所有权的可变性。因此,是的,Cell
和RefCell
的用例完全取决于代码中的可变性要求。
内部和外部可变性在官方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
。以下代码完全有效:{
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 的范围)时会发生修改。因此,即使存储在非 mut
变量中,Rc
也必须自我修改。
这是通过内部可变性实现的。标准库中有特殊类型,其中最基本的类型是 UnsafeCell
,它允许我们绕过外部可变性规则并改变某些东西的值,即使它(间接地)存储在非 mut
变量中。
另一种表达具有内部可变性的方式是,可以通过 &
-引用修改其内容 - 如果您有一个类型为 &T
的值,并且可以修改其指向的 T
的状态,则 T
具有内部可变性。
Cell
可以包含 Copy
数据,即使它存储在非 mut
位置也可以进行改变:let c: Cell<u32> = Cell::new(1);
c.set(2);
assert_eq!(c.get(), 2);
RefCell
可以包含非 Copy
数据,并且可以为其包含的值提供 &mut
指针,在运行时检查别名是否存在。这些都在它们的文档页面上详细解释。
Cell
和RefCell
并不是代码异味; 它们被描述为“最后的手段”的唯一原因是它们将检查可变性和别名规则的任务从编译器移动到运行时代码中,正如RefCell
的情况:你不能同时有两个指向相同数据的&mut
,这是由编译器静态强制执行的,但是对于RefCell
,你可以要求同一个RefCell
给你尽可能多的&mut
- 除非你这样做超过一次,否则它会在运行时产生panic,并强制执行别名规则。Panic比编译错误更糟糕,因为你只能在运行时而不是在编译时找到导致它们的错误。然而,有时编译器中的静态分析器过于严格,确实需要“解决”它。
Cell
/ RefCell
允许您“模拟字段级可变性”。这类似于将结构体的字段标记为 mut
,如果可能的话。感谢提供详细答案、示例和相关文档链接! - jocullCell
和RefCell
不是“代码异味”。通常情况下,可变性是继承的,也就是说,只有当你拥有整个数据结构的独占访问权时,才能对字段或数据结构的一部分进行突变,因此你可以使用mut
在该级别上选择可变性(即foo.x
从foo
继承其可变性或缺乏可变性)。这是一个非常强大的模式,应该在适当的时候使用它(这种情况出现得惊人地频繁)。但它并不足以表达所有的代码。
Box
和Rc
与此无关。像几乎所有其他类型一样,它们尊重继承的可变性:如果你拥有Box
的独占可变访问权限(因为这意味着你也拥有内容的独占访问权限),那么你可以突变Box
的内容。相反,你永远无法获得指向Rc
内容的&mut
,因为按其本质Rc
是共享的(即可以有多个Rc
引用相同的数据)。
Cell
或 RefCell
的一个常见用例是您需要在几个地方之间共享可变数据。通常不允许同时拥有两个对同一数据的 &mut
引用(这是有很好的原因的!)。然而,有时候你需要它,并且 cell 类型可以安全地实现它。
这可以通过常见的组合 Rc<RefCell<T>>
来实现,该组合允许数据在任何人使用它时保留,并允许每个人(但仅限一个人)对其进行更改。或者它可能像 &Cell<i32>
一样简单(即使 cell 包装在更有意义的类型中)。后者也常用于像引用计数这样的内部、私有、可变状态。
文档实际上有几个例子,说明何时使用Cell
或RefCell
。一个很好的例子实际上是Rc
本身。创建新的Rc
时,必须增加引用计数,但引用计数在所有Rc
之间共享,因此通过继承可变性,这不可能起作用。Rc
实际上必须使用Cell
。
一个好的指导方针是尽可能多地编写代码,而不使用cell类型,但在没有它们时会感到痛苦时使用它们。在某些情况下,有一种好的解决方案可以不使用cells,并且通过经验,您将能够找到以前错过的那些,但总会有一些事情是不可能没有它们的。
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
指向的值,则borrow_mut
函数将会出现panic,并因此强制执行Rust的别名规则。
Rc<RefCell<T>>
只是RefCell
的一个示例,还有许多其他合法的用途。但是文档是正确的。如果有其他方式,请使用其他方式,因为编译器无法帮助您推理RefCell
。
Rc
和Box
解决不同类别的问题:当对象的大小未知或太大无法内联存储时,它们被使用;而Cell
和RefCell
提供内部可变性,以便绕过继承的可变性。 - Francis GagnéRc<RefCell<T>>
是一种代码异味,是的。我不会直接称Rc
和RefCell
本身有异味,但它们经常一起使用,而且我认为大多数情况下,有更好的方法可以替代它们的使用。如果你的程序状态不需要成为一个完整的图形结构,那么最好将其重构为一个简单的拥有关系树。但如果你的程序状态确实是一个图形结构,那么它可能包含循环引用,而Rc<RefCell<T>>
会导致内存泄漏和恐慌。请参考https://jacko.io/object_soup.html。 - undefined