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, які керують змінністю та запозиченням. Код unsafe показує компілятору, що ми перевіряємо правила вручну, замість того щоб покладатися на компілятор, який перевірить їх за нас; про код unsafe ми докладніше поговоримо в розділі 20.

Ми можемо використовувати типи, що застосовують патерн внутрішньої змінності, лише коли можемо гарантувати, що правила запозичення буде дотримано під час виконання, навіть якщо компілятор не може цього гарантувати. unsafe-код, що використовується, тоді обгортається в безпечний API, а зовнішній тип усе ще є незмінним.

Дослідимо цю концепцію, подивившись на тип RefCell<T>, який дотримується патерну внутрішньої змінності.

Забезпечення правил запозичення під час виконання

На відміну від Rc<T>, тип RefCell<T> представляє єдину власність над даними, які він зберігає. Отже, що робить RefCell<T> відмінним від типу на кшталт Box<T>? Згадайте правила запозичення, які ви вивчили в розділі 4:

  • У будь-який момент часу ви можете мати або одне змінне посилання, або будь-яку кількість незмінних посилань (але не обидва варіанти одночасно).
  • Посилання завжди мають бути дійсними.

З посиланнями та Box<T> інваріанти правил запозичення забезпечуються під час компіляції. З RefCell<T> ці інваріанти забезпечуються під час виконання. З посиланнями, якщо ви порушите ці правила, отримаєте помилку компілятора. З RefCell<T>, якщо ви порушите ці правила, ваша програма завершиться через panic.

Переваги перевірки правил запозичення під час компіляції полягають у тому, що помилки буде виявлено раніше в процесі розробки, а також у тому, що це не впливає на продуктивність під час виконання, оскільки весь аналіз завершується заздалегідь. З цих причин перевірка правил запозичення під час компіляції є найкращим вибором у більшості випадків, тому це й є типовою поведінкою Rust.

Перевага перевірки правил запозичення під час виконання полягає в тому, що деякі безпечні для пам’яті сценарії тоді дозволяються, хоча їх було б заборонено перевірками під час компіляції. Статичний аналіз, як у компіляторі Rust, за своєю природою є обережним. Деякі властивості коду неможливо виявити, аналізуючи код: найвідоміший приклад — проблема зупинки, яка виходить за межі цієї книги, але є цікавою темою для дослідження.

Оскільки деякий аналіз неможливий, якщо компілятор Rust не може бути певним, що код відповідає правилам власності, він може відхилити правильну програму; таким чином він є обережним. Якби Rust прийняв неправильну програму, користувачі не могли б довіряти гарантіям, які дає Rust. Однак якщо Rust відхиляє правильну програму, це створить незручності для програміста, але нічого катастрофічного не станеться. Тип RefCell<T> корисний тоді, коли ви впевнені, що ваш код дотримується правил запозичення, але компілятор не може це зрозуміти та гарантувати.

Подібно до Rc<T>, RefCell<T> призначений лише для використання в однопотокових сценаріях і видасть вам помилку компіляції, якщо ви спробуєте використати його в багатопотоковому контексті. У розділі 16 ми поговоримо про те, як отримати функціональність RefCell<T> у багатопотоковій програмі.

Ось короткий підсумок причин обрати 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. Подумайте про це в сенсі дублера-каскадера у кіно, де людина підміняє актора для виконання особливо складної сцени. Test doubles виступають замість інших типів, коли ми запускаємо тести. Mock objects — це конкретні типи test doubles, які записують те, що відбувається під час тесту, щоб ви могли стверджувати, що відбулися правильні дії.

Rust не має об’єктів у тому ж сенсі, у якому об’єкти є в інших мовах, і Rust не має вбудованої у стандартну бібліотеку функціональності mock objects, як деякі інші мови. Однак ви цілком можете створити структуру, яка виконуватиме ті самі завдання, що й mock object.

Ось сценарій, який ми тестуватимемо: ми створимо бібліотеку, яка відстежує значення відносно максимального значення та надсилає повідомлення залежно від того, наскільки поточне значення близьке до максимального. Цю бібліотеку можна, наприклад, використати для відстеження квоти користувача на кількість API-викликів, які йому дозволено зробити.

Наша бібліотека надаватиме лише функціональність відстеження того, наскільки значення близьке до максимуму, і якими мають бути повідомлення та коли. Від застосунків, які використовують нашу бібліотеку, очікуватиметься надання механізму надсилання повідомлень: застосунок може показувати повідомлення користувачу безпосередньо, надсилати email, надсилати текстове повідомлення або робити щось інше. Бібліотеці не потрібно знати цю деталь. Усе, що їй потрібно, — це щось, що реалізує трейт, який ми надамо, під назвою Messenger. У переліку 15-20 показано код бібліотеки.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

