Warum kann ich keinen Wert und eine Referenz auf diesen Wert in derselben Struktur speichern?

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

struct Thing { count: u32, } struct Combined(Thing, &'a u32); fn make_combined() -> Combined { let thing = Thing { count: 42 }; Combined(thing, &thing.count) } 

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

 struct Combined(Thing, &'a Thing); fn make_combined() -> Combined { let thing = Thing::new(); Combined(thing, &thing) } 

Manchmal nehme ich nicht einmal einen Verweis auf den Wert und bekomme denselben Fehler:

 struct Combined(Parent, Child); fn make_combined() -> Combined { let parent = Parent::new(); let child = parent.child(); Combined(parent, child) } 

In jedem dieser Fälle bekomme ich einen Fehler, dass einer der Werte “nicht lange genug lebt”. Was bedeutet dieser Fehler?

Sehen 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 p = Parent { count: 42 }; let c = Child { parent: &p }; Combined { parent: p, child: c } } } fn main() {} 

Dies wird mit dem leicht bereinigten Fehler fehlschlagen:

 error: `p` does not live long enough --> src/main.rs:17:34 | 17 | let c = Child { parent: &p }; | ^ | note: reference must be valid for the lifetime 'a as defined on the block at 15:21... --> src/main.rs:15:22 | 15 | fn new() -> Self { | ^ note: ...but borrowed value is only valid for the block suffix following statement 0 at 16:37 --> src/main.rs:16:38 | 16 | let p = Parent { count: 42 }; | ^ 

Um diesen Fehler vollständig zu verstehen, müssen Sie darüber nachdenken, wie die Werte im Speicher dargestellt werden und was passiert, wenn Sie diese Werte verschieben. Lassen Sie uns Combined::new mit einigen hypothetischen Speicheradressen annotieren, die zeigen, wo sich Werte befinden:

 let p = Parent { count: 42 }; // `p` lives at address 0x1000 and takes up 4 bytes // The value of `p` is 42 let c = Child { parent: &p }; // `c` lives at address 0x1010 and takes up 4 bytes // The value of `c` is 0x1000 Combined { parent: p, child: c } // The return value lives at address 0x2000 and takes up 8 bytes // `p` is moved to 0x2000 // `c` is ... ? 

Was soll mit c passieren? Wenn der Wert einfach wie p was verschoben wurde, würde er sich auf den Speicher beziehen, der nicht mehr garantiert einen gültigen Wert enthält. Jeder andere Teil des Codes darf Werte an 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 Sicherheitserrorsn führen und ist eine der Hauptkategorien von Fehlern, die von Rust verhindert werden.

Dies ist genau das Problem, das die Lebenszeit verhindert. Eine Lebenszeit ist ein bisschen Metadaten, die es Ihnen und dem Compiler ermöglichen zu wissen, wie lange ein Wert an seinem aktuellen Speicherort gültig sein wird . Das ist ein wichtiger Unterschied, denn es ist ein häufiger Fehler, den Rost-Neulinge machen. Rust-Lebensdauern sind nicht der Zeitraum zwischen der Erstellung eines Objekts und der Zerstörung!

Betrachten wir es als Analogie so: Während des Lebens einer Person wohnen sie an vielen verschiedenen Orten, jede mit einer bestimmten Adresse. Eine Rust-Lebenszeit bezieht sich auf die Adresse, in der Sie sich gerade aufhalten , und nicht darauf, wann Sie in Zukunft sterben werden (obwohl das Sterben auch Ihre Adresse ändert). Jedes Mal, wenn Sie umziehen, ist dies relevant, weil Ihre Adresse nicht mehr gültig ist.

Es ist auch wichtig zu beachten, dass Lebenszeiten Ihren Code nicht ändern; Ihr Code steuert die Lebensdauern, Ihre Lebensdauern steuern den Code nicht. Das markige Sprichwort lautet: “Lebenszeiten sind beschreibend, nicht präskriptiv”.

Lassen Sie uns Combined::new mit einigen Zeilennummern annotieren, die wir verwenden werden, um Lebenszeiten hervorzuheben:

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

