RefCell<T> и шаблон внутренней изменяемости

Внутренняя изменяемость - это шаблон проектирования в Rust, позволяющий изменять данные даже если ссылки на эти данные неизменяемые. Обычно этого нельзя делать из-за правил владения. Для изменения данных, данный шаблон использует unsafe (небезопасный) код внутри структур данных, чтобы обойти обычные правила заимствования и изменяемости в Rust. Мы подробнее поговорим о небезопасном коде в Главе 19. Можно использовать типы, которые используют шаблон внутренней изменяемости, когда мы можем обеспечить соблюдение правил заимствования во время выполнения, даже если не возможно этого гарантировать при компиляции. Использованный unsafe небезопасный код помещается в безопасный API, а внешний тип остаётся неизменным.

Давайте изучим данную концепцию с помощью типа данных RefCell<T>, который реализует этот шаблон.

Применение правил заимствования во время выполнения с помощью RefCell<T>

В отличие от Rc<T> тип RefCell<T> предоставляет единоличное владение данными, которые он содержит. В чем же отличие типа RefCell<T> от Box<T>? Давайте вспомним правила заимствования из Главы 4:

  • В любой момент времени у вас может быть одна (но не обе) одна изменяемая ссылка или любое количество неизменяемых ссылок.
  • Ссылки всегда должны быть действительными.

С помощью ссылок и типа Box<T> инварианты правил заимствования применяются на этапе компиляции. С помощью RefCell<T> они применяются во время работы программы. Если вы нарушите эти правила, работая с ссылками, то будет ошибка компиляции. Если вы работаете с RefCell<T> и нарушите эти правила, то программа вызовет панику и завершится.

Преимущества проверки правил заимствования во время компиляции состоят в том, что ошибки будут обнаруживаться быстрее в процессе разработки и это не влияет на производительность во время выполнения программы, поскольку весь анализ выполняется заранее. По этим причинам проверка правил заимствования во время компиляции является лучшим выбором в большинстве случаев, именно поэтому она используется в Rust по умолчанию.

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

Изменение значения внутри неизменного значения является шаблоном внутренней изменяемости (interior mutability). Давайте посмотрим на ситуацию, в которой внутренняя изменяемость полезна и рассмотрим, как это возможно.

Внутренняя изменяемость: изменяемое заимствование неизменяемого значения

