RefCell<T>
та Шаблон Внутрішньої Мутабельності
Внутрішня мутабельність - це шаблон проєктування в Rust, що дозволяє вам змінювати дані, навіть якщо є немутабельні посилання на ці дані; зазвичай, ця дія заборонена правилами позичання. Щоб змінювати дані, цей шаблон використовує небезпечний
код у структурі даних, щоб обійти звичайні правила Rust, що керують мутабельністю та позичанням. Небезпечний код повідомляє компілятор, що ми перевіряємо правила самостійно, а не покладаємося на компілятор, щоб перевіряти їх для нас; ми детальніше поговоримо про небезпечний код у Розділі 19.
Ми можемо використовувати типи, які використовують шаблон внутрішньої мутабельності тільки тоді, коли ми можемо гарантувати дотримання правил позичання під час виконання, тоді як компілятор не може гарантувати цього. Небезпечний
код застосовується загорнутим у безпечне API, і зовнішній тип лишається немутабельним.
Дослідимо цю концепцію, розглянувши тип Refell<T>
, який слідує шаблону внутрішньої мутабельності.
Забезпечення Дотримання Правил Позичання Впродовж Часу Виконання з RefCell<T>
На відміну від Rc<T>
, тип RefCell<T>
представляє єдине володіння даними, що він містить. То що ж відрізняє RefCell<T>
від типу на кшталт Box<T>
? Згадайте правила позичання, які ви вивчили у Розділі 4:
- У будь-який час можна мати або одне мутабельне посилання, <0>або</0> будь-яку кількість немутабельних посилань.
- Посилання завжди мають бути коректними.
За допомогою посилань і 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>
є немутабельним.
Зміна значення всередині немутабельного значення - це шаблон внутрішньої мутабельності. Подивімося на ситуацію, в якій внутрішня мутабельність корисна і дослідимо, як її використовувати.
Внутрішня Мутабельність: Мутабельне Позичання Немутабельного Значення
З правил запозичення випливає, що якщо ви маєте немутабельне значення, то ви не можете його мутабельно позичити. Наприклад, цей код не компілюється:
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
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error
Проте, Існують ситуації, в яких було б зручним для значення змінювати себе у своїх методах, але виглядати немутабельним для іншого коду. Код за межами методів цього значення не має можливості змінювати значення. Використання RefCell<T>
є одним зі способів отримати можливість внутрішньої мутабельності, але RefCell<T>
не повністю оминає правила позичання: borrow checker у компіляторі дозволяє цю внутрішню мутабельність, і правила позичання перевіряються під час виконання програми. Якщо ви порушите ці правила, ви отримаєте 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: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Важливою частиною цього коду є те, що трейт Messenger
має один метод, що зветься send
, який приймає немутабельне посилання на self
і текст повідомлення. Цей трейт є інтерфейсом нашого імітаційного об'єкта, який треба реалізувати, щоб імітаційний об'єкт можна було використовувати так само, як і реальний об'єкт. Іншою важливою частиною є те, що ми хочемо перевірити поведінку методу set_value
у LimitTracker
. Ми можемо змінити значення, що передається як параметр value
, але set_value
не поверне нам нічого, на чому можна робити тестові твердження. Ми хочемо мати змогу сказати, що якщо ми створюємо LimitTracker
з чимось, що реалізує трейт Messenger
і конкретним значенням max
, то коли ми передаємо різні числа для value
, месенджеру накажуть відправляти відповідні повідомлення.
Нам потрібен імітаційний об'єкт, який, замість того, щоб надіслати електронне або текстове повідомлення коли ми викликаємо send
, лише стежитиме за повідомленнями, які йому сказано надіслати. Ми можемо створити новий екземпляр імітаційного об'єкта, створити LimitTracker
, який використовує цей імітаційний об'єкт, викликати метод set_value
для LimitTracker
і перевірити, чи цей імітаційний об'єкт містить повідомлення, на які ми очікуємо. Блок коду 15-21 показує спробу реалізувати імітаційний об'єкт, що робить саме це, але borrow checker не дозволяє так робити:
Файл: 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);
}
}
Код цього тесту визначає структуру MockMessenger
, що має поле sent_messages
з Vec
, що складається з String
, щоб відстежувати повідомлення, які йому сказано. відправити. Ми також визначили асоційовану функцію new
, щоб зручно було створювати нові значення MockMessenger
, які на початку мають порожній список повідомлень. Далі ми реалізуємо трейт Messenger
для MockMessenger
, щоб можна було передати Messenger
до 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
|
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
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
Ми не можемо змінити 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: &'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>
, зануримось у те, як воно працює!
Відстеження Позичань Впродовж Часу Виконання з RefCell<T>
Створюючи немутабельні і мутабельні посилання, ви використовуємо, відповідно, записи &
та &mut
. Для RefCell<T>
ми використовуємо методи borrow
і borrow_mut
, які є частиною безпечного API RefCell<T>
. Метод borrow
повертає розумний вказівник типу Ref<T>
, а borrow_mut
повертає розумний вказівник типу RefMut<T>
. Обидва типи реалізують Deref
, тому ми можемо працювати з ними як зі звичайними посиланнями.
RefCell<T>
відстежує, скільки є активних розумних вказівників Ref<T>
і Refut<T>
у кожен момент. Кожного разу коли ми викликаємо borrow
, RefCell<T>
збільшує кількість активних немутабельних позичань. Коли значення Ref<T>
виходить з області видимості, кількість немутабельних позичань зменшується на один. Так само як правила позичання часу компіляції, Refell<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);
}
}
Ми створюємо змінну 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 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 '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>
Звичайний спосіб використання e RefCell<T>
- комбінація з Rc<T>
. Згадайте, що Rc<T>
дозволяє мати кілька володільців одних даних, але надає лише немутабельний доступ до цих даних. Якщо ви маєте Rc<T>
, що містить RefCell<T>
, ви можете отримати значення, що може мати кількох власників і яке ви можете змінювати!
Наприклад, згадайте приклад зі списком cons у Блоці коду 15-18, де ми використовували Rc<T>
, щоб дозволити декільком спискам ділитися володінням іншим списком. Оскільки Rc<T>
має лише немутабельні значення, ми не можемо змінити жодне зі значень у списку після їх створення. Додамо RefCell<T>
, щоб отримати можливість змінити значення у списках. Блок коду 15-24 показує, що використовуючи Refell<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); }
Ми створюємо значення, що є екземпляром 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>
. Метод e borrow_mut
повертає розумний вказівник RefMut<T>
, і ми використовуємо на ньому оператор розіменування та змінюємо внутрішнє значення.
Коли ми виводимо a
, b
та c
, ми бачимо, що всі вони мають змінене значення 10 замість 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>
, які надають доступ до його внутрішньої мутабельності, тож ми можемо змінювати наші дані в разі потреби. Перевірка правил запозичення часу виконання захищає нас від гонитви даних, і це іноді варто виміняти на крихту швидкості швидкістю заради цієї гнучкості в наших структурах даних. Зверніть увагу, що RefCell<T>
не працює в багатопотоковому коді! Mutex<T>
є потоково-безпечною версією Refell<T>
, і ми обговоримо Mutex<T>
в Розділі 16.