Die konkrete Lebensdauer von p beträgt 1 bis einschließlich 4 (was ich als [1,4] ). Die konkrete Lebensdauer von c beträgt [2,4] , und die konkrete Lebensdauer des Rückgabewerts beträgt [4,5] . Es ist möglich, konkrete Lebensdauern zu haben, die bei Null beginnen – das wäre die Lebensdauer eines Parameters für eine function oder etwas, das außerhalb des Blocks existiert.

Beachten Sie, dass die Lebensdauer von c selbst [2,4] , sich aber auf einen Wert mit einer Lebensdauer von [1,4] bezieht . Dies ist in Ordnung, solange der Referenzwert ungültig wird, bevor der Referenzwert dies tut. Das Problem tritt auf, wenn wir versuchen, c aus dem Block zurückzugeben. Dies würde die Lebensdauer über ihre natürliche Länge hinaus “überdehnen”.

Dieses neue Wissen sollte die ersten beiden Beispiele erklären. Die dritte erfordert die Implementierung von Parent::child . Die Chancen stehen gut, es sieht ungefähr so ​​aus:

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

Dies verwendet lebenslange Elision , um das Schreiben expliziter generischer Lebensdauerparameter zu vermeiden. Es entspricht:

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

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

Dies lässt uns auch erkennen, dass mit unserer Erstellungsfunktion etwas nicht stimmt:

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

Obwohl Sie eher in einer anderen Form geschrieben 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, für die Combined parametrisiert wird, nicht durch irgendetwas eingeschränkt wird – es kann sein, was auch immer der Anrufer wünscht. Dies ist unsinnig, da der Aufrufer die 'static Lebensdauer angeben kann und es keine Möglichkeit gibt, diese Bedingung zu erfüllen.

Wie repariere ich es?

Die einfachste und am besten empfohlene Lösung besteht darin, nicht zu versuchen, diese Elemente in dieselbe Struktur zu bringen. Dadurch wird die Strukturverschachtelung die Lebensdauer Ihres Codes nachahmen. Platzieren Sie Typen, die Daten besitzen, zusammen in einer Struktur und stellen Sie Methoden bereit, mit denen Sie Referenzen oder Objekte mit Referenzen abrufen können.

Es gibt einen speziellen Fall, in dem die Lebensdauerüberwachung übereifrig ist: wenn Sie etwas auf den Haufen gelegt haben. Dies tritt auf, wenn Sie beispielsweise eine Box . In diesem Fall enthält die Struktur, die verschoben wird, einen pointers in den Heap. Der pointers mit der Spitze wird stabil bleiben, aber die Adresse des pointerss wird sich bewegen. In der Praxis spielt dies keine Rolle, da Sie immer dem pointers folgen.

Die Mietkiste oder die owning_ref-Kiste sind Möglichkeiten, diesen Fall darzustellen, aber sie erfordern, dass sich die Basisadresse nie bewegt . Dies schließt mutierende Vektoren aus, die eine Neuzuordnung und Verschiebung der dem Heap zugeordneten Werte verursachen können.

Mehr Informationen

Warum ist der Compiler nach dem Verschieben von p in die Struktur nicht in der Lage, einen neuen Verweis auf p und ihn c in der Struktur zuzuordnen?

Obwohl dies theoretisch möglich ist, würde dies eine große Menge an Komplexität und Overhead verursachen. Jedes Mal, wenn das Objekt verschoben wird, müsste der Compiler Code einfügen, um die Referenz zu “reparieren”. Dies würde bedeuten, dass das Kopieren einer Struktur nicht länger eine sehr billige Operation ist, die nur einige Bits umher bewegt. Es könnte sogar bedeuten, dass Code wie dieser teuer ist, abhängig davon, wie gut ein hypothetischer Optimierer wäre:

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

Statt dies für jede Bewegung zu erzwingen, kann der Programmierer entscheiden, wann dies geschieht, indem er Methoden erstellt, die nur dann die entsprechenden Referenzen verwenden, wenn Sie sie aufrufen.


