Czytałem rozdział poświęcony wcieleniom księgi Rdza i natknąłem się na ten przykład dla nazwanego / jawnego życia:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
Jest dla mnie jasne, że błąd jest uniemożliwiony przez kompilator jest stosowanie po zwolnieniu odniesienia przypisane do x
: po zakres wewnętrzna jest wykonywana, f
a zatem &f.x
stają się nieważne i nie powinny być przypisane dox
.
Moim problemem jest to, że problem można było łatwo przeanalizować bez użycia jawnego 'a
okresu istnienia, na przykład poprzez nielegalne przypisanie odwołania do szerszego zakresu (x = &f.x;
).
W jakich przypadkach rzeczywiście potrzebne są jawne czasy życia, aby zapobiec błędom użytkowania po zwolnieniu (lub innej klasie?)?
reference
rust
static-analysis
lifetime
corazza
źródło
źródło
Odpowiedzi:
Wszystkie pozostałe odpowiedzi mają istotne punkty ( konkretny przykład fjh, w którym potrzebne jest jawne życie ), ale brakuje jednej kluczowej rzeczy: dlaczego potrzebne są jawne czasy życia, kiedy kompilator powie ci, że się pomyliłeś ?
To jest właściwie to samo pytanie, „dlaczego potrzebne są typy jawne, skoro kompilator może je wywnioskować”. Hipotetyczny przykład:
Oczywiście kompilator może zobaczyć, że zwracam
&'static str
, więc dlaczego programista musi go wpisać?Głównym powodem jest to, że chociaż kompilator może zobaczyć, co robi twój kod, nie wie, jakie było twoje zamierzenie.
Funkcje są naturalną granicą zapory dla efektów zmiany kodu. Gdybyśmy pozwolili na całkowite sprawdzenie żywotności na podstawie kodu, wówczas niewinnie wyglądająca zmiana mogłaby wpłynąć na czasy życia, co mogłoby następnie spowodować błędy w funkcji daleko. To nie jest hipotetyczny przykład. Jak rozumiem, Haskell ma ten problem, gdy polegasz na wnioskowaniu o typie dla funkcji najwyższego poziomu. Rdza zdusiła ten szczególny problem w zarodku.
Kompilator zapewnia także korzyści związane z wydajnością - parsowane są tylko sygnatury funkcji w celu weryfikacji typów i czasów życia. Co ważniejsze, zapewnia programistom korzyści związane z wydajnością. Jeśli nie mielibyśmy jawnych okresów istnienia, co robi ta funkcja:
Nie można powiedzieć bez sprawdzenia źródła, co byłoby sprzeczne z ogromną liczbą najlepszych praktyk kodowania.
Zasadniczo zakresy są wcieleniami . Nieco bardziej wyraźnie, lifetime
'a
jest ogólnym parametrem lifetime, który może być wyspecjalizowany w określonym zakresie w czasie kompilacji, w oparciu o witrynę wywoływania.Ani trochę. Czasy życia są potrzebne, aby zapobiec błędom, ale jawne czasy życia są potrzebne, aby chronić to, co mają mało programiści rozsądku.
źródło
f x = x + 1
bez podpisu typu, którego używasz w innym module. Jeśli później zmienisz definicję naf x = sqrt $ x + 1
, jej typ zmieni się zNum a => a -> a
naFloating a => a -> a
, co spowoduje błędy typu we wszystkich witrynach wywoławczych, w którychf
jest wywoływany, np. ZInt
argumentem. Posiadanie podpisu typu gwarantuje, że błędy występują lokalnie.sqrt $
, po zmianie wystąpiłby tylko błąd lokalny, a nie wiele błędów w innych miejscach (co byłoby znacznie lepsze, gdybyśmy tego nie zrobili nie chcesz zmienić faktycznego typu)?Rzućmy okiem na następujący przykład.
Tutaj ważne są jawne czasy życia. Kompiluje się, ponieważ wynik
foo
ma taki sam okres istnienia jak pierwszy argument ('a
), więc może przeżyć drugi argument. Jest to wyrażone przez dożywotnie nazwy w podpisiefoo
. Jeśli przełączysz argumenty w wywołaniufoo
na kompilator, narzeka, żey
nie istnieje on wystarczająco długo:źródło
Adnotacja dożywotnia w następującej strukturze:
określa, że
Foo
instancja nie powinna przeżyć odwołania, które zawiera (x
pole).Przykład, na który natrafiłeś w książce Rust, nie ilustruje tego, ponieważ
f
iy
zmienne wychodzić z zakresu jednocześnie.Lepszym przykładem byłoby to:
Teraz
f
naprawdę przeżywa zmienną wskazywaną przezf.x
.źródło
Zauważ, że w tym fragmencie kodu nie ma wyraźnych okresów istnienia, z wyjątkiem definicji struktury. Kompilator doskonale potrafi wnioskować o żywotności
main()
.Jednak w definicjach typów nie można uniknąć jawnych okresów istnienia. Na przykład tutaj jest dwuznaczność:
Czy powinny to być różne czasy życia, czy powinny być takie same? Ma to znaczenie z punktu widzenia użytkowania,
struct RefPair<'a, 'b>(&'a u32, &'b u32)
różni się bardzo odstruct RefPair<'a>(&'a u32, &'a u32)
.Teraz, dla prostych przypadków, takich jak ten, który podałeś, kompilator może teoretycznie uchylać wcielenia tak, jak ma to miejsce w innych miejscach, ale takie przypadki są bardzo ograniczone i nie są warte dodatkowej złożoności w kompilatorze, a to zwiększenie przejrzystości byłoby co najmniej wątpliwe.
źródło
'static
,'static
może być stosowany wszędzie tam, gdzie można wykorzystać lokalne wcieleń, dlatego w swoim przykładziep
będzie musiał jej parametrem życia wywnioskować jako lokalnego życiay
.RefPair<'a>(&'a u32, &'a u32)
oznacza, że'a
będzie to przecięcie obu wejściowych okresów istnienia, tj. W tym przypadku okres istnieniay
.Obudowa z książki jest bardzo prosta z założenia. Temat wcieleń uważany jest za złożony.
Kompilator nie może łatwo wnioskować o czasie życia w funkcji z wieloma argumentami.
Ponadto moja opcjonalna skrzynia ma
OptionBool
typ zas_slice
metodą, której podpis jest w rzeczywistości:Nie ma absolutnie żadnej możliwości, aby kompilator mógł to rozgryźć.
źródło
Znalazłem inne świetne wyjaśnienie tutaj: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .
źródło
Jeśli funkcja otrzymuje dwa argumenty jako argumenty i zwraca odwołanie, wówczas implementacja funkcji może czasami zwrócić pierwsze odwołanie, a czasem drugie. Nie można przewidzieć, który numer referencyjny zostanie zwrócony dla danego połączenia. W takim przypadku nie można wnioskować o czasie życia dla zwróconego odwołania, ponieważ każde odwołanie do argumentu może odnosić się do innej zmiennej powiązanej z innym czasem życia. Jawne okresy życia pomagają uniknąć lub wyjaśnić taką sytuację.
Podobnie, jeśli struktura zawiera dwa odwołania (jako dwa pola elementów), wówczas funkcja elementu struktury może czasami zwracać pierwsze odwołanie, a czasem drugie. Ponownie jawne okresy życia zapobiegają takim dwuznacznościom.
W kilku prostych sytuacjach istnieje dożywotnia decyzja, w której kompilator może wnioskować o żywotności.
źródło
Powodem, dla którego twój przykład nie działa, jest po prostu fakt, że Rust ma tylko lokalny okres istnienia i wnioskowanie o typie. To, co sugerujesz, wymaga globalnego wnioskowania. Ilekroć masz odniesienie, którego życia nie można pominąć, należy je opatrzyć adnotacjami.
źródło
Jako nowicjusz Rust, rozumiem, że jawne czasy życia służą dwóm celom.
Umieszczenie wyraźnej adnotacji na całe życie na funkcji ogranicza rodzaj kodu, który może pojawić się wewnątrz tej funkcji. Jawne czasy życia pozwalają kompilatorowi upewnić się, że program robi to, co zamierzałeś.
Jeśli chcesz (kompilator) sprawdzić, czy fragment kodu jest poprawny, to (kompilator) nie będziesz musiał iteracyjnie sprawdzać każdej wywoływanej funkcji. Wystarczy spojrzeć na adnotacje funkcji, które są bezpośrednio wywoływane przez ten fragment kodu. To sprawia, że twój program jest o wiele łatwiejszy do uzasadnienia dla ciebie (kompilatora) i ułatwia zarządzanie czasami kompilacji.
W punkcie 1. Rozważmy następujący program napisany w języku Python:
który wydrukuje
Ten rodzaj zachowania zawsze mnie zaskakuje. To, co się dzieje,
df
polega na dzieleniu się pamięciąar
, więc gdy część treścidf
zmianwork
, ta zmiana również zarażaar
. Jednak w niektórych przypadkach może to być dokładnie to, czego chcesz, ze względu na wydajność pamięci (brak kopii). Prawdziwym problemem w tym kodzie jest to, że funkcjasecond_row
zwraca pierwszy wiersz zamiast drugiego; powodzenia w debugowaniu tego.Zamiast tego rozważ podobny program napisany w Rust:
Kompilujesz to
W rzeczywistości otrzymujesz dwa błędy, jest też jeden z rolami
'a
i'b
zamienionymi. Patrząc na adnotacjęsecond_row
, widzimy, że wyjście powinno być&mut &'b mut [i32]
, tzn. Wyjście powinno być odniesieniem do odwołania z czasem życia'b
(czas życia drugiego rzęduArray
). Ponieważ jednak zwracamy pierwszy wiersz (który ma okres istnienia'a
), kompilator skarży się na niedopasowanie czasu życia. We właściwym miejscu. We właściwym czasie. Debugowanie to pestka.źródło
Myślę, że adnotacja na całe życie jako umowa o danym ref była ważna tylko w zakresie odbiorczym, dopóki pozostaje ważna w zakresie źródłowym. Deklarowanie większej liczby referencji w tym samym cyklu życia łączy zakresy, co oznacza, że wszystkie referencje źródłowe muszą spełniać tę umowę. Taka adnotacja umożliwia kompilatorowi sprawdzenie wykonania umowy.
źródło