RefCell<T> i wzorzec mutowalności wewnętrznej
Mutowalność wewnętrzna to wzorzec projektowy w Rust, który pozwala na
modyfikowanie danych nawet wtedy, gdy istnieją do nich niemutowalne referencje;
zazwyczaj takie działanie jest zabronione przez zasady pożyczania. Aby
modyfikować dane, wzorzec ten używa kodu unsafe wewnątrz struktury danych,
aby naginać zwykłe zasady Rust dotyczące mutowalności i pożyczania. Kod
niebezpieczny wskazuje kompilatorowi, że ręcznie sprawdzamy zasady, zamiast
polegać na tym, że kompilator sprawdzi je za nas; o kodzie niebezpiecznym
będziemy rozmawiać szerzej w Rozdziale 20.
Typy używające wzorca mutowalności wewnętrznej możemy stosować tylko wtedy,
gdy jesteśmy pewni, że zasady pożyczania będą przestrzegane w czasie
wykonania, mimo że kompilator nie może tego zagwarantować. Kod unsafe jest
wtedy opakowany w bezpieczne API, a typ zewnętrzny nadal pozostaje
iemutowalny.
Przyjrzyjmy się tej koncepcji, badając typ RefCell<T>, który stosuje wzorzec
mutowalności wewnętrznej.
Egzekwowanie zasad pożyczania w czasie wykonania
W przeciwieństwie do Rc<T>, typ RefCell<T> reprezentuje pojedynczą własność
danych, które przechowuje. Co zatem odróżnia RefCell<T> od typu takiego jak
Box<T>? Przypomnij sobie zasady pożyczania, których nauczyłeś się w Rozdziale 4:
- W dowolnym momencie możesz mieć albo jedną mutowalną referencję, albo dowolną liczbę niemutowalnych referencji (ale nie obie naraz).
- Referencje muszą być zawsze prawidłowe.
W przypadku referencji i Box<T>, niezmienniki zasad pożyczania są
egzekwowane w czasie kompilacji. W przypadku RefCell<T> te niezmienniki są
egzekwowane w czasie wykonania. W przypadku referencji, jeśli naruszysz te
zasady, otrzymasz błąd kompilacji. W przypadku RefCell<T>, jeśli naruszysz
te zasady, Twój program zostanie przerwany (panic) i zakończy działanie.
Zaletami sprawdzania zasad pożyczania w czasie kompilacji są to, że błędy zostaną wykryte wcześniej w procesie rozwoju, a także brak wpływu na wydajność w czasie wykonania, ponieważ cała analiza jest zakończona wcześniej. Z tych powodów sprawdzanie zasad pożyczania w czasie kompilacji jest najlepszym wyborem w większości przypadków, dlatego jest to domyślne zachowanie Rust.
Zaletą sprawdzania zasad pożyczania w czasie wykonania jest to, że pozwala to na pewne scenariusze bezpieczne pod względem pamięci, które zostałyby zabronione przez sprawdzenia w czasie kompilacji. Analiza statyczna, taka jak kompilator Rust, jest z natury konserwatywna. Niektóre właściwości kodu są niemożliwe do wykrycia poprzez analizę kodu: Najsłynniejszym przykładem jest Problem Zatrzymania, który wykracza poza zakres tej książki, ale jest ciekawym tematem do zbadania.
Ponieważ niektóre analizy są niemożliwe, jeśli kompilator Rust nie może być
pewien, że kod jest zgodny z zasadami własności, może odrzucić poprawny
program; w ten sposób jest konserwatywny. Gdyby Rust akceptował niepoprawny
program, użytkownicy nie mogliby ufać gwarancjom, jakie daje Rust. Jednak
jeśli Rust odrzuci poprawny program, programista będzie miał niedogodności,
ale nic katastrofalnego nie może się wydarzyć. Typ RefCell<T> jest użyteczny,
gdy jesteś pewien, że Twój kod jest zgodny z zasadami pożyczania, ale
kompilator nie jest w stanie tego zrozumieć i zagwarantować.
Podobnie jak Rc<T>, RefCell<T> jest przeznaczony wyłącznie do użytku w
scenariuszach jednowątkowych i spowoduje błąd kompilacji, jeśli spróbujesz go
użyć w kontekście wielowątkowym. O tym, jak uzyskać funkcjonalność RefCell<T>
w programie wielowątkowym, będziemy rozmawiać w Rozdziale 16.
Oto podsumowanie powodów, dla których warto wybrać Box<T>, Rc<T> lub
RefCell<T>:
Rc<T>umożliwia wielu właścicieli tych samych danych;Box<T>iRefCell<T>mają pojedynczych właścicieli.Box<T>pozwala na niemutowalne lub mutowalne pożyczenia sprawdzane w czasie kompilacji;Rc<T>pozwala tylko na niemutowalne pożyczenia sprawdzane w czasie kompilacji;RefCell<T>pozwala na niemutowalne lub mutowalne pożyczenia sprawdzane w czasie wykonania.- Ponieważ
RefCell<T>pozwala na mutowalne pożyczenia sprawdzane w czasie wykonania, możesz modyfikować wartość wewnątrzRefCell<T>, nawet gdyRefCell<T>jest niemutowalny.
Modyfikacja wartości wewnątrz niemutowalnej wartości to wzorzec mutowalności wewnętrznej. Przyjrzyjmy się sytuacji, w której mutowalność wewnętrzna jest przydatna i zbadajmy, jak jest to możliwe.
Użycie mutowalności wewnętrznej
Konsekwencją zasad pożyczania jest to, że gdy masz niemutowalną wartość, nie możesz jej mutowalnie pożyczyć. Na przykład, ten kod się nie skompiluje:
fn main() {
let x = 5;
let y = &mut x;
}
Gdybyś spróbował skompilować ten kod, otrzymałbyś następujący błąd:
$ 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
Istnieją jednak sytuacje, w których byłoby przydatne, aby wartość mogła
modyfikować się w swoich metodach, ale wydawała się niemutowalna dla innego
kodu. Kod poza metodami wartości nie mógłby modyfikować wartości. Użycie
RefCell<T> jest jednym ze sposobów na uzyskanie zdolności do mutowalności
wewnętrznej, ale RefCell<T> nie omija całkowicie zasad pożyczania: Sprawdzanie
pożyczania w kompilatorze pozwala na tę mutowalność wewnętrzną, a zasady
pożyczania są sprawdzane w czasie wykonania. Jeśli naruszysz zasady,
otrzymasz panic! zamiast błędu kompilacji.
Przejdźmy przez praktyczny przykład, w którym możemy użyć RefCell<T> do
modyfikacji niemutowalnej wartości i zobaczyć, dlaczego jest to przydatne.
Testowanie z obiektami mockowymi
Czasami podczas testowania programista używa jednego typu zamiast innego, aby obserwować określone zachowanie i upewnić się, że jest ono poprawnie zaimplementowane. Ten zastępczy typ nazywa się dublerem testowym. Pomyśl o nim w sensie dublera kaskaderskiego w filmie, gdzie osoba wchodzi i zastępuje aktora, aby wykonać szczególnie trudną scenę. Dublery testowe zastępują inne typy, gdy przeprowadzamy testy. Obiekty mockowe to specyficzne typy dublerów testowych, które rejestrują, co dzieje się podczas testu, aby można było stwierdzić, że podjęto prawidłowe działania.
Rust nie ma obiektów w tym samym sensie, co inne języki, i Rust nie ma funkcji obiektów mockowych wbudowanych w bibliotekę standardową, jak to robią niektóre inne języki. Możesz jednak z pewnością stworzyć strukturę, która będzie służyć tym samym celom, co obiekt mockowy.
Oto scenariusz, który przetestujemy: stworzymy bibliotekę, która śledzi wartość w stosunku do wartości maksymalnej i wysyła wiadomości w zależności od tego, jak blisko wartości maksymalnej jest obecna wartość. Ta biblioteka mogłaby być używana na przykład do śledzenia limitu użytkownika na liczbę wywołań API, które są dozwolone.
Nasza biblioteka będzie zapewniać jedynie funkcjonalność śledzenia, jak blisko
wartości maksymalnej jest wartość i jakie wiadomości powinny być wysyłane w
danych momentach. Aplikacje korzystające z naszej biblioteki będą musiały
dostarczyć mechanizm wysyłania wiadomości: Aplikacja może pokazać wiadomość
użytkownikowi bezpośrednio, wysłać e-mail, wysłać wiadomość tekstową lub zrobić
cokolwiek innego. Biblioteka nie musi znać tych szczegółów. Potrzebuje tylko
czegoś, co implementuje cechę, którą dostarczymy, nazwaną Messenger. Listing
15-20 pokazuje kod biblioteki.
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!");
}
}
}
Jedną ważną częścią tego kodu jest to, że cecha Messenger ma jedną metodę
o nazwie send, która przyjmuje niemutowalną referencję do self i tekst
wiadomości. Ta cecha jest interfejsem, który nasz obiekt mockowy musi
implementować, aby mock mógł być używany w ten sam sposób, co prawdziwy obiekt.
Druga ważna część to to, że chcemy przetestować zachowanie metody set_value
w LimitTracker. Możemy zmienić to, co przekazujemy jako parametr value,
ale set_value nie zwraca niczego, na czym moglibyśmy oprzeć asercje. Chcemy
być w stanie powiedzieć, że jeśli utworzymy LimitTracker z czymś, co
implementuje cechę Messenger i określoną wartością dla max, to messenger
otrzymuje polecenie wysłania odpowiednich wiadomości, gdy przekazujemy
różne liczby dla value.
Potrzebujemy obiektu mockowego, który zamiast wysyłać e-mail lub wiadomość
tekstową po wywołaniu send, będzie jedynie śledził wiadomości, które mu
kazano wysłać. Możemy utworzyć nową instancję obiektu mockowego, stworzyć
LimitTracker używający obiektu mockowego, wywołać metodę set_value w
LimitTracker, a następnie sprawdzić, czy obiekt mockowy zawiera wiadomości,
których oczekujemy. Listing 15-21 pokazuje próbę zaimplementowania obiektu
mockowego, który ma to robić, ale sprawdzający pożyczki na to nie zezwala.
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);
}
}
Ten kod testowy definiuje strukturę MockMessenger, która ma pole
sent_messages z wektorem wartości String, aby śledzić wiadomości, które ma
wysyłać. Definiujemy również skojarzoną funkcję new, aby ułatwić tworzenie
nowych wartości MockMessenger, które zaczynają się od pustej listy wiadomości.
Następnie implementujemy cechę Messenger dla MockMessenger, aby móc
przekazać MockMessenger do LimitTracker. W definicji metody send
pobieramy wiadomość przekazaną jako parametr i przechowujemy ją na liście
sent_messages MockMessenger.
W teście sprawdzamy, co dzieje się, gdy LimitTracker otrzymuje polecenie
ustawienia value na wartość większą niż 75 procent wartości max. Najpierw
tworzymy nowy MockMessenger, który rozpocznie się od pustej listy wiadomości.
Następnie tworzymy nowy LimitTracker i przekazujemy mu referencję do nowego
obiektu MockMessenger oraz wartość max równą 100. Wywołujemy metodę
set_value w LimitTracker z wartością 80, co stanowi ponad 75 procent z
100. Następnie twierdzimy, że lista wiadomości śledzonych przez
MockMessenger powinna teraz zawierać jedną wiadomość.
Jest jednak jeden problem z tym testem, jak pokazano tutaj:
$ 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
Nie możemy modyfikować MockMessenger, aby śledził wiadomości, ponieważ
metoda send przyjmuje niemutowalną referencję do self. Nie możemy również
skorzystać z sugestii komunikatu o błędzie, aby użyć &mut self zarówno w
metodzie impl, jak i w definicji cechy. Nie chcemy zmieniać cechy Messenger
wyłącznie dla celów testowania. Zamiast tego musimy znaleźć sposób, aby nasz
kod testowy działał poprawnie z naszym istniejącym projektem.
To sytuacja, w której mutowalność wewnętrzna może pomóc! Będziemy
przechowywać sent_messages wewnątrz RefCell<T>, a następnie metoda send
będzie mogła modyfikować sent_messages w celu przechowywania widzianych przez
nas wiadomości. Listing 15-22 pokazuje, jak to wygląda.
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);
}
}
Pole sent_messages jest teraz typu RefCell<Vec<String>> zamiast
Vec<String>. W funkcji new tworzymy nową instancję RefCell<Vec<String>>
wokół pustego wektora.
Dla implementacji metody send pierwszy parametr nadal jest niemutowalnym
pożyczeniem self, co odpowiada definicji cechy. Wywołujemy borrow_mut na
RefCell<Vec<String>> w self.sent_messages, aby uzyskać mutowalną referencję
do wartości wewnątrz RefCell<Vec<String>>, która jest wektorem. Następnie
możemy wywołać push na mutowalnej referencji do wektora, aby śledzić wiadomości
wysłane podczas testu.
Ostatnia zmiana, jaką musimy wprowadzić, dotyczy asercji: aby sprawdzić, ile
elementów jest w wewnętrznym wektorze, wywołujemy borrow na
RefCell<Vec<String>>, aby uzyskać niemutowalną referencję do wektora.
Teraz, gdy widziałeś, jak używać RefCell<T>, zagłębmy się w to, jak to działa!
Śledzenie pożyczeń w czasie wykonania
Podczas tworzenia niemutowalnych i mutowalnych referencji używamy odpowiednio
składni & i &mut. W przypadku RefCell<T> używamy metod borrow i
borrow_mut, które są częścią bezpiecznego API należącego do RefCell<T>. Metoda
borrow zwraca typ wskaźnika sprytnego Ref<T>, a borrow_mut zwraca typ
wskaźnika sprytnego RefMut<T>. Oba typy implementują Deref, więc możemy je
traktować jak zwykłe referencje.
RefCell<T> śledzi, ile wskaźników sprytnych Ref<T> i RefMut<T> jest
aktywnie używanych. Za każdym razem, gdy wywołujemy borrow, RefCell<T>
zwiększa licznik aktywnych niemutowalnych pożyczeń. Gdy wartość Ref<T> wyjdzie
poza zakres, licznik niemutowalnych pożyczeń zmniejsza się o 1. Podobnie jak
zasady pożyczania w czasie kompilacji, RefCell<T> pozwala nam na wiele
niemutowalnych pożyczeń lub jedno mutowalne pożyczenie w dowolnym momencie.
Jeśli spróbujemy naruszyć te zasady, zamiast otrzymania błędu kompilacji, jak
miałoby to miejsce w przypadku referencji, implementacja RefCell<T> spowoduje
panikę w czasie wykonania. Listing 15-23 pokazuje modyfikację implementacji
send z Listingu 15-22. Celowo próbujemy stworzyć dwa mutowalne pożyczenia
aktywne w tym samym zakresie, aby zilustrować, że RefCell<T> zapobiega temu
w czasie wykonania.
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);
}
}
Tworzymy zmienną one_borrow dla wskaźnika sprytnego RefMut<T> zwróconego
przez borrow_mut. Następnie tworzymy kolejne mutowalne pożyczenie w ten sam
sposób w zmiennej two_borrow. Tworzy to dwie mutowalne referencje w tym samym
zakresie, co jest niedozwolone. Kiedy uruchomimy testy dla naszej biblioteki,
kod z Listingu 15-23 skompiluje się bez błędów, ale test się nie powiedzie:
$ 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`
Zauważ, że kod spowodował panikę z komunikatem already borrowed: BorrowMutError. W ten sposób RefCell<T> obsługuje naruszenia zasad
pożyczania w czasie wykonania.
Decyzja o wychwytywaniu błędów pożyczania w czasie wykonania, a nie w czasie
kompilacji, jak to zrobiliśmy tutaj, oznacza, że potencjalnie możesz
znajdować błędy w swoim kodzie później w procesie rozwoju: być może dopiero po
wdrożeniu kodu do produkcji. Ponadto, Twój kod poniesie niewielką karę
wydajnościową w czasie wykonania w wyniku śledzenia pożyczeń w czasie
wykonania, a nie w czasie kompilacji. Jednakże, użycie RefCell<T> umożliwia
napisanie obiektu mockowego, który może modyfikować się, aby śledzić
wiadomości, które widział, podczas gdy używasz go w kontekście, w którym
dozwolone są tylko niemutowalne wartości. Możesz używać RefCell<T> pomimo
jego kompromisów, aby uzyskać większą funkcjonalność niż zapewniają zwykłe
referencje.
Zezwalanie na wielu właścicieli mutowalnych danych
Częstym sposobem użycia RefCell<T> jest połączenie go z Rc<T>. Przypomnijmy,
że Rc<T> pozwala na posiadanie wielu właścicieli danych, ale daje dostęp do
tych danych tylko w trybie niemutowalnym. Jeśli masz Rc<T>, który zawiera
RefCell<T>, możesz uzyskać wartość, która może mieć wielu właścicieli i którą
możesz modyfikować!
Na przykład, przypomnij sobie przykład listy konsensusowej z Listingu 15-18,
w którym użyliśmy Rc<T>, aby umożliwić wielu listom współdzielenie własności
innej listy. Ponieważ Rc<T> przechowuje tylko niemutowalne wartości, nie możemy
zmienić żadnej z wartości na liście po ich utworzeniu. Dodajmy RefCell<T>,
aby móc zmieniać wartości na listach. Listing 15-24 pokazuje, że używając
RefCell<T> w definicji Cons, możemy modyfikować wartość przechowywaną we
wszystkich listach.
#[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:?}");
}
Tworzymy wartość, która jest instancją Rc<RefCell<i32>> i przechowujemy ją
w zmiennej o nazwie value, abyśmy mogli uzyskać do niej bezpośredni dostęp
później. Następnie tworzymy List w a z wariantem Cons, który przechowuje
value. Musimy sklonować value, aby zarówno a, jak i value posiadały
własność wewnętrznej wartości 5, zamiast przenosić własność z value do a
lub aby a pożyczało z value.
Opakowujemy listę a w Rc<T>, aby po utworzeniu list b i c obie mogły
odnosić się do a, co zrobiliśmy w Listingu 15-18.
Po utworzeniu list w a, b i c, chcemy dodać 10 do wartości w value. Robimy
to, wywołując borrow_mut na value, co wykorzystuje funkcję automatycznego
rozpożyczania, którą omówiliśmy w „Gdzie jest operator ->?”
w Rozdziale 5, aby rozpożyczyć Rc<T> do wewnętrznej wartości RefCell<T>. Metoda
borrow_mut zwraca wskaźnik sprytny RefMut<T>, a my używamy na nim operatora
rozpożyczania i zmieniamy wewnętrzną wartość.
Kiedy wypiszemy a, b i c, widzimy, że wszystkie mają zmodyfikowaną
wartość 15 zamiast 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))
Ta technika jest całkiem sprytna! Używając RefCell<T>, mamy zewnętrznie
niemutowalną wartość List. Ale możemy użyć metod RefCell<T>, które zapewniają
dostęp do jego wewnętrznej mutowalności, dzięki czemu możemy modyfikować nasze
dane, gdy tego potrzebujemy. Sprawdzenia zasad pożyczania w czasie wykonania
chronią nas przed wyścigami danych, a czasami warto poświęcić trochę szybkości
dla tej elastyczności w naszych strukturach danych. Zauważ, że RefCell<T>
nie działa w kodzie wielowątkowym! Mutex<T> to wersja RefCell<T> bezpieczna
wielowątkowo, a o Mutex<T> będziemy rozmawiać w Rozdziale 16.