Mam wartość i chcę przechowywać tę wartość i odwołanie do czegoś wewnątrz tej wartości we własnym typie:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
Czasami mam wartość i chcę przechowywać tę wartość i odwołanie do tej wartości w tej samej strukturze:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
Czasami nawet nie biorę odwołania do wartości i pojawia się ten sam błąd:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
W każdym z tych przypadków pojawia się błąd, że jedna z wartości „nie żyje wystarczająco długo”. Co oznacza ten błąd?
lifetime
borrow-checker
rust
Shepmaster
źródło
źródło
Parent
iChild
może pomóc ...Odpowiedzi:
Spójrzmy na prostą implementację tego :
Błąd zakończy się niepowodzeniem:
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:Co powinno się stać
child
? Jeśli wartość została właśnie przeniesiona takparent
, 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:Życia beton z
parent
wynosi od 1 do 4, włącznie (który będę reprezentować jako[1,4]
). Konkretny czas życiachild
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:Wykorzystuje dożywotnią eliminację, aby uniknąć zapisywania wyraźnych ogólnych parametrów dożywotnich . Jest to równoważne z:
W obu przypadkach metoda mówi, że
Child
zostanie zwrócona struktura, która została sparametryzowana na konkretny czas życiaself
. 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:
Chociaż bardziej prawdopodobne jest, że zobaczysz to napisane w innej formie:
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
lubArc
.Więcej informacji
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:
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: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:
Co
Pin
?Pin
, ustabilizowany w Rust 1.33, ma to w dokumentacji modułu :Należy zauważyć, że „autoreferencja” niekoniecznie oznacza użycie odniesienia . Rzeczywiście, przykład struktury autoreferencyjnej mówi konkretnie (moje podkreślenie):
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ż:
źródło
Combined
jest właścicielem tego,Child
który jest właścicielemParent
. To może, ale nie musi mieć sensu, w zależności od rzeczywistych typów, które masz. Zwracanie odniesień do własnych danych wewnętrznych jest dość typowe.Pin
jest głównie sposobem na poznanie bezpieczeństwa struktury zawierającej wskaźnik referencyjny . Możliwość użycia surowego wskaźnika do tego samego celu istnieje od wersji Rust 1.0.Nieco innym problemem, który powoduje bardzo podobne komunikaty kompilatora, jest zależność czasu życia obiektu, zamiast przechowywania wyraźnego odwołania. Przykładem tego jest biblioteka ssh2 . Opracowując coś większego niż projekt testowy, kuszące jest, aby spróbować umieścić
Session
iChannel
pozyskać z tej sesji obok siebie w strukturze, ukrywając szczegóły implementacji przed użytkownikiem. Należy jednak pamiętać, żeChannel
definicja zawiera'sess
okres istnienia w adnotacji typu, podczas gdy jejSession
brak.Powoduje to podobne błędy kompilatora związane z czasem życia.
Jednym ze sposobów rozwiązania tego w bardzo prosty sposób jest zadeklarowanie strony
Session
zewnętrznej w dzwoniącym, a następnie dodanie adnotacji do referencji w strukturze przez całe życie, podobnie jak w odpowiedzi na ten post na forum użytkownika Rust mówiącego o tym samym problemie podczas enkapsulacji SFTP . Nie będzie to wyglądać elegancko i może nie zawsze mieć zastosowanie - ponieważ teraz masz do czynienia z dwoma podmiotami, a nie z jednym, który chciałeś!Okazuje się, że skrzynka z wypożyczalnią lub skrzynka z właścicielem są z drugiej odpowiedzi, są również rozwiązania tego problemu. Rozważmy owning_ref, który posiada specjalny przedmiot do dokładnego tym celu:
OwningHandle
. Aby uniknąć poruszania się obiektu bazowego, przydzielamy go na stercie za pomocą aBox
, co daje nam następujące możliwe rozwiązanie:Wynikiem tego kodu jest to, że nie możemy już go używać
Session
, ale jest on przechowywany razem z tym,Channel
którego będziemy używać. PonieważOwningHandle
obiekt odwołuje się doBox
, do którego odwołuje sięChannel
, podczas przechowywania go w strukturze, nazywamy go jako taki. UWAGA: to tylko moje zrozumienie. Podejrzewam, że może to nie być poprawne, ponieważ wydaje się, że jest to dość bliskie dyskusji na temat brakuOwningHandle
bezpieczeństwa .Ciekawym szczegółem jest to, że
Session
logicznie ma podobny związek z tym,TcpStream
coChannel
musiSession
, ale jego własność nie jest przejmowana i nie ma wokół tego adnotacji typu. Zamiast tego to użytkownik musi się tym zająć, ponieważ dokumentacja metody uzgadniania mówi:Tak więc przy
TcpStream
użyciu, programista jest całkowicie odpowiedzialny za zapewnienie poprawności kodu. Za pomocąOwningHandle
przyciąga się uwagę na to, gdzie dzieje się „niebezpieczna magia”unsafe {}
klocka .Dalsza i bardziej ogólna dyskusja na ten temat znajduje się w wątku Forum użytkowników Rust - który zawiera inny przykład i jego rozwiązanie z wykorzystaniem skrzynki do wypożyczenia, która nie zawiera niebezpiecznych bloków.
źródło