Jednym z głównych celów Rust jest zapobieganie zmianom w danych niemutowalnych oraz unikanie wyścigów danych na etapie kompilacji. W normalnych warunkach Rust nie pozwala na zmianę danych przez niemutowalną referencję. Jednak w systemach z pamięcią podręczną, leniwym obliczaniem lub logiką wymagającą zmiany wewnętrznego stanu za pomocą referencji, może to być konieczne. Dlatego powstał wzorzec interior mutability.
Bez interior mutability wdrożenie pamięci podręcznej, leniwej inicjalizacji oraz wielu innych idiomów jest trudne lub niemożliwe, zachowując bezpieczne posiadanie i referencje. Klasycznym przykładem jest pamięć podręczna wewnątrz funkcji, która zapewnia zewnętrznemu światu tylko niemutowalną referencję do siebie.
W Rust istnieją specjalne typy — Cell i RefCell (oraz ich odpowiedniki dla wielowątkowości), które pozwalają na zmianę wewnętrznej wartości nawet przez niemutowalną referencję, kontrolując bezpieczeństwo na etapie wykonywania, a nie kompilacji.
Cell<T> — to typ kopii, umożliwiający zmianę lub odczyt wartości bez używania referencji, ale tylko dla typów implementujących Copy.RefCell<T> — pozwala na uzyskanie mutowalnej referencji "w locie" nawet przy posiadaniu tylko niemutowalnej zewnętrznej referencji, ale jeśli spróbujesz uzyskać jednocześnie dwie mutowalne lub jedną mutowalną i kilka niemutowalnych — nastąpi panika w trakcie wykonywania.Przykład kodu:
use std::cell::RefCell; struct Foo { cache: RefCell<Option<u32>>, } impl Foo { fn get_or_compute(&self) -> u32 { if let Some(val) = *self.cache.borrow() { return val; } let computed = 42; *self.cache.borrow_mut() = Some(computed); computed } }
Kluczowe cechy:
Czy możemy bezpiecznie używać RefCell w strukturach wielowątkowych?
Nie, RefCell nie jest bezpieczny w użyciu w wątkach. Do pracy w środowisku wielowątkowym użyj Mutex lub RwLock.
Czy można zwrócić referencję do zawartości Cell<T>?
Nie, Cell nie wydaje żadnych referencji, tylko kopiuje lub aktualizuje wartość. Działa tylko z typami Copy, dla wszystkich innych użyj RefCell.
Co się stanie, jeśli wywołasz borrow_mut dwukrotnie z rzędu na tym samym RefCell?
Wystąpi panika w czasie wykonywania, ponieważ RefCell śledzi liczbę aktywnych referencji. Druga próba uzyskania mutowalnego dostępu przy już istniejącej referencji spowoduje panikę.
W projekcie do przechowywania wewnątrz struktury wszelkiego zmiennego stanu zawsze stosuje się RefCell, nawet jeśli można użyć mutowalnego posiadania. Kod staje się rozwlekły, pojawiają się paniki w czasie wykonywania, a testowanie staje się trudne.
Zalety: Można szybko obejść ograniczenia kompilatora i osiągnąć pożądane zachowanie
Wady: Wysokie ryzyko błędów na etapie wykonania, awaria aplikacji, trudna debugowanie logiki
RefCell używa się tylko do implementacji leniwych pamięci podręcznych w dużych strukturach, a w pozostałej części kodu pozostaje przy klasycznym posiadaniu i referencjach. Wszystkie wartości są poprawnie czyszczone, nie ma panik.
Zalety: Przejrzysta logika, minimalizacja użycia interior mutability, kod jest odporny i przewidywalny
Wady: Wymaga uwagi na granice zmiany danych: odczyt pamięci podręcznej jest zawsze bezpieczny, zapis tylko w uzasadnionych przypadkach i w wąsko określonych miejscach