Lifetimes in Rust
Bei Lifetimes (Lebensdauern) stoßen viele Rust-Neulinge zum ersten Mal an eine echte Hürde. Diese Seite geht es deshalb sanft an. Du musst hier nicht jede Regel beherrschen. Es reicht, wenn du erkennst, was eine Lifetime ist und warum der Compiler danach fragt.
Welches Problem Lifetimes lösen
Abschnitt betitelt „Welches Problem Lifetimes lösen“Vom Borrowing weißt du schon: Mit einer Referenz (&) kannst du einen Wert benutzen, ohne dass er dir gehört. Eine Referenz ist aber nur so lange gültig, wie der Wert existiert, auf den sie zeigt. Rust muss also sicherstellen, dass du nie eine Referenz auf etwas behältst, das schon aufgeräumt wurde. So einen Fehler nennt man hängende Referenz (dangling reference). In Sprachen wie C ist das eine klassische Ursache für Abstürze.
Meistens findet Rust das ganz allein heraus, indem es sich die Gültigkeitsbereiche (Scopes) in deinem Code ansieht. Du musst gar nicht darüber nachdenken – genau das hast du auf der Seite Gültigkeitsbereich (Scope) gesehen. Manchmal, vor allem bei Funktionen, kommt der Compiler aber nicht allein weiter. Dann bittet er dich, es ausdrücklich hinzuschreiben. Genau das ist eine Lifetime-Annotation.
Eine Funktion, bei der Rust Hilfe braucht
Abschnitt betitelt „Eine Funktion, bei der Rust Hilfe braucht“Diese Funktion gibt den längeren von zwei String-Slices zurück:
fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}Das lässt sich nicht kompilieren. Die Fehlermeldung sieht so aus:
error[E0106]: missing lifetime specifierDer Compiler sagt dir damit: „Hier fehlt eine Lifetime-Angabe.“ Das Problem ist, dass die zurückgegebene Referenz von a oder von b stammen kann. Rust kann das nicht erkennen, nur indem es die Funktion liest. Also kann es auch nicht garantieren, dass die Referenz noch gültig ist, wenn du sie benutzt.
Eine Lifetime-Annotation hinzufügen
Abschnitt betitelt „Eine Lifetime-Annotation hinzufügen“Du behebst das, indem du einer Lifetime einen Namen gibst (üblich ist ein kleiner Buchstabe wie 'a). Damit sagst du Rust, dass die Lifetime des Rückgabewerts an beide Parameter gebunden ist:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("hi");
let result = longest(&s1, &s2); println!("Der längere: {result}");}Ausgabe:
Der längere: helloDie beiden Wörter "hello" und "hi" bleiben hier auf Englisch, weil die Funktion ihre Länge in Bytes vergleicht. Umlaute würden mehr Bytes belegen und das Ergebnis verändern.
Lies 'a als Etikett, nicht als Wert. Es legt keine echte Zeitspanne fest. Es sagt Rust nur: „Diese Referenzen müssen alle mindestens über denselben Abschnitt des Codes gültig bleiben.“ Der Compiler prüft dann bei jedem Aufruf, ob das wirklich stimmt.
Warum das wichtig ist
Abschnitt betitelt „Warum das wichtig ist“Ohne diese Annotation kann der Compiler wirklich nicht wissen, ob die Referenz, die longest zurückgibt, länger leben könnte als einer der Strings, aus denen sie stammt. Erst mit der Lifetime-Annotation kann er beweisen, dass die zurückgegebene Referenz immer gültig ist. Das passiert beim Kompilieren und kostet zur Laufzeit nichts.
Structs, die Referenzen enthalten
Abschnitt betitelt „Structs, die Referenzen enthalten“Als Nächstes begegnen dir Lifetimes in einem Struct. Bisher gehörten jedem Struct-Feld seine Daten selbst, zum Beispiel ein String. Ein Feld kann aber auch eine Referenz sein. Dann ist das Struct nur so lange gültig wie die Daten, auf die es zeigt. Rust lässt dich das mit einer Lifetime hinschreiben:
struct Highlight<'a> { text: &'a str, // leiht sich etwas aus einem String, der woanders lebt}
fn main() { let book = String::from("Call me Ishmael. Some years ago..."); let first = Highlight { text: &book[..15] };
println!("Markierung: {}", first.text); println!("Das ganze Buch ist {} Bytes lang", book.len());}Ausgabe:
Markierung: Call me IshmaelDas ganze Buch ist 34 Bytes langDer Buchtext (der Anfang von „Moby Dick“) bleibt auf Englisch, weil das Programm ihn in Bytes zerschneidet und zählt. Umlaute würden mehr Bytes belegen und die Ergebnisse verändern.
Lies Highlight<'a> als „ein Highlight, das sich etwas für die Lifetime 'a ausleiht“. Der Teil &book[..15] ist ein Slice: Er leiht sich die ersten 15 Bytes von book aus, ohne sie zu kopieren. Weil first nur ausleiht, kannst du book danach weiter benutzen.
Dank der Lifetime erkennt Rust diesen Fehler: Der String wird aufgeräumt, während das Struct noch hineinzeigt.
struct Highlight<'a> { text: &'a str,}
fn main() { let highlight; { let book = String::from("Call me Ishmael. Some years ago..."); highlight = Highlight { text: &book[..15] }; } // hier wird book aufgeräumt ...
println!("{}", highlight.text); // ... aber highlight zeigt noch hinein}error[E0597]: `book` does not live long enough --> src/main.rs:9:40 | 8 | let book = String::from("Call me Ishmael. Some years ago..."); | ---- binding `book` declared here 9 | highlight = Highlight { text: &book[..15] }; | ^^^^ borrowed value does not live long enough10 | } // hier wird book aufgeräumt ... | - `book` dropped here while still borrowed11 |12 | println!("{}", highlight.text); // ... aber highlight zeigt noch hinein | -------------- borrow later used hereDie Meldung bedeutet: „book lebt nicht lange genug.“ Der Compiler zeigt dir, wo book aufgeräumt wird, obwohl es noch ausgeliehen ist, und wo der Borrow später noch benutzt wird.
Ein praktischer Tipp: Wenn dir Lifetimes in einem Struct im Weg stehen, speichere einen eigenen String statt eines &str. Dabei wird der Text kopiert, aber das Struct hängt von nichts anderem mehr ab, und die Lifetime fällt weg. Viele erfahrene Rust-Entwickler fangen so an und steigen erst auf Referenzen um, wenn sie einen guten Grund dafür haben.
Die gute Nachricht
Abschnitt betitelt „Die gute Nachricht“Du wirst 'a viel seltener schreiben, als diese Seite vermuten lässt. Viel Rust-Code kommt ganz ohne ausdrückliche Lifetimes aus, weil der Compiler sie in den meisten Alltagssituationen selbst ableiten kann. Meist begegnen sie dir, wenn eine Funktion eine Referenz zurückgibt und der Compiler nicht allein herausfindet, woher sie stammt. Dann denk daran: Das ist kein Fehler in deinem Code. Der Compiler bittet dich nur, etwas zu bestätigen, das er selbst nicht beweisen kann.
Übungen
Abschnitt betitelt „Übungen“1. Der kürzere String
Abschnitt betitelt „1. Der kürzere String“Schreib eine Funktion shortest, die zwei String-Slices annimmt und den kürzeren zurückgibt. Sind beide gleich lang, gib den ersten zurück. Ruf sie mit "elephant" und "cat" auf.
Tipp: Sie hat dieselbe Form wie longest oben. Du brauchst <'a> nach dem Namen und 'a bei beiden Parametern und beim Rückgabetyp.
Lösung anzeigen
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() <= b.len() { a } else { b }}
fn main() { let big = String::from("elephant"); let small = String::from("cat");
println!("Der kürzere: {}", shortest(&big, &small)); println!("Bei Gleichstand gewinnt der erste: {}", shortest("dog", "cow"));}Ausgabe:
Der kürzere: catBei Gleichstand gewinnt der erste: dogDie Tiernamen bleiben auf Englisch, weil die Funktion Längen in Bytes vergleicht.
Der zurückgegebene Slice kann von a oder von b stammen. Deshalb braucht Rust 'a, um zu wissen: Das Ergebnis ist nur gültig, solange beide Eingaben gültig sind. Mit <= statt < gewinnt bei Gleichstand a.
2. Ein Struct, das ausleiht
Abschnitt betitelt „2. Ein Struct, das ausleiht“Leg ein Struct Quote mit zwei &str-Feldern an, text und author. Erstelle eine Instanz, bei der sich text die ersten 20 Bytes des String "Fearless concurrency is a goal of Rust" ausleiht und author das Literal "The Rust Book" ist. Gib sie als Fearless concurrency (The Rust Book) aus.
Tipp: Wie Highlight oben braucht das Struct eine Lifetime: struct Quote<'a>, mit &'a str für beide Felder. &speech[..20] ist ein Slice der ersten 20 Bytes.
Lösung anzeigen
struct Quote<'a> { text: &'a str, author: &'a str,}
fn main() { let speech = String::from("Fearless concurrency is a goal of Rust"); let quote = Quote { text: &speech[..20], author: "The Rust Book", };
println!("{} ({})", quote.text, quote.author);}Ausgabe:
Fearless concurrency (The Rust Book)Das Zitat bleibt auf Englisch, weil es per Slice in Bytes zerschnitten wird.
quote leiht sich etwas von speech aus und kann deshalb nicht länger leben als speech. Das Literal "The Rust Book" ist fest ins Programm eingebaut und lebt während der ganzen Laufzeit. Es hält also locker lange genug für 'a. Probier aus, speech in einem inneren { }-Block zu erstellen und das Zitat nach dem Ende des Blocks auszugeben, wie im Fehlerbeispiel oben. Dann siehst du, wie der Compiler dich aufhält.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Eine Lifetime sagt dem Compiler, wie Referenzen zueinander stehen. So kann er hängende Referenzen ausschließen.
- Du musst sie nur hinschreiben, wenn der Compiler sie nicht selbst ableiten kann – meistens bei Funktionen, die eine Referenz zurückgeben.
'alegt keine Zeitspanne fest. Es verknüpft die Lifetimes verschiedener Referenzen, damit der Compiler sie prüfen kann.- Auch ein Struct, das eine Referenz enthält, braucht eine Lifetime (
struct Highlight<'a>) und kann nicht länger leben als die Daten, die es ausleiht. Mit einem eigenenStringumgehst du das. - Das Ganze ist eine reine Prüfung beim Kompilieren und kostet zur Laufzeit nichts.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Welches Problem lösen Lifetimes?
2. Warum ließ sich fn longest(a: &str, b: &str) -> &str ohne Lifetime-Annotation nicht kompilieren?
3. Kostet eine Lifetime-Annotation zur Laufzeit etwas?
4. Wann musst du laut dieser Seite am häufigsten eine ausdrückliche Lifetime schreiben?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Referenzen validieren mit Lebensdauern – Das Rust-Buch: die vollständige offizielle Erklärung, auch zu Lifetimes in Structs, den Elisionsregeln und
'static. - Functions – Rust by Example (auf Englisch): kurze Beispiele für Funktionen mit Lifetime-Annotationen und welche davon der Compiler ablehnt.
- Structs – Rust by Example (auf Englisch): weitere Structs und Enums, die Referenzen enthalten.
- Lifetime elision – The Rust Reference (auf Englisch): die genauen Regeln, nach denen der Compiler Lifetimes für dich ergänzt. So weißt du, wann du sie weglassen kannst.