Важливою частиною цього коду є те, що трейт Messenger має один метод, send, який приймає незмінне посилання на self і текст повідомлення. Цей трейт — інтерфейс, який наш мок-об’єкт має реалізувати, щоб мок можна було використовувати так само, як реальний об’єкт. Інша важлива частина полягає в тому, що ми хочемо протестувати поведінку методу set_value на LimitTracker. Ми можемо змінювати те, що передаємо для параметра value, але set_value не повертає нічого, на чому ми могли б зробити ствердження. Ми хочемо бути в змозі сказати, що якщо ми створимо LimitTracker із чимось, що реалізує трейт Messenger, і певним значенням для max, то messenger буде наказано надіслати відповідні повідомлення, коли ми передаємо різні числа для value.

Нам потрібен мок-об’єкт, який замість надсилання email або текстового повідомлення під час виклику send лише відстежуватиме повідомлення, які йому наказано надсилати. Ми можемо створити новий екземпляр мок-об’єкта, створити LimitTracker, який використовує мок-об’єкт, викликати метод set_value на LimitTracker, а потім перевірити, що мок-об’єкт має повідомлення, яких ми очікуємо. У переліку 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, яка має поле sent_messages з Vec значень String для відстеження повідомлень, які їй наказано надсилати. Ми також визначаємо асоційовану функцію new, щоб було зручно створювати нові значення MockMessenger, які починаються з порожнього списку повідомлень. Потім ми реалізуємо трейт Messenger для MockMessenger, щоб ми могли передати MockMessenger до LimitTracker. У визначенні методу send ми беремо повідомлення, передане як параметр, і зберігаємо його у списку sent_messages структури MockMessenger.

У тесті ми перевіряємо, що станеться, коли LimitTracker буде наказано встановити value у значення, яке перевищує 75 відсотків від значення max. Спочатку ми створюємо новий MockMessenger, який починатиметься з порожнього списку повідомлень. Потім ми створюємо новий LimitTracker і даємо йому посилання на новий MockMessenger та значення max 100. Ми викликаємо метод set_value на LimitTracker зі значенням 80, яке становить понад 75 відсотків від 100. Потім ми стверджуємо, що список повідомлень, які відстежує MockMessenger, тепер має містити одне повідомлення.

Однак у цьому тесті є одна проблема, як показано тут:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
 2 ~     fn send(&mut self, msg: &str);
 3 | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

Ми не можемо змінювати MockMessenger, щоб відстежувати повідомлення, тому що метод send приймає незмінне посилання на self. Ми також не можемо прийняти пропозицію з тексту помилки використати &mut self і в методі impl, і в оголошенні трейта. Ми не хочемо змінювати трейт Messenger лише заради тестування. Натомість нам потрібно знайти спосіб, щоб наш тестовий код коректно працював із наявним дизайном.

Це ситуація, у якій внутрішня змінність може допомогти! Ми зберігатимемо sent_messages всередині RefCell<T>, і тоді метод send зможе змінювати sent_messages, щоб зберігати повідомлення, які ми побачили. У переліку 15-22 показано, як це виглядає.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Поле 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>, давайте розберемося, як це працює!

Відстеження запозичень під час виконання

Під час створення незмінних і змінних посилань ми використовуємо синтаксис & і &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> викличе panic під час виконання. У переліку 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);
    }
}

Ми створюємо змінну one_borrow для розумного вказівника RefMut<T>, який повертається з borrow_mut. Потім ми створюємо ще одне змінне запозичення таким самим способом у змінній two_borrow. Це створює два змінні посилання в одній області видимості, що не дозволено. Коли ми запускаємо тести для нашої бібліотеки, код у переліку 15-23 скомпілюється без жодних помилок, але тест зазнає невдачі:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
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`

Зверніть увагу, що код викликав panic із повідомленням 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>>, і зберігаємо його в змінній з назвою 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` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Ця техніка досить гарна! Використовуючи RefCell<T>, ми маємо зовні незмінне значення List. Але ми можемо використовувати методи на RefCell<T>, які надають доступ до його внутрішньої змінності, щоб змінювати наші дані, коли нам це потрібно. Перевірки правил запозичення під час виконання захищають нас від станів гонки даних, і іноді варто обміняти трохи швидкості на цю гнучкість у наших структурах даних. Зверніть увагу, що RefCell<T> не працює для багатопотокового коду! Mutex<T> — це потокобезпечна версія RefCell<T>, і ми обговоримо Mutex<T> у розділі 16.