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?
lifetime
borrow-checker
rust
Shepmaster
quelle
quelle
Parent
undChild
könnte helfen ...Antworten:
Schauen wir uns eine einfache Implementierung an :
Dies wird mit dem Fehler fehlschlagen:
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::new
einige hypothetische Speicheradressen kommentieren , die zeigen, wo sich Werte befinden:Was soll passieren
child
? Wenn der Wert nur so verschobenparent
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::new
einige Zeilennummern kommentieren, mit denen wir die Lebensdauer hervorheben:Die konkrete Lebensdauer von
parent
beträgt 1 bis einschließlich 4 (die ich als darstellen werde[1,4]
). Die konkrete Lebensdauer vonchild
ist[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
child
selbst 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,child
vom 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:Hierbei wird die Lebensdauerelision verwendet , um das Schreiben expliziter generischer Lebensdauerparameter zu vermeiden . Es ist äquivalent zu:
In beiden Fällen gibt die Methode an, dass eine
Child
Struktur zurückgegeben wird, die mit der konkreten Lebensdauer von parametrisiert wurdeself
. Anders gesagt, dieChild
Instanz enthält einen Verweis auf die InstanzParent
, die sie erstellt hat, und kann daher nicht länger als dieseParent
Instanz leben.Dadurch können wir auch erkennen, dass etwas mit unserer Erstellungsfunktion wirklich nicht stimmt:
Obwohl Sie dies eher in einer anderen Form sehen:
In beiden Fällen wird kein Lebensdauerparameter über ein Argument bereitgestellt. Dies bedeutet, dass die Lebensdauer,
Combined
mit der parametrisiert wird, durch nichts eingeschränkt wird - es kann alles sein, was der Anrufer möchte. Dies ist unsinnig, da der Anrufer die'static
Lebensdauer 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
Rc
oderArc
.Mehr Informationen
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:
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
Option
in zwei Schritten verwenden: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:
Was ist mit
Pin
?Pin
, stabilisiert in Rust 1.33, hat dies in der Moduldokumentation :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):
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
Pin
der Tabelle hinzugefügt wird, ist eine übliche Methode, um anzugeben, dass sich ein bestimmter Wert garantiert nicht bewegt.Siehe auch:
quelle
Combined
besitzt,Child
was das besitztParent
. 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.Pin
ist 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.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
Session
und dasChannel
Ergebnis dieser Sitzung nebeneinander in eine Struktur zu stellen, um die Implementierungsdetails vor dem Benutzer zu verbergen. Beachten Sie jedoch, dass dieChannel
Definition'sess
in ihrer Typanmerkung die Lebensdauer hat , diesSession
jedoch 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
Session
Auß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 zuBox
, wodurch wir die folgende mögliche Lösung erhalten:Das Ergebnis dieses Codes ist, dass wir den nicht
Session
mehr verwenden können, aber er wird zusammen mit dem Code gespeichert, denChannel
wir verwenden werden. Da dasOwningHandle
Objekt beim Speichern in einer Struktur dereferenziertBox
, worauf dereferenziertChannel
, 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 überOwningHandle
Unsicherheit ziemlich nahe zu kommen scheint .Ein merkwürdiges Detail hier ist, dass das
Session
logisch eine ähnliche Beziehung hatTcpStream
wie esChannel
mussSession
, 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:Bei der
TcpStream
Verwendung liegt es also ganz beim Programmierer, die Richtigkeit des Codes sicherzustellen. Mit demOwningHandle
wird die Aufmerksamkeit auf den Ort gelenkt, an dem die "gefährliche Magie" geschiehtunsafe {}
.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.
quelle