Warum kann ich einen Wert und einen Verweis auf diesen Wert nicht in derselben Struktur speichern?


222

Ich habe einen Wert und möchte diesen Wert und einen Verweis auf etwas in diesem Wert in meinem eigenen Typ speichern:

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)
}

Manchmal habe ich einen Wert und möchte diesen Wert und einen Verweis auf diesen Wert in derselben Struktur speichern:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

Manchmal nehme ich nicht einmal eine Referenz des Werts und erhalte den gleichen Fehler:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

In jedem dieser Fälle erhalte ich die Fehlermeldung, dass einer der Werte "nicht lange genug lebt". Was bedeutet dieser Fehler?


1
Für das letztere Beispiel eine Definition von Parentund Childkönnte helfen ...
Matthieu M.

1
@MatthieuM. Ich habe darüber diskutiert, mich aber aufgrund der beiden miteinander verbundenen Fragen dagegen entschieden. Keine dieser Fragen befasste sich mit der Definition der Struktur oder der fraglichen Methode, daher dachte ich, es wäre am besten nachzuahmen, dass die Leute diese Frage leichter an ihre eigene Situation anpassen können. Beachten Sie, dass ich tun die Methodensignatur in der Antwort zeigen.
Shepmaster

Antworten:


245

Schauen wir uns eine einfache Implementierung an :

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() {}

