Dlaczego nie mogę przechowywać wartości i odwołania do tej wartości w tej samej strukturze?


222

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?


1
Dla drugiego przykładu definicja Parenti Childmoże pomóc ...
Matthieu M.

1
@MatthieuM. Dyskutowałem o tym, ale zdecydowałem się go nie opierać na dwóch połączonych pytaniach. Żadne z tych pytań nie dotyczyło definicji struktury ani omawianej metody, więc pomyślałem, że najlepiej byłoby naśladować, że ludzie mogą łatwiej dopasować to pytanie do swojej sytuacji. Zauważ, że ja zrobić pokazać podpis metoda w odpowiedzi.
Shepmaster

Odpowiedzi:


245

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::newpomocą 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::newpomocą 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 parentwynosi od 1 do 4, włącznie (który będę reprezentować jako [1,4]). Konkretny czas życia childto[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 childjest [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 Childzostanie zwrócona struktura, która została sparametryzowana na konkretny czas życia self. Inaczej mówiąc, Childinstancja zawiera odniesienie do tego, Parentktó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 Combinedzostanie 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ą Rclub Arc.

Więcej informacji

parentDlaczego po przejściu do struktury, kompilator nie jest w stanie uzyskać nowego odwołania parenti przypisać go do childstruktury?

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, Optionaby 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ż:


1
Czy coś takiego ( is.gd/wl2IAt ) jest uważane za idiomatyczne? Tj., Aby udostępnić dane metodami zamiast surowych danych.
Peter Hall

2
@PeterHall na pewno oznacza to, że Combinedjest właścicielem tego, Childktóry jest właścicielem Parent. 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.
Shepmaster

Jakie jest rozwiązanie problemu ze stertą?
derekdreery

@derekdreery może mógłbyś rozwinąć swój komentarz? Dlaczego cały akapit mówi o skrzynce owning_ref jest niewystarczający?
Shepmaster

1
@FynnBecker nadal nie można zapisać odwołania i wartości do tego odwołania. Pinjest 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.
Shepmaster,

4

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ć Sessioni Channelpozyskać z tej sesji obok siebie w strukturze, ukrywając szczegóły implementacji przed użytkownikiem. Należy jednak pamiętać, że Channeldefinicja zawiera 'sessokres istnienia w adnotacji typu, podczas gdy jej Sessionbrak.

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 Sessionzewnę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ą a Box, co daje nam następujące możliwe rozwiązanie:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Wynikiem tego kodu jest to, że nie możemy już go używać Session, ale jest on przechowywany razem z tym, Channelktórego będziemy używać. Ponieważ OwningHandleobiekt odwołuje się do Box, 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 braku OwningHandlebezpieczeństwa .

Ciekawym szczegółem jest to, że Sessionlogicznie ma podobny związek z tym, TcpStreamco Channelmusi Session, 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:

Ta sesja nie przejmuje na własność podanego gniazda, zaleca się upewnić się, że gniazdo utrzymuje trwałość tej sesji, aby zapewnić prawidłowe wykonanie komunikacji.

Zaleca się również, aby dostarczony strumień nie był używany jednocześnie w innym miejscu podczas trwania tej sesji, ponieważ może to zakłócać protokół.

Tak więc przy TcpStreamużyciu, programista jest całkowicie odpowiedzialny za zapewnienie poprawności kodu. Za pomocą OwningHandleprzycią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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.