Co to są nieleksykalne okresy życia?

88

Rust ma dokument RFC dotyczący nieleksykalnych okresów istnienia, który został zatwierdzony do implementacji w języku przez długi czas. Ostatnio obsługa tej funkcji przez Rust znacznie się poprawiła i jest uważana za kompletną.

Moje pytanie brzmi: czym właściwie jest nieleksykalne życie?

Stargateur
źródło

Odpowiedzi:

130

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 scoresjest to zapożyczone przez scorezmienną, 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 scoresby scorejest 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);                 //
                                    // <-- score stops borrowing here
}

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 <[email protected]>"]
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

Shepmaster
źródło
11
Myślę, że warto by było podkreślić, że być może wbrew intuicji, nieleksykalne okresy życia nie dotyczą czasu życia zmiennych, ale czasu życia pożyczek. Lub, inaczej mówiąc, Non-Lexical Lifetimes dotyczy dekorelacji czasu życia zmiennych od czasu pożyczki ... chyba że się mylę? (ale nie sądzę, że NLL zmienia się po wykonaniu destruktora)
Matthieu M.
2
Co ciekawe, pewne dobre wzorce zostały opracowane z powodu leksykalnych okresów życia ” - przypuszczam zatem, że istnieje ryzyko, że istnienie NLL może znacznie utrudnić identyfikację przyszłych dobrych wzorców?
eggyal
1
@eggyal to z pewnością taka możliwość. Projektowanie w ramach zestawu ograniczeń (nawet arbitralnych!) Może prowadzić do nowych, interesujących projektów. Bez tych ograniczeń moglibyśmy oprzeć się na naszej istniejącej wiedzy i wzorcach i nigdy nie uczyć się ani nie badać, aby znaleźć coś nowego. To powiedziawszy, prawdopodobnie ktoś pomyśli „och, hash jest obliczany dwukrotnie, mogę to naprawić” i API zostałoby utworzone, ale użytkownikom może być trudniej znaleźć API. Mam nadzieję, że takie narzędzia jak clippy pomogą tym ludziom.
Shepmaster