Введение
Добро пожаловать в RefCell
В этой лабораторной работе мы исследуем концепцию внутренней изменяемости в Rust и то, как она реализуется с использованием типа RefCell<T>.
RefCell<T> и Паттерн внутренней изменяемости
«Внутренняя изменяемость» — это шаблон проектирования в Rust, который позволяет изменять данные даже в том случае, если есть неизменяемые ссылки на эти данные; обычно такое действие запрещено правилами заимствования. Чтобы изменить данные, этот шаблон использует unsafe код внутри структуры данных, чтобы обойти обычные правила Rust, которые регулируют изменение и заимствование. Unsafe код сообщает компилятору, что мы вручную проверяем правила, а не полагаемся на компилятор, чтобы он проверил их для нас; мы поговорим об unsafe коде более подробно в главе 19.
Мы можем использовать типы, которые используют шаблон внутренней изменяемости, только в том случае, если можем гарантировать, что правила заимствования будут соблюдены во время выполнения, хотя компилятор не может этого гарантировать. Затем unsafe код, который используется, заключается в безопасный API, и внешний тип по-прежнему остается неизменяемым.
Давайте исследуем этот концепт, рассмотрев тип RefCell<T>, который следит за шаблоном внутренней изменяемости.
Применение правил заимствования во время выполнения с помощью RefCell<T>
В отличие от Rc<T>, тип RefCell<T> представляет собой единственную собственность над данными, которые он хранит. То, что делает RefCell<T> различным от типа, такой как Box<T>? Назовите правила заимствования, которые вы узнали в главе 4:
- В любое конкретное время у вас может быть либо одна изменяемая ссылка, либо любое количество неизменяемых ссылок (но не оба сразу).
- Ссылки должны всегда быть действительными.
При использовании ссылок и Box<T> инварианты правил заимствования накладываются на этапе компиляции. При использовании RefCell<T> эти инварианты накладываются во время выполнения. При использовании ссылок, если вы нарушаете эти правила, вы получите ошибку компилятора. При использовании RefCell<T>, если вы нарушаете эти правила, ваша программа будет аварийно завершена и выйдете из нее.
Преимуществом проверки правил заимствования на этапе компиляции является то, что ошибки будут обнаружены раньше в процессе разработки, и это не влияет на производительность во время выполнения, так как все анализ завершается заранее. По этим причинам проверка правил заимствования на этапе компиляции является наилучшим выбором в большинстве случаев, и именно поэтому это и есть стандартный подход в 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>является неизменяемым.
Изменение значения внутри неизменяемого значения — это шаблон внутренней изменяемости. Рассмотрим ситуацию, в которой внутренняя изменяемость полезна, и изучим, как это возможно.
Внутренняя изменяемость: изменяемая ссылка на неизменяемое значение
В результате правил заимствования, когда у вас есть неизменяемое значение, вы не можете взять его в изменяемую ссылку. Например, этот код не скомпилируется:
Имя файла: src/main.rs
fn main() {
let x = 5;
let y = &mut x;
}
Если вы попытаетесь скомпилировать этот код, вы получите следующую ошибку:
error[E0596]: cannot borrow `x` as mutable, as it is not declared
as mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
Однако бывают ситуации, когда полезно, чтобы значение изменяло себя в своих методах, но казалось бы неизменным для другого кода. Код за пределами методов значения не должен был бы изменять значение. Использование RefCell<T> — один из способов получить возможность внутренней изменяемости, но RefCell<T> не полностью обходит правила заимствования: проверщик заимствований в компиляторе позволяет эту внутреннюю изменяемость, и правила заимствования проверяются во время выполнения. Если вы нарушаете правила, вы получите panic! вместо ошибки компилятора.
Рассмотрим практический пример, в котором мы можем использовать RefCell<T> для изменения неизменяемого значения и увидим, почему это полезно.
Применение внутренней изменяемости: моки-объекты
Иногда при тестировании программист использует один тип вместо другого, чтобы наблюдать за определенным поведением и проверить, правильно ли оно реализовано. Этот заменяющий тип называется тестовым替身ом (test double). Представьте себе его в смысле стunts double в кино, когда человек замещается актером, чтобы выполнить особенно сложную сцену. Тестовые替身ы замещают другие типы, когда мы запускаем тесты. Моки-объекты (Mock objects) — это конкретный тип тестовых替身ов, которые записывают, что происходит во время теста, чтобы вы могли проверить, были ли выполнены правильные действия.
В Rust нет объектов в том же смысле, что и в других языках, и в стандартной библиотеке Rust нет встроенной функциональности для моки-объектов, как это есть в некоторых других языках. Однако вы можете определенно создать структуру, которая будет служить тем же целям, что и моки-объект.
Вот сценарий, который мы будем тестировать: мы создадим библиотеку, которая отслеживает значение по отношению к максимальному значению и отправляет сообщения в зависимости от того, насколько близко текущее значение к максимальному. Эта библиотека, например, может быть использована для отслеживания квоты пользователя по количеству разрешенных вызовов API.
Наша библиотека будет предоставлять только функциональность отслеживания насколько близко значение к максимальному и какие сообщения должны быть отправлены в определенные моменты. Приложения, которые используют нашу библиотеку, должны обеспечить механизм отправки сообщений: приложение может поместить сообщение в приложение, отправить электронное письмо, отправить текстовое сообщение или что-то другое. Библиотека не должна знать эти детали. Все, что она нуждается в том, чтобы иметь что-то, которое реализует трейт, который мы предоставим под названием Messenger. Listing 15-20 показывает код библиотеки.
Имя файла: src/lib.rs
pub trait Messenger {
1 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,
}
}
2 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: You're at 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You're at 75% of your quota!");
}
}
}
Listing 15-20: Библиотека для отслеживания насколько близко значение к максимальному и предупреждения при достижении определенных уровней
Одним из важных моментов этого кода является то, что трейт Messenger имеет один метод под названием send, который принимает неизменяемую ссылку на self и текст сообщения [1]. Этот трейт — это интерфейс, который должен реализовать наш моки-объект, чтобы мок мог быть использован так же, как и настоящий объект. Другой важный момент — это то, что мы хотим протестировать поведение метода set_value на LimitTracker [2]. Мы можем изменить то, что мы передаем в качестве параметра value, но set_value не возвращает ничего, чтобы мы могли сделать утверждения. Мы хотим быть уверены, что если мы создадим LimitTracker с тем, что реализует трейт Messenger и определенным значением для max, когда мы передаем разные числа для value, мессенджеру будет сказано отправить соответствующие сообщения.
Нам нужен моки-объект, который вместо отправки электронного письма или текстового сообщения при вызове send будет только отслеживать сообщения, которые ему сообщают отправить. Мы можем создать новый экземпляр моки-объекта, создать LimitTracker, который использует моки-объект, вызвать метод set_value на LimitTracker и затем проверить, что в моки-объекте есть сообщения, которые мы ожидаем. Listing 15-21 показывает попытку реализовать моки-объект для этого, но проверщик заимствований не позволит это сделать.
Имя файла: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
1 struct MockMessenger {
2 sent_messages: Vec<String>,
}
impl MockMessenger {
3 fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
4 impl Messenger for MockMessenger {
fn send(&self, message: &str) {
5 self.sent_messages.push(String::from(message));
}
}
#[test]
6 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);
}
}
Listing 15-21: Попытка реализовать MockMessenger, которая не допускается проверщиком заимствований
Этот тестовый код определяет структуру MockMessenger [1], которая имеет поле sent_messages с Vec значений String [2], чтобы отслеживать сообщения, которые ей сообщают отправить. Мы также определяем связанную функцию new [3], чтобы было удобно создавать новые значения MockMessenger, которые начинаются с пустого списка сообщений. Затем мы реализуем трейт Messenger для MockMessenger [4], чтобы мы могли передать MockMessenger в LimitTracker. В определении метода send [5] мы берем сообщение, переданное в качестве параметра, и сохраняем его в списке sent_messages MockMessenger.
В тесте мы проверяем, что происходит, когда LimitTracker получает команду установить value на что-то, что больше 75 процентов от max значения [6]. Сначала мы создаем новый MockMessenger, который начнется с пустого списка сообщений. Затем мы создаем новый LimitTracker и передаем ему ссылку на новый MockMessenger и max значение 100. Мы вызываем метод set_value на LimitTracker со значением 80, что больше 75 процентов от 100. Затем мы утверждаем, что список сообщений, который отслеживает MockMessenger, должен теперь содержать одно сообщение.
Однако есть одна проблема с этим тестом, как показано здесь:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a
`&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference:
`&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a
`&` reference, so the data it refers to cannot be borrowed as mutable
Мы не можем изменить MockMessenger, чтобы отслеживать сообщения, потому что метод send принимает неизменяемую ссылку на self. Мы также не можем принять предложение из текста ошибки и использовать &mut self вместо этого, потому что тогда сигнатура send не соответствовала бы сигнатуре в определении трейта Messenger (свободно попробуйте это и посмотрите, какую ошибку вы получите).
Это ситуация, в которой внутренняя изменяемость может помочь! Мы будем хранить sent_messages внутри RefCell<T>, и тогда метод send сможет изменить sent_messages, чтобы сохранить сообщения, которые мы увидели. Listing 15-22 показывает, как это выглядит.
Имя файла: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
1 sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
2 sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages
3.borrow_mut()
.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
--snip--
assert_eq!(
4 mock_messenger.sent_messages.borrow().len(),
1
);
}
}
Listing 15-22: Использование RefCell<T> для изменения внутреннего значения, когда внешнее значение считается неизменяемым
Поле sent_messages теперь имеет тип RefCell<Vec<String>> [1] вместо Vec<String>. В функции new мы создаем новый экземпляр RefCell<Vec<String>> вокруг пустого вектора [2].
Для реализации метода send первый параметр по-прежнему является неизменяемой ссылкой на self, что соответствует определению трейта. Мы вызываем borrow_mut на RefCell<Vec<String>> в self.sent_messages [3], чтобы получить изменяемую ссылку на значение внутри RefCell<Vec<String>>, которое является вектором. Затем мы можем вызвать push на изменяемую ссылку на вектор, чтобы отслеживать сообщения, отправленные во время теста.
Последнее изменение, которое мы должны сделать, — это в утверждении: чтобы увидеть, сколько элементов в внутреннем векторе, мы вызываем borrow на RefCell<Vec<String>>, чтобы получить неизменяемую ссылку на вектор [4].
Теперь, когда вы видели, как использовать 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> будет аварийно завершена во время выполнения. Listing 15-23 показывает модификацию реализации send из Listing 15-22. Мы特意 пытаемся создать два активных изменяемых заимствования для одной области видимости, чтобы показать, что RefCell<T> не позволяет нам этого делать во время выполнения.
Имя файла: src/lib.rs
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));
}
}
Listing 15-23: Создание двух изменяемых ссылок в одной области видимости, чтобы увидеть, что RefCell<T> будет аварийно завершаться
Мы создаем переменную one_borrow для умного указателя RefMut<T>, возвращаемого методом borrow_mut. Затем мы создаем еще одно изменяемое заимствование так же в переменной two_borrow. Это создает две изменяемые ссылки в одной области видимости, что не допускается. Когда мы запускаем тесты для нашей библиотеки, код из Listing 15-23 будет скомпилирован без ошибок, но тест будет провален:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Заметим, что код аварийно завершился с сообщением already borrowed: BorrowMutError. Именно так RefCell<T> обрабатывает нарушения правил заимствования во время выполнения.
Выбор поймать ошибки заимствования во время выполнения вместо этапа компиляции, как мы сделали здесь, означает, что вы потенциально будете обнаруживать ошибки в своем коде позже в процессе разработки: возможно, не до тех пор, пока ваш код не будет развернут в продакшене. Также ваш код будет иметь небольшую штрафу по производительности во время выполнения в результате отслеживания заимствований во время выполнения вместо этапа компиляции. Однако использование RefCell<T> позволяет написать моки-объект, который может изменять себя, чтобы отслеживать сообщения, которые он видел, в то время как вы используете его в контексте, где допускаются только неизменяемые значения. Вы можете использовать RefCell<T> несмотря на его недостатки, чтобы получить больше функциональности, чем обычные ссылки предоставляют.
Позволение нескольким владельцам изменяемым данным с помощью Rc<T> и RefCell<T>
Одним из распространенных способов использования RefCell<T> является сочетание его с Rc<T>. Назовите, что Rc<T> позволяет вам иметь несколько владельцев некоторых данных, но он предоставляет только неизменяемый доступ к этим данным. Если у вас есть Rc<T>, который содержит RefCell<T>, вы можете получить значение, которое может иметь несколько владельцев и которое вы можете изменять!
Например, вспомните пример списка из Listing 15-18, где мы использовали Rc<T>, чтобы позволить нескольким спискам делиться владением другого списка. Поскольку Rc<T> хранит только неизменяемые значения, мы не можем изменить любое из значений в списке, после того, как мы его создали. Добавим RefCell<T> для его способности изменять значения в списках. Listing 15-24 показывает, что с использованием RefCell<T> в определении Cons мы можем изменить значение, хранимое во всех списках.
Имя файла: src/main.rs
#[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() {
1 let value = Rc::new(RefCell::new(5));
2 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));
3 *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
Listing 15-24: Использование Rc<RefCell<i32>> для создания List, который мы можем изменить
Мы создаем значение, которое является экземпляром Rc<RefCell<i32>>, и сохраняем его в переменной с именем value [1], чтобы мы могли позже напрямую обращаться к нему. Затем мы создаем List в a с вариантом Cons, который содержит value [2]. Мы должны склонировать value, чтобы и a, и value имели владение внутренним значением 5, а не передавать владение из value в a или чтобы a заимствовало от value.
Мы заключаем список a в Rc<T>, чтобы когда мы создаем списки b и c, они оба могли ссылаться на a, как мы делали в Listing 15-18.
После того, как мы создали списки в a, b и c, мы хотим добавить 10 к значению в value [3]. Мы это делаем, вызвав borrow_mut на value, который использует автоматическое снятие ссылочности, о котором мы говорили в разделе "Где находится оператор ->?" для снятия ссылочности Rc<T> до внутреннего значения RefCell<T>. Метод borrow_mut возвращает умный указатель RefMut<T>, и мы используем оператор снятия ссылочности на нем и изменяем внутреннее значение.
Когда мы выводим a, b и c, мы можем увидеть, что у них все есть измененное значение 15 вместо 5:
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.
Резюме
Поздравляем! Вы завершили лабораторную работу по RefCell