Najłatwiej jest zrozumieć, czym są nieleksykalne okresy życia, poprzez zrozumienie, czym są leksykalne wcielenia. W wersjach Rusta przed nieleksykalnymi okresami istnienia ten kod zakończy się niepowodzeniem:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Kompilator Rusta widzi, że scores
jest to zapożyczone przez score
zmienną, więc nie pozwala na dalszą mutację scores
:
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let score = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(4);
| ^^^^^^ mutable borrow occurs here
5 | }
| - immutable borrow ends here
Jednak człowiek może w trywialny sposób zobaczyć, że ten przykład jest zbyt konserwatywny: nigdy niescore
jest używany ! Problem w tym, że zapożyczenie scores
by score
jest leksykalne - trwa do końca bloku, w którym się znajduje:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Nieleksykalne okresy istnienia naprawiają ten problem, rozszerzając kompilator, aby rozumiał ten poziom szczegółowości. Kompilator może teraz dokładniej określić, kiedy potrzebne jest wypożyczenie, a ten kod zostanie skompilowany.
Cudowną rzeczą w nieleksykalnych wcieleniach jest to, że po ich włączeniu nikt nigdy o nich nie pomyśli . Stanie się po prostu „tym, co robi Rust” i (miejmy nadzieję) wszystko będzie działać.
Dlaczego dozwolone były leksykalne okresy życia?
Rust ma zezwalać na kompilację tylko znanych bezpiecznych programów. Jednak niemożliwe jest dokładne zezwolenie tylko na bezpieczne programy i odrzucenie tych niebezpiecznych. W tym celu Rust popełnia błąd konserwatywny: niektóre bezpieczne programy są odrzucane. Jednym z przykładów są wcielenia leksykalne.
Leksykalne czasy życia były znacznie łatwiejsze do zaimplementowania w kompilatorze, ponieważ znajomość bloków jest „trywialna”, podczas gdy znajomość przepływu danych jest mniejsza. Kompilator musiał zostać przepisany, aby wprowadzić i używać „reprezentacji pośredniej średniego poziomu” (MIR) . Następnie narzędzie sprawdzające pożyczkę (inaczej „pożyczkę”) musiało zostać przepisane tak, aby korzystało z MIR zamiast abstrakcyjnego drzewa składni (AST). Następnie zasady sprawdzającego pożyczkę musiały zostać dopracowane, aby były bardziej szczegółowe.
Leksykalne okresy życia nie zawsze przeszkadzają programiście, a istnieje wiele sposobów obchodzenia się z leksykalnymi okresami życia, nawet jeśli są irytujące. W wielu przypadkach wymagało to dodania dodatkowych nawiasów klamrowych lub wartości logicznej. Pozwoliło to Rust 1.0 na dostarczenie i użytkowanie przez wiele lat, zanim zaimplementowano nieleksykalne okresy istnienia.
Co ciekawe, pewne dobre wzorce zostały opracowane z powodu leksykalnych żywotów. Najlepszym przykładem jest dla mnie wzór . Ten kod kończy się niepowodzeniem przed nieleksykalnymi okresami istnienia i kompiluje się z nim:entry
fn example(mut map: HashMap<i32, i32>, key: i32) {
match map.get_mut(&key) {
Some(value) => *value += 1,
None => {
map.insert(key, 1);
}
}
}
Jednak ten kod jest nieefektywny, ponieważ dwukrotnie oblicza skrót klucza. Rozwiązanie, które powstało ze względu na leksykalne okresy życia, jest krótsze i wydajniejsze:
fn example(mut map: HashMap<i32, i32>, key: i32) {
*map.entry(key).or_insert(0) += 1;
}
Nazwa „nieleksykalne wcielenia” nie brzmi dla mnie dobrze
Okres istnienia wartości to przedział czasu, w którym wartość pozostaje pod określonym adresem pamięci (zobacz Dlaczego nie mogę przechowywać wartości i odwołania do tej wartości w tej samej strukturze ?, aby uzyskać dłuższe wyjaśnienie). Funkcja znana jako nieleksykalne okresy życia nie zmienia czasów życia żadnych wartości, więc nie może uczynić czasów życia nieleksykalnymi. Dzięki temu śledzenie i sprawdzanie pożyczek tych wartości jest tylko bardziej precyzyjne.
Bardziej dokładną nazwą funkcji mogą być „ zapożyczenia nieleksykalne ”. Niektórzy programiści kompilatorów odwołują się do podstawowej „pożyczki opartej na MIR”.
Nieleksykalne okresy istnienia nigdy nie miały być funkcją „widoczną dla użytkownika”, per se . W większości urosły w naszych umysłach z powodu małych wycinków, które otrzymujemy z ich nieobecności. Ich nazwa była przeznaczona głównie na wewnętrzne potrzeby rozwojowe, a zmiana na potrzeby marketingowe nigdy nie była priorytetem.
Tak, ale jak tego używać?
W wersji Rust 1.31 (wydanej 06.12.2018) musisz wyrazić zgodę na edycję Rust 2018 w swoim Cargo.toml:
[package]
name = "foo"
version = "0.0.1"
authors = ["An Devloper <an.devloper@example.com>"]
edition = "2018"
Od wersji Rust 1.36 wersja Rust 2015 umożliwia również nieleksykalne okresy istnienia.
Obecna implementacja nieleksykalnych okresów istnienia jest w „trybie migracji”. Jeśli narzędzie sprawdzania wypożyczeń NLL przejdzie pomyślnie, kompilacja będzie kontynuowana. Jeśli tak się nie stanie, wywoływany jest poprzedni program sprawdzający pożyczki. Jeśli stary program do sprawdzania wypożyczeń zezwala na kod, drukowane jest ostrzeżenie informujące, że kod prawdopodobnie zepsuje się w przyszłej wersji Rusta i powinien zostać zaktualizowany.
W nocnych wersjach Rusta możesz wyrazić zgodę na wymuszone uszkodzenie za pomocą flagi funkcji:
#![feature(nll)]
Możesz nawet wyrazić zgodę na eksperymentalną wersję NLL, używając flagi kompilatora -Z polonius
.
Próbka rzeczywistych problemów rozwiązanych przez nieleksykalne okresy życia