RefCell<T> и внутренняя изменяемость
Внутренняя изменяемость (interior mutability) — шаблон проектирования в
Rust, который позволяет менять данные, даже когда на них есть неизменяемые
ссылки; обычно правила заимствования такое запрещают. Чтобы менять данные, этот
шаблон использует внутри структуры данных небезопасный код (unsafe), который
обходит обычные правила Rust об изменении и заимствовании. Небезопасный код
сообщает компилятору, что правила мы проверяем сами, а не полагаемся на него;
подробнее о небезопасном коде мы поговорим в главе 20.
Типы с внутренней изменяемостью можно использовать, только когда мы можем
гарантировать, что правила заимствования будут соблюдены во время выполнения,
даже если компилятор этого гарантировать не может. Задействованный при этом
unsafe-код спрятан за безопасным API, а внешний тип по-прежнему
неизменяемый.
Разберёмся в этой идее на примере типа RefCell<T>, который реализует шаблон
внутренней изменяемости.
Проверка правил заимствования во время выполнения
В отличие от Rc<T>, у данных внутри RefCell<T> один владелец. Чем же тогда
RefCell<T> отличается от, скажем, Box<T>? Вспомните правила заимствования
из главы 4:
- В каждый момент времени может быть либо одна изменяемая ссылка, либо сколько угодно неизменяемых (но не то и другое сразу).
- Ссылки всегда должны быть действительными.
Для ссылок и Box<T> эти правила проверяются при компиляции. Для RefCell<T>
— во время выполнения. Если нарушить правила со ссылками, получится ошибка
компиляции. А если нарушить их с RefCell<T>, программа запаникует и
завершится.
У проверки правил заимствования при компиляции есть преимущества: ошибки обнаруживаются раньше, а на производительность во время работы это не влияет — весь анализ сделан заранее. Поэтому в большинстве случаев проверка при компиляции — лучший выбор, и в Rust она по умолчанию.
А преимущество проверки во время выполнения в том, что она допускает некоторые безопасные для памяти сценарии, которые проверка при компиляции отвергла бы. Статический анализ, который выполняет и компилятор Rust, по своей природе консервативен. Некоторые свойства кода невозможно установить, просто анализируя код. Самый известный пример — проблема остановки (Halting Problem): она выходит за рамки этой книги, но в ней интересно покопаться.
Раз некоторый анализ невозможен, компилятор 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> можно менять
неизменяемое значение, и посмотрим, чем это полезно.
Тестирование с мок-объектами
Иногда при тестировании программист подставляет вместо одного типа другой, чтобы понаблюдать за определённым поведением и проверить, что оно реализовано правильно. Такой тип-заменитель называется тестовым двойником (test double) — по аналогии с дублёром в кино, который подменяет актёра в особенно сложной сцене. Тестовые двойники подменяют другие типы во время тестов. Мок-объекты (mock objects) — особый вид тестовых двойников: они записывают, что происходило во время теста, чтобы потом можно было проверить, что выполнились нужные действия.
Объектов в том смысле, в каком они есть в других языках, в Rust нет, и встроенной в стандартную библиотеку поддержки мок-объектов, как в некоторых других языках, тоже нет. Но создать структуру, которая будет служить тем же целям, что мок-объект, вполне можно.
Вот сценарий, который мы будем тестировать. Сделаем библиотеку, которая следит за значением относительно максимума и отправляет сообщения в зависимости от того, насколько текущее значение близко к максимуму. Такую библиотеку можно использовать, например, чтобы отслеживать квоту пользователя на количество обращений к API.
Наша библиотека будет только следить за тем, насколько значение близко к
максимуму, и решать, какие сообщения и когда отправлять. А механизм отправки
сообщений должны предоставить приложения, которые её используют: приложение
может показать сообщение пользователю, отправить письмо, SMS или сделать
что-то ещё. Библиотеке эта деталь не важна. Ей нужно лишь что-нибудь,
реализующее трейт 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 и текст сообщения. Это интерфейс,
который должен реализовать наш мок-объект, чтобы им можно было пользоваться
так же, как настоящим объектом. Другая важная часть — мы хотим протестировать
поведение метода set_value у LimitTracker. Мы можем менять то, что
передаём в параметр value, но set_value ничего не возвращает, и проверять
нечего. Мы хотим убедиться в следующем: если создать LimitTracker с чем-то,
что реализует трейт Messenger, и с определённым значением max, то при
передаче разных чисел в value LimitTracker поручает мессенджеру отправить
нужные сообщения.
Нам нужен мок-объект, который при вызове send не отправляет письмо или SMS, а
лишь запоминает сообщения, которые ему поручили отправить. Можно создать
новый экземпляр мок-объекта, создать LimitTracker, использующий этот
мок-объект, вызвать у LimitTracker метод set_value и проверить, что в
мок-объекте оказались ожидаемые сообщения. В листинге 15-21 — попытка
реализовать такой мок-объект, но анализатор заимствований её не пропустит.
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 — вектором значений String, в котором запоминаются сообщения,
которые ей поручили отправить. Ещё мы определяем ассоциированную функцию
new, чтобы удобно создавать новые значения MockMessenger с пустым списком
сообщений. Затем реализуем для MockMessenger трейт Messenger, чтобы можно
было передать MockMessenger в LimitTracker. В методе send мы берём
переданное параметром сообщение и сохраняем его в списке sent_messages
структуры MockMessenger.
В тесте мы проверяем, что происходит, когда LimitTracker просят установить
value больше 75 процентов от max. Сначала создаём новый MockMessenger с
пустым списком сообщений. Затем создаём новый LimitTracker и передаём ему
ссылку на этот MockMessenger и значение max, равное 100. Вызываем у
LimitTracker метод set_value со значением 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 it 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() {
// --пропуск--
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, что соответствует определению трейта. У
RefCell<Vec<String>> в self.sent_messages мы вызываем borrow_mut и
получаем изменяемую ссылку на значение внутри RefCell<Vec<String>>, то есть
на вектор. Затем у изменяемой ссылки на вектор можно вызвать push и
запомнить сообщение, отправленное во время теста.
Последнее изменение — в проверке: чтобы узнать, сколько элементов во
внутреннем векторе, вызываем у RefCell<Vec<String>> метод borrow и
получаем неизменяемую ссылку на вектор.
Теперь, когда вы увидели, как пользоваться RefCell<T>, разберёмся, как он
устроен!
Учёт заимствований во время выполнения
Неизменяемые и изменяемые ссылки мы создаём с помощью & и &mut. А у
RefCell<T> для этого есть методы borrow и borrow_mut — часть его
безопасного API. Метод 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' (6028024) panicked at src/lib.rs:60:53:
RefCell already borrowed
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> позволяет написать
мок-объект, который может менять себя, запоминая увиденные сообщения, даже в
контексте, где разрешены только неизменяемые значения. Несмотря на эти
компромиссы, 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,
чтобы потом обращаться к нему напрямую. Затем создаём в a список List с
вариантом Cons, который хранит value. value нужно клонировать: тогда
внутренним значением 5 будут владеть и a, и value. Без клонирования
владение перешло бы от value к a (или a лишь заимствовал бы его у
value).
Список a мы оборачиваем в Rc<T>, чтобы созданные затем списки b и c
могли оба ссылаться на a, — как в листинге 15-18.
Создав списки a, b и c, мы хотим прибавить 10 к значению в value. Для
этого вызываем у value метод borrow_mut. Тут срабатывает автоматическое
разыменование, о котором мы говорили в разделе «А где же оператор
->?» главы 5: 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> не работает в многопоточном коде!
Потокобезопасная версия RefCell<T> — это Mutex<T>; о нём мы поговорим в
главе 16.