Spójrzmy na prostą implementację tego :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Błąd zakończy się niepowodzeniem:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Aby całkowicie zrozumieć ten błąd, musisz pomyśleć o tym, jak wartości są reprezentowane w pamięci i co dzieje się, gdy przenosisz
te wartości. Dodajmy adnotacje za Combined::new
pomocą niektórych hipotetycznych adresów pamięci, które pokazują, gdzie znajdują się wartości:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Co powinno się stać child
? Jeśli wartość została właśnie przeniesiona tak parent
, jak była, wówczas odnosiłaby się do pamięci, w której nie można zagwarantować, że będzie zawierała prawidłową wartość. Każdy inny fragment kodu może przechowywać wartości pod adresem pamięci 0x1000. Dostęp do tej pamięci, zakładając, że jest to liczba całkowita, może prowadzić do awarii i / lub błędów bezpieczeństwa i jest jedną z głównych kategorii błędów, których zapobiega Rust.
To jest właśnie problem, który trwa przez całe życie zapobiega . Cykl życia to trochę metadanych, które pozwalają tobie i kompilatorowi wiedzieć, jak długo wartość będzie ważna w jej bieżącej lokalizacji w pamięci . To ważne rozróżnienie, ponieważ jest to powszechny błąd, jaki popełniają początkujący Rust. Okresy istnienia rdzy nie są okresem między stworzeniem obiektu a jego zniszczeniem!
Jako analogię, pomyśl o tym w ten sposób: w ciągu życia osoby będą przebywać w wielu różnych lokalizacjach, z których każda ma inny adres. Żywotność Rdzy dotyczy adresu, na którym obecnie przebywasz , a nie tego, kiedy umrzesz w przyszłości (chociaż śmierć również zmienia twój adres). Za każdym razem, gdy się przeprowadzasz, jest to istotne, ponieważ Twój adres jest już nieaktualny.
Ważne jest również, aby pamiętać, że życia nie zmieniają kodu; twój kod kontroluje czasy życia, twoje czasy życia nie kontrolują kodu. Rdzące powiedzenie brzmi: „życia są opisowe, a nie nakazowe”.
Adnotujmy za Combined::new
pomocą niektórych numerów linii, których użyjemy do podkreślenia żywotności:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
Życia beton z parent
wynosi od 1 do 4, włącznie (który będę reprezentować jako [1,4]
). Konkretny czas życia child
to[2,4]
, a konkretny czas życia wartości zwracanej to [4,5]
. Możliwe są konkretne czasy życia, które zaczynają się od zera - co reprezentowałoby czas życia parametru dla funkcji lub czegoś, co istniało poza blokiem.
Zauważ, że sam okres istnienia child
jest [2,4]
, ale odnosi się do wartości o okresie życia [1,4]
. Jest to w porządku, dopóki wartość referencyjna stanie się nieważna, zanim ta wartość się zmieni. Problem występuje, gdy próbujemy wrócićchild
z bloku. To „przedłużyłoby” żywotność poza jej naturalną długość.
Ta nowa wiedza powinna wyjaśniać dwa pierwsze przykłady. Trzeci wymaga spojrzenia na wdrożenie Parent::child
. Możliwe, że będzie to wyglądać mniej więcej tak:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Wykorzystuje dożywotnią eliminację, aby uniknąć zapisywania wyraźnych ogólnych parametrów dożywotnich . Jest to równoważne z:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
W obu przypadkach metoda mówi, że Child
zostanie zwrócona struktura, która została sparametryzowana na konkretny czas życia
self
. Inaczej mówiąc, Child
instancja zawiera odniesienie do tego, Parent
który ją utworzył, a zatem nie może dłużej żyć
Parent
instancja.
Pozwala nam to również rozpoznać, że coś jest naprawdę nie tak z naszą funkcją tworzenia:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Chociaż bardziej prawdopodobne jest, że zobaczysz to napisane w innej formie:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
W obu przypadkach argument nie zawiera parametru czasu życia. Oznacza to, że czas życia, który Combined
zostanie sparametryzowany, nie jest przez nic ograniczony - może być tym, czym chce osoba dzwoniąca. Jest to nonsensowne, ponieważ osoba dzwoniąca może określić'static
czas życia i nie ma możliwości spełnienia tego warunku.
Jak to naprawić?
Najłatwiejszym i najbardziej zalecanym rozwiązaniem jest to, aby nie próbować łączyć tych elementów w tej samej strukturze. W ten sposób zagnieżdżanie struktury naśladuje okres istnienia kodu. Umieść typy posiadające dane w strukturze razem, a następnie zapewnij metody, które pozwolą ci uzyskać odniesienia lub obiekty zawierające odniesienia w razie potrzeby.
Istnieje szczególny przypadek, w którym śledzenie życia jest nadgorliwe: gdy masz coś na stosie. Dzieje się tak, gdy używasz
Box<T>
na przykład . W takim przypadku przenoszona struktura zawiera wskaźnik na stos. Wskazana wartość pozostanie stabilna, ale adres samego wskaźnika się zmieni. W praktyce nie ma to znaczenia, ponieważ zawsze podążasz za wskaźnikiem.
Wynajem paka (JUŻ NIE utrzymana lub obsługiwane) lub owning_ref paka są sposoby reprezentujących tę sprawę, ale wymagają one, że adres bazowy nigdy poruszać . Wyklucza to mutowanie wektorów, co może spowodować realokację i przesunięcie wartości przydzielonych na stercie.
Przykłady problemów rozwiązanych w Wypożyczalni:
W innych przypadkach możesz przejść do pewnego rodzaju liczenia referencji, na przykład za pomocą Rc
lub Arc
.
Więcej informacji
parent
Dlaczego po przejściu do struktury, kompilator nie jest w stanie uzyskać nowego odwołania parent
i przypisać go do child
struktury?
Chociaż teoretycznie jest to możliwe, spowodowałoby to dużą złożoność i narzut. Za każdym razem, gdy obiekt jest przenoszony, kompilator musiałby wstawić kod, aby „naprawić” referencję. Oznaczałoby to, że kopiowanie struktury nie jest już bardzo tanią operacją, która po prostu przesuwa niektóre bity. Może to nawet oznaczać, że taki kod jest drogi, w zależności od tego, jak dobry byłby hipotetyczny optymalizator:
let a = Object::new();
let b = a;
let c = b;
Zamiast zmuszać to do każdego ruchu, programiści mogą wybrać, kiedy to się stanie, tworząc metody, które przyjmą odpowiednie referencje tylko wtedy, gdy je wywołasz.
Typ z odniesieniem do siebie
Jest jeden przypadek specyficzny, gdzie można utworzyć typ z odniesieniem do siebie. Musisz jednak użyć czegoś takiego, Option
aby zrobić to w dwóch krokach:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
W pewnym sensie to działa, ale utworzona wartość jest mocno ograniczona - nigdy nie można jej przenieść. W szczególności oznacza to, że nie można go zwrócić z funkcji ani przekazać wartości do niczego. Funkcja konstruktora pokazuje ten sam problem z czasem życia co powyżej:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Co Pin
?
Pin
, ustabilizowany w Rust 1.33, ma to w dokumentacji modułu :
Najlepszym przykładem takiego scenariusza byłoby budowanie struktur referencyjnych, ponieważ przesunięcie obiektu ze wskaźnikami do siebie unieważni je, co może spowodować niezdefiniowane zachowanie.
Należy zauważyć, że „autoreferencja” niekoniecznie oznacza użycie odniesienia . Rzeczywiście, przykład struktury autoreferencyjnej mówi konkretnie (moje podkreślenie):
Nie możemy poinformować o tym kompilatora za pomocą normalnego odniesienia, ponieważ wzorca tego nie można opisać zwykłymi regułami pożyczania. Zamiast tego używamy surowego wskaźnika , chociaż taki, o którym wiadomo, że nie ma wartości zerowej, ponieważ wiemy, że wskazuje na ciąg.
Możliwość użycia surowego wskaźnika dla tego zachowania istnieje od wersji Rust 1.0. Rzeczywiście, posiadanie-referencje i wynajem używają surowych wskaźników pod maską.
Jedyne, co Pin
dodaje do tabeli, jest powszechny sposób stwierdzenia, że dana wartość z pewnością się nie poruszy.
Zobacz też:
Parent
iChild
może pomóc ...