Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Файл: src/lib.rs
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!");
        }
    }
}
Листинг 15-20: Библиотека, которая следит, насколько значение близко к максимуму, и предупреждает, когда оно достигает определённых уровней

Важная часть этого кода — у трейта Messenger есть метод send, который принимает неизменяемую ссылку на self и текст сообщения. Это интерфейс, который должен реализовать наш мок-объект, чтобы им можно было пользоваться так же, как настоящим объектом. Другая важная часть — мы хотим протестировать поведение метода set_value у LimitTracker. Мы можем менять то, что передаём в параметр value, но set_value ничего не возвращает, и проверять нечего. Мы хотим убедиться в следующем: если создать LimitTracker с чем-то, что реализует трейт Messenger, и с определённым значением max, то при передаче разных чисел в value LimitTracker поручает мессенджеру отправить нужные сообщения.

Нам нужен мок-объект, который при вызове send не отправляет письмо или SMS, а лишь запоминает сообщения, которые ему поручили отправить. Можно создать новый экземпляр мок-объекта, создать LimitTracker, использующий этот мок-объект, вызвать у LimitTracker метод set_value и проверить, что в мок-объекте оказались ожидаемые сообщения. В листинге 15-21 — попытка реализовать такой мок-объект, но анализатор заимствований её не пропустит.

Файл: src/lib.rs
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);
    }
}
Листинг 15-21: Попытка реализовать 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.

Файл: src/lib.rs
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);
    }
}
Листинг 15-22: 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> не даст этого сделать во время выполнения.

Файл: src/lib.rs
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);
    }
}
Листинг 15-23: Создаём две изменяемые ссылки в одной области видимости — 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, можно изменить значение, хранящееся во всех списках.

Файл: 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() {
    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:?}");
}
Листинг 15-24: 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.