Warum zwingt Rust einen, Lebensdauern anzugeben? Und wie vereinfacht das das Leben?
\n\nWenn Sie Rust lernen, stoßen Sie früher oder später auf diese mysteriöse Syntax: 'a, 'b... Und es stellt sich die berechtigte Frage: „Warum muss ich explizit Lebensdauern angeben? Andere Sprachen kommen doch auch ohne aus!“
Diese Frage ist eine der beliebtesten auf Stack Overflow (tausende Aufrufe). Und das ist nicht verwunderlich: Das Konzept der Lifetimes in Rust erscheint kompliziert, bis man sein Wesen versteht. Lassen Sie es uns ohne langweilige Theorie anhand von lebendigen Beispielen klären.
\n\nKurze Antwort: Sicherheit ohne Garbage Collector
\n\nRust garantiert Speichersicherheit zur Kompilierzeit. Es muss genau wissen, wie lange Referenzen leben, um hängende Referenzen (dangling references) zu verhindern. In Java oder Go kümmert sich der Garbage Collector darum. In C++ sind Sie selbst für den Speicher verantwortlich (und machen oft Fehler). Rust hat einen dritten Weg gewählt: Der Compiler überprüft alles statisch.
\n\nAber warum kann der Compiler nicht selbst darauf kommen? Kann er – in 80% der Fälle. Das nennt man Lifetime Elision (Regeln zur impliziten Ableitung). In komplexen Situationen benötigt er jedoch Ihre Hilfe.
\n\nRegel Nr. 1: Der Compiler ist kein Hellseher
\n\nSchauen Sie sich diese Funktion an:
\n\nfn longest(x: &str, y: &str) -> &str {\n if x.len() > y.len() { x } else { y }\n}\n\nAuf den ersten Blick ist alles logisch. Aber Rust gibt einen Fehler aus: „missing lifetime specifier“. Warum?
\n\nStellen Sie sich vor, Sie übergeben x – eine Referenz auf einen String, der 5 Sekunden lebt, und y – eine Referenz auf einen String, der 1 Sekunde lebt. Die Funktion kann jeden von ihnen zurückgeben. Wie groß ist die Lebensdauer der zurückgegebenen Referenz? Der Compiler weiß es nicht!
Die Lösung – explizit sagen: „Die zurückgegebene Referenz wird genauso lange leben wie beide Eingaben“:
\n\nfn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n\nDas Symbol 'a ist eine Lebensdauer-Annotation. Wir sagen: „Alle drei Referenzen müssen die gleiche minimale Lebensdauer haben“. Jetzt kann der Compiler überprüfen, dass der aufrufende Code die Daten nicht vorzeitig zerstört.
Regel Nr. 2: Unterschiedliche Referenzen – unterschiedliche Lebensdauern
\n\nEin häufiger Anfängerfehler ist zu denken, dass alle 'a gleich sein müssen. Tatsächlich kann man unterschiedliche Lebensdauern angeben:
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {\n x // Ok, wir geben eine Referenz mit der Lebensdauer 'a zurück\n // y // Fehler! y lebt 'b, aber wir haben 'a versprochen\n}\n\nHier sagen wir explizit: x und y können unterschiedliche Lebensdauern haben. Aber wir können nur die Referenz zurückgeben, deren Lebensdauer mit der im Rückgabetyp versprochenen übereinstimmt.
Regel Nr. 3: Auch Strukturen können Referenzen speichern
\n\nWenn Sie eine Struktur erstellen, die Referenzen enthält, müssen Lebensdauern in Rust zwingend angegeben werden:
\n\nstruct Book<'a> {\n title: &'a str,\n author: &'a str,\n}\n\nfn main() {\n let title = String::from("Rust Book");\n let book;\n {\n let author = String::from("John Doe");\n book = Book { title: &title, author: &author };\n } // author wird hier zerstört!\n println!("{}", book.title); // Fehler! book.author hängt\n}\n\nDer Compiler lässt diesen Code nicht kompilieren, weil book.author auf einen bereits gelöschten String verweist. Explizite Lifetimes in Rust ermöglichen es, solche Fehler zur Kompilierzeit abzufangen, nicht zur Laufzeit.
Regel Nr. 4: Lifetime Elision – wann man sie nicht schreiben muss
\n\nRust ist klug genug, um Lebensdauern in einfachen Fällen selbst abzuleiten. Hier kann man auf die Angabe verzichten:
\n\n- \n
- Eine Eingangsreferenz – die Lebensdauer der Ausgabe ist gleich der