询问何时应该使用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 的替代品;他们解决了在默认情况下不允许的地方为您提供可变性的任务。您可以完全不使用它们来编写代码;如果您遇到需要它们的情况,您会知道的。
Cells 和 RefCells 不是代码气味;将它们描述为“最后手段”的唯一原因是它们将检查可变性和别名规则的任务从编译器转移到运行时代码,例如 RefCell:你不能有两个 &muts同时指向相同的数据,这是由编译器静态强制执行的,但是使用RefCells,您可以要求相同的RefCell 给您尽可能多的&muts - 除非您这样做它不止一次会惊慌失措,在运行时强制执行别名规则。恐慌可能比编译错误更糟糕,因为您只能在运行时而不是在编译时找到导致它们的错误。然而,有时编译器中的静态分析器过于严格,您确实需要“解决”它。