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::new
einige 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::new
einige 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 parent
beträgt 1 bis einschließlich 4 (die ich als darstellen werde [1,4]
). Die konkrete Lebensdauer von child
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:
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 Child
Struktur zurückgegeben wird, die mit der konkreten Lebensdauer von parametrisiert wurde
self
. Anders gesagt, die Child
Instanz enthält einen Verweis auf die Instanz Parent
, die sie erstellt hat, und kann daher nicht länger als diese
Parent
Instanz 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, 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
oder Arc
.
Mehr Informationen
parent
Warum kann der Compiler nach dem Verschieben in die Struktur keinen neuen Verweis auf die Struktur abrufen parent
und dieser child
in 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 Option
in 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 Pin
der Tabelle hinzugefügt wird, ist eine übliche Methode, um anzugeben, dass sich ein bestimmter Wert garantiert nicht bewegt.
Siehe auch:
Parent
undChild
könnte helfen ...