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> 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> i RefCell<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ątrz RefCell<T>, nawet gdy RefCell<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.