RefCell<T> и паттерн внутренней изменяемости
Внутренняя изменяемость — это паттерн проектирования в Rust, который позволяет изменять данные даже при наличии неизменяемых ссылок на эти данные; обычно это действие запрещено правилами заимствования. Для изменения данных паттерн использует небезопасный код внутри структуры данных, чтобы обойти обычные правила Rust, регулирующие изменение и заимствование. Небезопасный код указывает компилятору, что мы проверяем правила вручную, а не полагаемся на компилятор; мы обсудим небезопасный код подробнее в Главе 20.
Мы можем использовать типы, которые следуют паттерну внутренней изменяемости, только когда можем гарантировать, что правила заимствования будут соблюдены во время выполнения, даже если компилятор не может этого гарантировать. Затем небезопасный код оборачивается в безопасный API, и внешний тип остаётся неизменяемым.
Давайте изучим эту концепцию на примере типа RefCell<T>, который следует паттерну внутренней изменяемости.
Принудительное соблюдение правил заимствования во время выполнения с помощью RefCell<T>
В отличие от Rc<T>, тип RefCell<T> представляет собой единоличное владение данными, которые он хранит. Так в чём же разница между RefCell<T> и типом вроде Box<T>? Вспомните правила заимствования из Главы 4:
- В любой момент времени у вас может быть либо одна изменяемая ссылка, либо любое количество неизменяемых ссылок (но не одновременно).
- Ссылки всегда должны быть действительными.
Со ссылками и Box<T> инварианты правил заимствования проверяются на этапе компиляции. С RefCell<T> эти инварианты проверяются во время выполнения. Со ссылками, если вы нарушите эти правила, вы получите ошибку компиляции. С RefCell<T>, если вы нарушите эти правила, ваша программа аварийно завершится (panic).
Преимущества проверки правил заимствования на этапе компиляции в том, что ошибки будут обнаружены раньше в процессе разработки, и нет влияния на производительность во время выполнения, так как весь анализ завершён заранее. По этим причинам проверка правил заимствования на этапе компиляции является лучшим выбором в большинстве случаев, поэтому это поведение Rust по умолчанию.
Преимущество проверки правил заимствования во время выполнения вместо этого в том, что тогда разрешаются определённые безопасные с точки зрения памяти сценарии, которые были бы запрещены проверками на этапе компиляции. Статический анализ, подобный компилятору Rust, по своей природе консервативен. Некоторые свойства кода невозможно обнаружить путём анализа кода: самый известный пример — проблема остановки, которая выходит за рамки этой книги, но является интересной темой для исследования.
Поскольку некоторый анализ невозможен, если компилятор Rust не может быть уверен, что код соответствует правилам владения, он может отклонить правильную программу; таким образом, он консервативен. Если бы Rust принял неправильную программу, пользователи не смогли бы доверять гарантиям Rust. Однако, если Rust отклоняет правильную программу, программисту будет неудобно, но ничего катастрофического не произойдёт. Тип RefCell<T> полезен, когда вы уверены, что ваш код следует правилам заимствования, но компилятор не может понять и гарантировать это.
Подобно Rc<T>, RefCell<T> предназначен только для использования в однопоточных сценариях и выдаст ошибку компиляции, если вы попытаетесь использовать его в многопоточном контексте. Мы поговорим о том, как получить функциональность RefCell<T> в многопоточной программе, в Главе 16.
Вот краткое резюме причин выбора Box<T>, Rc<T> или RefCell<T>:
Rc<T>позволяет нескольким владельцам одних и тех же данных;Box<T>иRefCell<T>имеют единоличных владельцев.Box<T>позволяет неизменяемые или изменяемые заимствования, проверяемые на этапе компиляции;Rc<T>позволяет только неизменяемые заимствования, проверяемые на этапе компиляции;RefCell<T>позволяет неизменяемые или изменяемые заимствования, проверяемые во время выполнения.- Поскольку
RefCell<T>позволяет изменяемые заимствования, проверяемые во время выполнения, вы можете изменять значение внутриRefCell<T>, даже когда самRefCell<T>является неизменяемым.
Изменение значения внутри неизменяемого значения — это паттерн внутренней изменяемости. Давайте рассмотрим ситуацию, в которой внутренняя изменяемость полезна, и изучим, как это возможно.
Внутренняя изменяемость: изменяемое заимствование для неизменяемого значения
Следствием правил заимствования является то, что когда у вас есть неизменяемое значение, вы не можете заимствовать его изменяемым. Например, этот код не скомпилируется:
fn main() {
let x = 5;
let y = &mut x;
}
Если бы вы попытались скомпилировать этот код, вы получили бы следующую ошибку:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Однако есть ситуации, в которых было бы полезно, чтобы значение изменяло себя в своих методах, но казалось неизменяемым для другого кода. Код за пределами методов значения не смог бы изменять значение. Использование RefCell<T> — один из способов получить возможность внутренней изменяемости, но RefCell<T> не обходит правила заимствования полностью: проверка заимствования в компиляторе разрешает эту внутреннюю изменяемость, и правила заимствования проверяются во время выполнения вместо этого. Если вы нарушите правила, вы получите panic! вместо ошибки компиляции.
Давайте разберём практический пример, где мы можем использовать RefCell<T> для изменения неизменяемого значения, и посмотрим, почему это полезно.
Сценарий использования внутренней изменяемости: Mock-объекты
Иногда во время тестирования программист использует один тип вместо другого, чтобы наблюдать за определённым поведением и убедиться, что оно реализовано правильно. Этот тип-заменитель называется тестовым дублёром (test double). Подумайте об этом в смысле дублёра трюков в кинематографе, где человек подменяет актёра, чтобы выполнить особенно сложную сцену. Тестовые дублёры подменяют другие типы, когда мы запускаем тесты. Mock-объекты — это конкретные типы тестовых дублёров, которые записывают, что происходит во время теста, чтобы вы могли утверждать, что правильные действия были выполнены.
У Rust нет объектов в том же смысле, что у других языков, и в стандартной библиотеке Rust нет встроенной функциональности mock-объектов, как в некоторых других языках. Однако вы certainly можете создать структуру, которая будет служить теми же целями, что и mock-объект.
Вот сценарий, который мы будем тестировать: мы создадим библиотеку, которая отслеживает значение относительно максимального значения и отправляет сообщения в зависимости от того, насколько текущее значение близко к максимальному. Эта библиотека могла бы использоваться для отслеживания квоты пользователя на количество разрешённых вызовов API, например.
Наша библиотека будет предоставлять только функциональность отслеживания того, насколько значение близко к максимуму, и какие сообщения должны быть в какие моменты. Приложения, использующие нашу библиотеку, должны будут предоставить механизм для отправки сообщений: приложение может вывести сообщение в интерфейсе, отправить email, отправить текстовое сообщение или сделать что-то ещё. Библиотеке не нужно знать эту деталь. Ей нужно только нечто, реализующее типаж, который мы предоставим, под названием Messenger. Листинг 15-20 показывает код библиотеки.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Одна важная часть этого кода в том, что типаж Messenger имеет один метод под названием send, который принимает неизменяемую ссылку на self и текст сообщения. Этот типаж — интерфейс, который наш mock-объект должен реализовать, чтобы его можно было использовать так же, как реальный объект. Другая важная часть в том, что мы хотим протестировать поведение метода set_value на LimitTracker. Мы можем изменить то, что передаём в параметр value, но set_value ничего не возвращает, на что мы могли бы делать утверждения. Мы хотим иметь возможность сказать, что если мы создаём LimitTracker с чем-то, что реализует типаж Messenger, и определённым значением для max, когда мы передаём разные числа для value, messenger получает команду отправить соответствующие сообщения.
Нам нужен mock-объект, который вместо отправки email или текстового сообщения, когда мы вызываем send, будет только отслеживать сообщения, которые ему сказано отправить. Мы можем создать новый экземпляр mock-объекта, создать LimitTracker, который использует mock-объект, вызвать метод set_value на LimitTracker, а затем проверить, что у mock-объекта есть ожидаемые сообщения. Листинг 15-21 показывает попытку реализовать mock-объект для этого, но проверка заимствования этого не позволит.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
MockMessenger, которая не разрешена проверкой заимствованияЭтот тестовый код определяет структуру MockMessenger, у которой есть поле sent_messages с Vec значений String для отслеживания сообщений, которые ей сказано отправить. Мы также определяем ассоциированную функцию new, чтобы было удобно создавать новые значения MockMessenger, которые начинаются с пустого списка сообщений. Затем мы реализуем типаж Messenger для MockMessenger, чтобы мы могли передать MockMessenger в LimitTracker. В определении метода send мы берём сообщение, переданное в качестве параметра, и сохраняем его в списке sent_messages MockMessenger.
В тесте мы проверяем, что происходит, когда LimitTracker получает команду установить value в нечто, что составляет более 75 процентов от значения max. Сначала мы создаём новый MockMessenger, который начнётся с пустого списка сообщений. Затем мы создаём новый LimitTracker и передаём ему ссылку на новый MockMessenger и значение max равное 100. Мы вызываем метод set_value на LimitTracker со значением 80, что составляет более 75 процентов от 100. Затем мы утверждаем, что список сообщений, который MockMessenger отслеживает, теперь должен содержать одно сообщение.
Однако есть одна проблема с этим тестом, как показано здесь:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Мы не можем изменить MockMessenger для отслеживания сообщений, потому что метод send принимает неизменяемую ссылку на self. Мы также не можем принять предложение из текста ошибки использовать &mut self как в impl методе, так и в определении типажа. Мы не хотим изменять типаж Messenger только ради тестирования. Вместо этого нам нужно найти способ заставить наш тестовый код работать правильно с нашим существующим дизайном.
Это ситуация, в которой может помочь внутренняя изменяемость! Мы будем хранить sent_messages внутри RefCell<T>, и тогда метод send сможет изменять sent_messages для сохранения сообщений, которые мы видели. Листинг 15-22 показывает, как это выглядит.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> для изменения внутреннего значения, пока внешнее значение считается неизменяемымПоле sent_messages теперь имеет тип RefCell<Vec<String>> вместо Vec<String>. В функции new мы создаём новый экземпляр RefCell<Vec<String>> вокруг пустого вектора.
Для реализации метода send первый параметр по-прежнему является неизменяемым заимствованием self, что соответствует определению типажа. Мы вызываем borrow_mut на RefCell<Vec<String>> в self.sent_messages, чтобы получить изменяемую ссылку на значение внутри RefCell<Vec<String>>, то есть на вектор. Затем мы можем вызвать push на изменяемой ссылке на вектор, чтобы отслеживать сообщения, отправленные во время теста.
Последнее изменение, которое нам нужно сделать, — в утверждении: чтобы увидеть, сколько элементов во внутреннем векторе, мы вызываем borrow на RefCell<Vec<String>>, чтобы получить неизменяемую ссылку на вектор.
Теперь, когда вы увидели, как использовать RefCell<T>, давайте углубимся в то, как он работает!
Отслеживание заимствований во время выполнения с помощью RefCell<T>
При создании неизменяемых и изменяемых ссылок мы используем синтаксис & и &mut соответственно. С RefCell<T> мы используем методы borrow и borrow_mut, которые являются частью безопасного API, принадлежащего RefCell<T>. Метод borrow возвращает тип умного указателя Ref<T>, а borrow_mut возвращает тип умного указателя RefMut<T>. Оба типа реализуют Deref, поэтому мы можем обращаться с ними как с обычными ссылками.
RefCell<T> отслеживает, сколько умных указателей Ref<T> и RefMut<T> в данный момент активны. Каждый раз, когда мы вызываем borrow, RefCell<T> увеличивает счётчик активных неизменяемых заимствований. Когда значение Ref<T> выходит из области видимости, счётчик неизменяемых заимствований уменьшается на 1. Подобно правилам заимствования на этапе компиляции, RefCell<T> позволяет нам иметь много неизменяемых заимствований или одно изменяемое заимствование в любой момент времени.
Если мы попытаемся нарушить эти правила, вместо получения ошибки компиляции, как было бы со ссылками, реализация RefCell<T> вызовет панику во время выполнения. Листинг 15-23 показывает изменение реализации send из Листинга 15-22. Мы сознательно пытаемся создать два изменяемых заимствования, активных в одной области видимости, чтобы проиллюстрировать, что RefCell<T> предотвращает это во время выполнения.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> вызовет паникуМы создаём переменную one_borrow для умного указателя RefMut<T>, возвращённого из borrow_mut. Затем мы создаём другое изменяемое заимствование таким же образом в переменной two_borrow. Это создаёт две изменяемые ссылки в одной области видимости, что не разрешено. Когда мы запускаем тесты для нашей библиотеки, код в Листинге 15-23 скомпилируется без ошибок, но тест завершится неудачей:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Обратите внимание, что код вызвал панику с сообщением already borrowed: BorrowMutError. Вот так RefCell<T> обрабатывает нарушения правил заимствования во время выполнения.
Выбор перехвата ошибок заимствования во время выполнения, а не на этапе компиляции, как мы сделали здесь, означает, что вы потенциально будете находить ошибки в своём коде позже в процессе разработки: возможно, не до тех пор, пока ваш код не будет развёрнут в производстве. Также ваш код понесёт небольшое снижение производительности во время выполнения в результате отслеживания заимствований во время выполнения, а не на этапе компиляции. Однако использование RefCell<T> делает возможным написание mock-объекта, который может изменять себя для отслеживания сообщений, которые он видел, пока вы используете его в контексте, где разрешены только неизменяемые значения. Вы можете использовать RefCell<T> несмотря на его компромиссы, чтобы получить больше функциональности, чем предоставляют обычные ссылки.
Разрешение нескольким владельцам изменяемых данных с помощью Rc<T> и RefCell<T>
Распространённый способ использования RefCell<T> — в комбинации с Rc<T>. Вспомните, что Rc<T> позволяет иметь нескольких владельцев одних и тех же данных, но даёт только неизменяемый доступ к этим данным. Если у вас есть Rc<T>, который хранит RefCell<T>, вы можете получить значение, которое может иметь нескольких владельцев и которое можно изменять!
Например, вспомните пример cons-списка в Листинге 15-18, где мы использовали Rc<T>, чтобы позволить нескольким спискам разделять владение другим списком. Поскольку Rc<T> хранит только неизменяемые значения, мы не можем изменить ни одно из значений в списке после того, как создали их. Давайте добавим RefCell<T> для его способности изменять значения в списках. Листинг 15-24 показывает, что, используя RefCell<T> в определении Cons, мы можем изменить значение, хранящееся во всех списках.
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
Rc<RefCell<i32>> для создания List, который мы можем изменятьМы создаём значение, которое является экземпляром Rc<RefCell<i32>>, и сохраняем его в переменной с именем value, чтобы позже иметь к нему прямой доступ. Затем мы создаём List в a с вариантом Cons, который хранит value. Нам нужно клонировать value, чтобы и a, и value имели владение внутренним значением 5, а не передавали владение из value в a или чтобы a заимствовал из value.
Мы оборачиваем список a в Rc<T>, чтобы когда мы создаём списки b и c, они оба могли ссылаться на a, что мы и сделали в Листинге 15-18.
После того как мы создали списки в a, b и c, мы хотим добавить 10 к значению в value. Мы делаем это, вызывая borrow_mut на value, что использует функцию автоматического разыменования, которую мы обсуждали в Главе 4, чтобы разыменовать Rc<T> до внутреннего значения RefCell<T>. Метод borrow_mut возвращает умный указатель RefMut<T>, и мы используем оператор разыменования на нём и изменяем внутреннее значение.
Когда мы печатаем a, b и c, мы видим, что у всех них изменённое значение 15 вместо 5:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Эта техника довольно интересна! Используя RefCell<T>, мы имеем внешне неизменяемое значение List. Но мы можем использовать методы на RefCell<T>, которые предоставляют доступ к его внутренней изменяемости, чтобы изменять наши данные, когда нам это нужно. Проверка правил заимствования во время выполнения защищает нас от гонок данных, и иногда стоит променять немного скорости на эту гибкость в наших структурах данных. Обратите внимание, что RefCell<T> не работает для многопоточного кода! Mutex<T> — это потокобезопасная версия RefCell<T>, и мы обсудим Mutex<T> в Главе 16.