Dies wird mit dem Fehler fehlschlagen:

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`

Um diesen Fehler vollständig zu verstehen, müssen Sie darüber nachdenken , wie sich die Werte im Speicher dargestellt und was passiert , wenn Sie bewegen diese Werte. Lassen Sie uns Combined::neweinige hypothetische Speicheradressen kommentieren , die zeigen, wo sich Werte befinden:

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 ... ?

Was soll passieren child? Wenn der Wert nur so verschoben parent wurde, wie er war, bezieht er sich auf den Speicher, in dem kein gültiger Wert mehr garantiert ist. Jeder andere Code darf Werte unter der Speicheradresse 0x1000 speichern. Der Zugriff auf diesen Speicher unter der Annahme, dass es sich um eine Ganzzahl handelt, kann zu Abstürzen und / oder Sicherheitslücken führen und ist eine der Hauptkategorien von Fehlern, die Rust verhindert.

Dies ist genau das Problem, das Lebensdauern verhindern. Eine Lebensdauer besteht aus einigen Metadaten, mit denen Sie und der Compiler wissen können, wie lange ein Wert an seinem aktuellen Speicherort gültig ist . Das ist eine wichtige Unterscheidung, da es ein häufiger Fehler ist, den Rust-Neulinge machen. Rostlebensdauern sind nicht der Zeitraum zwischen der Erstellung eines Objekts und seiner Zerstörung!

Stellen Sie sich das als Analogie folgendermaßen vor: Während des Lebens einer Person werden sie sich an vielen verschiedenen Orten aufhalten, von denen jeder eine eigene Adresse hat. Eine Rust-Lebensdauer bezieht sich auf die Adresse, unter der Sie derzeit wohnen , und nicht darauf, wann Sie in Zukunft sterben werden (obwohl das Sterben auch Ihre Adresse ändert). Jedes Mal, wenn Sie umziehen, ist dies relevant, da Ihre Adresse nicht mehr gültig ist.

Es ist auch wichtig zu beachten, dass die Lebensdauer Ihren Code nicht ändert. Ihr Code steuert die Lebensdauern, Ihre Lebensdauern steuern nicht den Code. Das markige Sprichwort lautet: "Lebenszeiten sind beschreibend, nicht vorschreibend".

Lassen Sie uns Combined::neweinige Zeilennummern kommentieren, mit denen wir die Lebensdauer hervorheben:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

Die konkrete Lebensdauer von parentbeträgt 1 bis einschließlich 4 (die ich als darstellen werde [1,4]). Die konkrete Lebensdauer von childist [2,4]und die konkrete Lebensdauer des Rückgabewerts ist [4,5]. Es ist möglich, konkrete Lebensdauern zu haben, die bei Null beginnen - das würde die Lebensdauer eines Parameters für eine Funktion oder etwas darstellen, das außerhalb des Blocks existiert.

Beachten Sie, dass die Lebensdauer von sich childselbst ist [2,4], sich jedoch auf einen Wert mit einer Lebensdauer von bezieht[1,4] . Dies ist in Ordnung, solange der verweisende Wert ungültig wird, bevor der referenzierte Wert dies tut. Das Problem tritt auf, wenn wir versuchen, childvom Block zurückzukehren. Dies würde die Lebensdauer über ihre natürliche Länge hinaus "verlängern".

Dieses neue Wissen sollte die ersten beiden Beispiele erklären. Der dritte erfordert einen Blick auf die Implementierung von Parent::child. Die Chancen stehen gut, dass es ungefähr so ​​aussieht:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

Hierbei wird die Lebensdauerelision verwendet , um das Schreiben expliziter generischer Lebensdauerparameter zu vermeiden . Es ist äquivalent zu:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

In beiden Fällen gibt die Methode an, dass eine ChildStruktur zurückgegeben wird, die mit der konkreten Lebensdauer von parametrisiert wurde self. Anders gesagt, die ChildInstanz enthält einen Verweis auf die Instanz Parent, die sie erstellt hat, und kann daher nicht länger als diese ParentInstanz leben.

Dadurch können wir auch erkennen, dass etwas mit unserer Erstellungsfunktion wirklich nicht stimmt:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

Obwohl Sie dies eher in einer anderen Form sehen:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

In beiden Fällen wird kein Lebensdauerparameter über ein Argument bereitgestellt. Dies bedeutet, dass die Lebensdauer, Combinedmit der parametrisiert wird, durch nichts eingeschränkt wird - es kann alles sein, was der Anrufer möchte. Dies ist unsinnig, da der Anrufer die 'staticLebensdauer angeben kann und es keine Möglichkeit gibt, diese Bedingung zu erfüllen.

Wie behebe ich das?

Die einfachste und am meisten empfohlene Lösung besteht darin, nicht zu versuchen, diese Elemente in derselben Struktur zusammenzufügen. Auf diese Weise ahmt Ihre Strukturverschachtelung die Lebensdauer Ihres Codes nach. Platzieren Sie Typen, die Daten besitzen, in einer Struktur und stellen Sie dann Methoden bereit, mit denen Sie nach Bedarf Referenzen oder Objekte mit Referenzen abrufen können.

Es gibt einen Sonderfall, in dem die Lebensdauersuche übereifrig ist: Wenn Sie etwas auf den Haufen gelegt haben. Dies tritt auf, wenn Sie Box<T>beispielsweise a verwenden. In diesem Fall enthält die verschobene Struktur einen Zeiger auf den Heap. Der Wert, auf den gezeigt wird, bleibt stabil, aber die Adresse des Zeigers selbst wird verschoben. In der Praxis spielt dies keine Rolle, da Sie immer dem Zeiger folgen.

Die Mietkiste (NICHT MEHR WARTET ODER UNTERSTÜTZT) oder die owning_ref-Kiste stellen diesen Fall dar, erfordern jedoch, dass sich die Basisadresse niemals bewegt . Dies schließt mutierende Vektoren aus, die eine Neuzuweisung und eine Verschiebung der Heap-zugewiesenen Werte verursachen können.

Beispiele für Probleme, die mit Rental gelöst wurden:

In anderen Fällen möchten Sie möglicherweise zu einer Art Referenzzählung wechseln, z. B. mit Rcoder Arc.

Mehr Informationen

parentWarum kann der Compiler nach dem Verschieben in die Struktur keinen neuen Verweis auf die Struktur abrufen parentund dieser childin der Struktur zuweisen ?

Während dies theoretisch möglich ist, würde dies eine große Menge an Komplexität und Overhead mit sich bringen. Jedes Mal, wenn das Objekt verschoben wird, muss der Compiler Code einfügen, um die Referenz zu "reparieren". Dies würde bedeuten, dass das Kopieren einer Struktur keine sehr billige Operation mehr ist, bei der nur einige Bits verschoben werden. Es könnte sogar bedeuten, dass Code wie dieser teuer ist, je nachdem, wie gut ein hypothetischer Optimierer wäre:

let a = Object::new();
let b = a;
let c = b;

Anstatt dies für jede Bewegung zu erzwingen , kann der Programmierer auswählen, wann dies geschehen soll, indem er Methoden erstellt, die nur dann die entsprechenden Referenzen verwenden, wenn Sie sie aufrufen.

Ein Typ mit einem Verweis auf sich selbst

Es gibt einen speziellen Fall, in dem Sie einen Typ mit einem Verweis auf sich selbst erstellen können . Sie müssen jedoch so etwas wie Optionin zwei Schritten verwenden:

#[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);
}

Dies funktioniert in gewissem Sinne, aber der geschaffene Wert ist stark eingeschränkt - er kann niemals verschoben werden. Dies bedeutet insbesondere, dass es nicht von einer Funktion zurückgegeben oder als Wert an irgendetwas übergeben werden kann. Eine Konstruktorfunktion zeigt das gleiche Problem mit den Lebensdauern wie oben:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

Was ist mit Pin?

Pin, stabilisiert in Rust 1.33, hat dies in der Moduldokumentation :

Ein Paradebeispiel für ein solches Szenario wäre das Erstellen selbstreferenzieller Strukturen, da das Verschieben eines Objekts mit Zeigern auf sich selbst diese ungültig macht, was zu undefiniertem Verhalten führen kann.

Es ist wichtig zu beachten, dass "selbstreferenziell" nicht unbedingt die Verwendung einer Referenz bedeutet . In der Tat sagt das Beispiel einer selbstreferenziellen Struktur ausdrücklich (Hervorhebung von mir):

Wir können den Compiler nicht mit einer normalen Referenz darüber informieren, da dieses Muster nicht mit den üblichen Ausleihregeln beschrieben werden kann. Stattdessen verwenden wir einen Rohzeiger , von dem bekannt ist, dass er nicht null ist, da wir wissen, dass er auf die Zeichenfolge zeigt.

Die Möglichkeit, einen Rohzeiger für dieses Verhalten zu verwenden, besteht seit Rust 1.0. In der Tat verwenden Eigentümer-Ref und Vermietung rohe Zeiger unter der Haube.

Das einzige, was Pinder Tabelle hinzugefügt wird, ist eine übliche Methode, um anzugeben, dass sich ein bestimmter Wert garantiert nicht bewegt.

Siehe auch:


1
Wird so etwas ( is.gd/wl2IAt ) als idiomatisch angesehen? Dh, um die Daten über Methoden anstelle der Rohdaten verfügbar zu machen.
Peter Hall

2
@ PeterHall sicher, es bedeutet nur, dass das Combinedbesitzt, Childwas das besitzt Parent. Das kann sinnvoll sein oder auch nicht, abhängig von den tatsächlichen Typen, die Sie haben. Das Zurückgeben von Verweisen auf Ihre eigenen internen Daten ist ziemlich typisch.
Shepmaster

Was ist die Lösung für das Heap-Problem?
Derekdreery

@derekdreery vielleicht könntest du deinen Kommentar erweitern? Warum reicht der gesamte Absatz über die owning_ref- Kiste nicht aus?
Shepmaster

1
@FynnBecker Es ist immer noch unmöglich, eine Referenz und einen Wert für diese Referenz zu speichern . Pinist meistens eine Möglichkeit, die Sicherheit einer Struktur zu ermitteln, die einen selbstreferenziellen Zeiger enthält . Die Möglichkeit, einen Rohzeiger für denselben Zweck zu verwenden, besteht seit Rust 1.0.
Shepmaster

4

Ein etwas anderes Problem, das sehr ähnliche Compilermeldungen verursacht, ist die Abhängigkeit von der Objektlebensdauer, anstatt eine explizite Referenz zu speichern. Ein Beispiel dafür ist die ssh2- Bibliothek. Wenn Sie etwas Größeres als ein Testprojekt entwickeln, ist es verlockend zu versuchen, das Sessionund das ChannelErgebnis dieser Sitzung nebeneinander in eine Struktur zu stellen, um die Implementierungsdetails vor dem Benutzer zu verbergen. Beachten Sie jedoch, dass die ChannelDefinition 'sessin ihrer Typanmerkung die Lebensdauer hat , dies Sessionjedoch nicht.

Dies führt zu ähnlichen Compilerfehlern in Bezug auf die Lebensdauer.

Eine Möglichkeit, dies auf sehr einfache Weise zu lösen, besteht darin, die SessionAußenseite im Aufrufer zu deklarieren und dann die Referenz innerhalb der Struktur mit einer Lebensdauer zu versehen, ähnlich der Antwort in diesem Beitrag im Rust-Benutzerforum, in der über dasselbe Problem beim Einkapseln von SFTP gesprochen wird . Dies sieht nicht elegant aus und trifft möglicherweise nicht immer zu - denn jetzt müssen Sie sich mit zwei Entitäten befassen, anstatt mit einer, die Sie wollten!

Stellt sich heraus , die Miete Kiste oder der owning_ref Kiste von der anderen Antwort sind die Lösungen auch für dieses Problem. Betrachten wir die owning_ref, die genau für diesen Zweck das spezielle Objekt hat : OwningHandle. Um zu vermeiden, dass sich das zugrunde liegende Objekt bewegt, ordnen wir es dem Heap mit a zu Box, wodurch wir die folgende mögliche Lösung erhalten:

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
    }
}

Das Ergebnis dieses Codes ist, dass wir den nicht Sessionmehr verwenden können, aber er wird zusammen mit dem Code gespeichert, den Channelwir verwenden werden. Da das OwningHandleObjekt beim Speichern in einer Struktur dereferenziert Box, worauf dereferenziert Channel, nennen wir es als solches. HINWEIS: Dies ist nur mein Verständnis. Ich habe den Verdacht, dass dies möglicherweise nicht korrekt ist, da es der Diskussion über OwningHandleUnsicherheit ziemlich nahe zu kommen scheint .

Ein merkwürdiges Detail hier ist, dass das Sessionlogisch eine ähnliche Beziehung hat TcpStreamwie es Channelmuss Session, aber sein Eigentum nicht übernommen wird und es keine Typanmerkungen gibt, die dies tun. Stattdessen ist es Sache des Benutzers, sich darum zu kümmern, wie in der Dokumentation der Handshake- Methode angegeben:

Diese Sitzung übernimmt keinen Besitz des bereitgestellten Sockets. Es wird empfohlen, sicherzustellen, dass der Socket die Lebensdauer dieser Sitzung beibehält, um sicherzustellen, dass die Kommunikation ordnungsgemäß ausgeführt wird.

Es wird außerdem dringend empfohlen, den bereitgestellten Stream für die Dauer dieser Sitzung nicht gleichzeitig an anderer Stelle zu verwenden, da dies das Protokoll beeinträchtigen kann.

Bei der TcpStreamVerwendung liegt es also ganz beim Programmierer, die Richtigkeit des Codes sicherzustellen. Mit dem OwningHandlewird die Aufmerksamkeit auf den Ort gelenkt, an dem die "gefährliche Magie" geschieht unsafe {}.

Eine weitere und ausführlichere Diskussion dieses Problems finden Sie in diesem Thread des Rust User's Forum, der ein anderes Beispiel und dessen Lösung unter Verwendung der Mietkiste enthält, die keine unsicheren Blöcke enthält.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.