Следствием правил заимствования является то, что когда у вас есть неизменяемое значение, вы не можете заимствовать его с изменением. Например, этот код не будет компилироваться:

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
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing`

To learn more, run the command again with --verbose.

Однако существуют ситуации в которых было бы полезно, чтобы значение изменяло само себя в своих методах, но казалось неизменным для другого кода. Код за пределами таких методов над значениями не мог бы изменить сами значения. Использование RefCell<T> является одним из способов получить внутреннюю изменяемость. Но RefCell<T> не обходит правила заимствования полностью: анализатор заимствования компилятора допускает эту внутреннюю изменяемость и вместо этого правила заимствования проверяются во время выполнения. Если вы нарушаете правила, вы получите panic! вместо ошибки компилятора.

Давайте разберём практический пример, в котором мы можем использовать RefCell<T> для изменения неизменяемого значения и посмотрим, почему это полезно.

Вариант использования внутренней изменяемости: мок объекты

Тест дубликаты - это общая программная концепция для типа, используемого вместо другого типа во время тестирования. Мок объекты - это особые типы тест дубликатов, которые записывают то, что происходит во время теста, поэтому после прохождения теста можно утверждать, что выполнились правильные действия в мок объекте.

В Rust нет объектов в том же смысле, в каком они есть в других языках и в Rust нет функциональности мок объектов, встроенных в стандартную библиотеку, как в некоторых других языках. Однако вы определённо можете создать структуру, которая будет служить тем же целям, что и мок объект.

Вот сценарий, который мы будем тестировать: мы создадим библиотеку, которая отслеживает значение по отношению к заранее определённому максимальному значению и отправляет сообщения в зависимости от того, насколько текущее значение находится близко к такому максимальному значению. Эта библиотека может использоваться, например, для отслеживания квоты количества вызовов API пользователя, которые ему разрешено делать.

Наша библиотека будет предоставлять только функции отслеживания того, насколько близко к максимальному значению находится значение и какие сообщения должны быть внутри в этот момент. Ожидается, что приложения, использующие нашу библиотеку, предоставят механизм для отправки сообщений: приложение может поместить сообщение в приложение, отправить электронное письмо, отправить текстовое сообщение или что-то ещё. Библиотеке не нужно знать эту деталь. Все что ему нужно - это что-то, что реализует типаж, который мы предоставим с названием 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: &T, max: usize) -> LimitTracker<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 экземпляр self.messenger отправляет соответствующие сообщения.

Нам нужен мок объект, который вместо отправки электронного письма или текстового сообщения будет отслеживать сообщения, которые были ему поручены для отправки через send. Мы можем создать новый экземпляр мок объекта, создать LimitTracker с использованием мок объект для него, вызвать метод set_value у экземпляра LimitTracker, а затем проверить, что мок объект имеет ожидаемое сообщение. В листинге 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: &T, max: usize) -> LimitTracker<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 со значениями типа Vec из String для отслеживания сообщений, которые поручены структуре для отправки. Мы также определяем ассоциированную функцию new, чтобы было удобно создавать новые экземпляры MockMessenger, которые создаются с пустым списком сообщений. Затем мы реализуем типаж Messenger для типа MockMessenger, чтобы передать MockMessenger в LimitTracker. В сигнатуре метода send мы принимаем сообщение для передачи в качестве параметра и сохраняем его в MockMessenger внутри списка sent_messages.

В этом тесте мы проверяем, что происходит, когда 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
   |
57 |         fn send(&self, message: &str) {
   |                 ----- help: consider changing this 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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker`

To learn more, run the command again with --verbose.
warning: build failed, waiting for other jobs to finish...
error: build failed

Мы не можем изменять MockMessenger для отслеживания сообщений, потому что метод send принимает неизменяемую ссылку на self. Мы также не можем принять предложение из текста ошибки, чтобы использовать &mut self, потому что тогда сигнатура send не будет соответствовать сигнатуре в определении типажа 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: &T, max: usize) -> LimitTracker<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);
    }
}

Листинг 15-22: Использование RefCell для изменения внутреннего значения, в то время как внешнее значение считается неизменным

Поле 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> выходит из области видимости, то количество неизменяемых заимствований уменьшается на единицу. Как и с правилами заимствования во время компиляции, 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: &T, max: usize) -> LimitTracker<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 будет паниковать.

Мы создаём переменную one_borrow для умного указателя RefMut<T> возвращаемого из метода borrow_mut. Затем мы создаём другое изменяемое заимствование таким же образом в переменной two_borrow. Это создаёт две изменяемые ссылки в одной области видимости, что недопустимо. Когда мы запускаем тесты для нашей библиотеки, код в листинге 15-23 компилируется без ошибок, но тест завершится неудачно:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running target/debug/deps/limit_tracker-d1b2637139dca6ca

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 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
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> можно использовать, чтобы получить больше функциональности чем позволяют обычные ссылки.

Наличие нескольких владельцев изменяемых данных путём объединения типов 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 так, что мы можем изменить значение хранящееся во всех списках:

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

Стандартная библиотека имеет другие типы, которые обеспечивают внутреннюю изменяемость, например тип Cell<T> аналогичен, за исключением того, что вместо предоставления ссылки на внутреннее значение, значение копируется внутрь Cell<T> и изнутри наружу. Есть также тип Mutex<T>, который предлагает внутреннюю изменяемость, которую можно безопасно использовать в разных потоках; мы обсудим его использование в главе 16. Посмотрите документацию стандартной библиотеки для получения более подробной информации о различиях между этими типами.