Ситуации, когда Cell или RefCell - лучший выбор

Когда вам понадобится использовать Cell или RefCell? Похоже, есть много других вариантов выбора, которые были бы подходящими вместо них, и в документации предупреждается, что использование RefCell немного "последнее средство".

Использует эти типы " запах кода"? Может ли кто-нибудь показать пример, где использование этих типов имеет больше смысла, чем использование другого типа, например Rc или даже Box?

Ответ 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 - за исключением того, что если вы сделаете это несколько раз, он будет паниковать на вас, применяя правила псевдонимов. во время выполнения. Паника, возможно, хуже, чем ошибки компиляции, потому что вы можете найти ошибки, вызывающие их только во время выполнения, а не во время компиляции. Однако иногда статический анализатор в компиляторе слишком ограничен, и вам действительно нужно "обойти" его.

Ответ 2

Нет, Cell и RefCell не являются "запахами кода". Обычно изменчивость наследуется, то есть вы можете мутировать поле или часть структуры данных тогда и только тогда, когда у вас есть эксклюзивный доступ ко всей структуре данных, и, следовательно, вы можете выбрать изменчивость на этом уровне с помощью mut ( т.е. foo.x наследует свою изменчивость или ее отсутствие от foo). Это очень мощный шаблон и должен использоваться, когда он работает хорошо (что на удивление часто). Но это не достаточно выразительно для всего кода во всем мире.

Box и Rc не имеют к этому никакого отношения. Как почти все другие типы, они уважают унаследованную изменчивость: вы можете мутировать содержимое Box, если у вас есть эксклюзивный, изменяемый доступ к Box (потому что это означает, что вы также имеете эксклюзивный доступ к содержимому). И наоборот, вы никогда не сможете получить &mut содержимое Rc, поскольку по своей природе Rc является общим (т.е. Может быть несколько Rc, ссылающихся на одни и те же данные).

Один общий случай 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();

Чтобы получить изменяемую ссылку на ваш тип, вы используете borrow_mut на RefCell.

let yval = x.borrow_mut();
*yval = 45;

Если вы уже заимствовали значение, которое ваш Rc указывает на то, что он изменен или неравномерен, функция borrow_mut будет паниковать и, следовательно, будет применять правила псевдонимов Rust.

Rc<RefCell<T>> является всего лишь одним примером для RefCell, существует много других законных целей. Но документация правильная. Если есть другой способ, используйте его, потому что компилятор не может помочь вам рассуждать о RefCell s.