Ответ 1
Не совсем корректно спрашивать, когда 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
in Rust. Следующий код полностью действителен:
{
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
На первый взгляд неясно, как с этим связана изменчивость, но напомним, что указатели с подсчетом ссылок называются так, потому что они содержат внутренний счетчик ссылок, который изменяется, когда ссылка дублируется (clone()
в Rust) и уничтожается ( выходит за рамки в Rust
). Следовательно, Rc
должен модифицировать себя, даже если он хранится в переменной non- mut
.
Это достигается за счет внутренней изменчивости. В стандартной библиотеке есть специальные типы, наиболее основными из которых являются UnsafeCell
, которые позволяют обходить правила внешней изменчивости и изменять что-либо, даже если это хранится (транзитивно) в переменной mut
non-.
Другой способ сказать, что что-то имеет внутреннюю изменчивость, это то, что это что-то может быть изменено с помощью &
-reference - то есть, если у вас есть значение типа &T
и вы можете изменить состояние T
которое оно указывает, то T
имеет внутренняя изменчивость.
Например, Cell
может содержать данные Copy
и может быть видоизменена, даже если она хранится в местоположении non- mut
:
let c: Cell<u32> = Cell::new(1);
c.set(2);
assert_eq!(c.get(), 2);
RefCell
может содержать non- данные Copy
и может &mut
указатели на все содержащиеся в нем значения, а отсутствие псевдонимов проверяется во время выполнения. Все это подробно объясняется на их страницах документации.
Как оказалось, в подавляющем числе ситуаций вы можете легко перейти только с внешней изменчивостью. Большая часть существующего высокоуровневого кода на Rust написана именно так. Однако иногда внутренняя изменчивость неизбежна или делает код намного понятнее. Один пример, реализация Rc
, уже описан выше. Другой случай, когда вам нужно общее изменяемое владение (то есть вам нужно получить доступ и изменить одно и то же значение из разных частей вашего кода) - это обычно достигается с помощью Rc<RefCell<T>>
, потому что это невозможно сделать только со ссылками. Еще одним примером является Arc<Mutex<T>>
, Mutex
- это другой тип внутренней изменчивости, который также безопасно использовать в разных потоках.
Итак, как видите, Cell
и RefCell
не являются заменой Rc
или Box
; они решают задачу предоставления вам изменчивости где-то там, где это не разрешено по умолчанию. Вы можете написать свой код, не используя их вообще; и если вы попадете в ситуацию, когда они вам понадобятся, вы об этом узнаете.
Cell
и RefCell
не являются кодовым запахом; единственная причина, по которой их называют "последним средством", заключается в том, что они переносят задачу проверки правил изменчивости и псевдонимов из компилятора в код времени выполнения, как в случае с RefCell
: вы не можете иметь два &mut
указывающие на одни и те же данные в то же время это статически обеспечивается компилятором, но с помощью RefCell
вы можете попросить тот же RefCell
дать вам столько &mut
RefCell
- за исключением того, что если вы сделаете это несколько раз, он будет паниковать на вас, применяя правила псевдонимов. во время выполнения. Паника, возможно, хуже, чем ошибки компиляции, потому что вы можете найти ошибки, вызывающие их только во время выполнения, а не во время компиляции. Однако иногда статический анализатор в компиляторе слишком ограничен, и вам действительно нужно "обойти" его.