In einem bestimmten Fall können Sie einen Typ mit einem Verweis auf sich selbst erstellen. Sie müssen etwas wie Option , um es in zwei Schritten zu machen:

 #[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 erzeugte Wert ist stark eingeschränkt – er kann niemals verschoben werden. Bemerkenswerterweise bedeutet dies, dass es nicht von einer function zurückgegeben oder an irgendeinen Wert weitergegeben werden kann. Eine Konstruktorfunktion zeigt das gleiche Problem mit den Lebensdauern wie oben:

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

Ein etwas anderes Problem, das sehr ähnliche Compiler-Nachrichten verursacht, ist die Lebensdauerabhängigkeit des Objekts, anstatt eine explizite Referenz zu speichern. Ein Beispiel dafür ist die ssh2- Bibliothek. Wenn man etwas größer als ein Testprojekt entwickelt, ist es verlockend zu versuchen, die Session und den Channel die von dieser Sitzung erhalten wurden, nebeneinander in eine Struktur zu stellen und die Implementierungsdetails vor dem Benutzer zu verbergen. Beachten Sie jedoch, dass die 'sess in der Annotation des Typs 'sess Lebenszeit 'sess , während dies bei der Session nicht der Fall ist.

Dies verursacht ähnliche Compilererrors in Bezug auf die Lebensdauern.

Eine Möglichkeit, dies auf sehr einfache Weise zu lösen, besteht darin, die Session im Aufrufer extern zu deklarieren und dann die Referenz innerhalb der Struktur mit einer Lebenszeit zu kommentieren, ähnlich der Antwort in diesem Rust User’s Forum , in der das gleiche Problem beim Einkapseln angesprochen wird SFTP. Dies wird nicht elegant aussehen und nicht immer zutreffen – denn jetzt haben Sie zwei Entitäten, mit denen Sie umgehen müssen, statt einer, den Sie wollten!

Stellt sich heraus, dass die Mietkiste oder die owning_ref-Kiste aus der anderen Antwort die Lösungen für dieses Problem sind. Betrachten wir die owning_ref, die das spezielle Objekt für genau diesen Zweck hat: OwningHandle . Um zu vermeiden, dass sich das zugrundeliegende Objekt bewegt, weisen wir es auf dem Heap mithilfe einer Box , was uns die folgende mögliche Lösung bietet:

 use ssh2::{Channel, Error, Session}; use std::net::TcpStream; use owning_ref::OwningHandle; struct DeviceSSHConnection { tcp: TcpStream, channel: OwningHandle, Box>>, } 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 die Session nicht mehr verwenden können, aber sie wird zusammen mit dem Channel gespeichert, den wir verwenden werden. Da das OwningHandle Objekt zu Box OwningHandle wird, die zu Channel OwningHandle , wenn wir es in einer Struktur speichern, nennen wir es als solches. HINWEIS: Dies ist nur mein Verständnis. Ich habe den Verdacht, dass dies nicht korrekt ist, da es der Diskussion um die OwningHandle von OwningHandle ziemlich nahe kommt .

Ein kurioses Detail hier ist, dass die Session logisch eine ähnliche Beziehung zu TcpStream wie Channel zu Session , aber ihr Besitz wird nicht übernommen und es gibt keine Typ Anmerkungen dazu. Stattdessen liegt es beim Benutzer, sich darum zu kümmern, wie die Dokumentation der Handshake- Methode sagt:

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

Es wird auch dringend empfohlen, dass der bereitgestellte Stream während der gesamten Sitzung nicht gleichzeitig verwendet wird, da dies das Protokoll stören könnte.

Bei der Verwendung von TcpStream liegt es also ganz bei dem Programmierer, die Korrektheit des Codes sicherzustellen. Mit dem OwningHandle wird die Aufmerksamkeit auf die OwningHandle , an der die “gefährliche Magie” stattfindet, mit dem unsafe {} Block gezeichnet.

Eine weitere und hochrangigere Diskussion dieses Themas findet sich in diesem Rust User Forum-Thread – der ein anderes Beispiel und seine Lösung mit der Mietkiste enthält, die keine unsicheren Blöcke enthält.