Lebensdauern: wann der Compiler fragt und wann nicht
Die Angabe beschreibt keine Dauer, sondern eine Beziehung zwischen zwei Referenzen. Wer das einmal sortiert hat, schreibt sie in drei klar erkennbaren Fällen und sonst nie.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Die Schreibweise sieht nach Speicherverwaltung aus und meint etwas anderes
Das Hochkomma mit einem Buchstaben dahinter liest sich wie eine Dauer, und entsprechend wird es gelesen: dieser Wert lebt so lange. Das stimmt nicht. Es ist ein Name für einen Bereich des Programms, in dem eine Referenz gültig bleiben muss, und welcher Bereich das konkret ist, entscheidet der Compiler an jeder Aufrufstelle neu. Genau deshalb funktioniert dieselbe Funktion mit einem Wert aus einer Konstanten und mit einem Wert aus einer Schleife.
Aus dem Missverständnis folgt der teure Teil. Wer Angaben verteilt, bis die Meldung verschwindet, bekommt Angaben, die sich ausbreiten. Eine Struktur mit einem Referenzfeld trägt ihren Parameter in jede Funktion, die sie berührt, in jeden Trait, den sie erfüllt, und in jede weitere Struktur, die sie enthält. Zwei Tage später kostet eine Änderung in einem Modul Anfassen in sechs.
Die Alternative ist meistens billiger, als sie aussieht: den Besitz übernehmen. Ein String statt eines Ausschnitts im Feld kostet eine Speicheranforderung und nimmt dafür einen ganzen Strang von Folgeentscheidungen aus dem Entwurf. Referenzen in Feldern lohnen sich, wenn du in einer engen Schleife das Kopieren wirklich vermeiden musst, und das ist deutlich seltener der Fall, als der Reflex nahelegt.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Eine Signatur mit Lebensdauern, Stück für Stück
Die Funktion gibt den längeren von zwei Textausschnitten zurück und schreibt nebenbei einen Vermerk in ein Protokoll. Genau an dieser Mischung lässt sich zeigen, welche Referenz eine Angabe braucht und welche nicht.
Der Aufbau
fn longest<'a>(left: &'a str, right: &'a str, log: &mut Vec<String>) -> &'a str - 01 Der Lebensdauer-Parameter
<'a>Das ist eine Deklaration wie ein generischer Typparameter, keine Zeitangabe. Der Name ist frei wählbar, 'a ist nur Gewohnheit. Die Zeile sagt: Diese Funktion arbeitet für irgendeine Lebensdauer, und welche das ist, entscheidet der Aufrufer.
- 02 Die erste geliehene Eingabe
left: &'a strDie Referenz trägt den Parameter. Gemeint ist nicht, dass der Wert genau so lange lebt, sondern mindestens so lange. Ein Ausschnitt aus einer Konstanten erfüllt das genauso wie einer aus einem String, der zwei Zeilen weiter unten endet.
- 03 Die zweite Eingabe mit derselben Marke
right: &'a strHier steckt die eigentliche Aussage. Weil beide Eingaben dieselbe Marke tragen, setzt der Compiler an der Aufrufstelle die kürzere der beiden tatsächlichen Lebensdauern ein. Deshalb ist das Ergebnis nur so lange gültig wie die kurzlebigere der beiden Eingaben.
- 04 Die Referenz ohne eigene Marke
log: &mut Vec<String>Diese Referenz taucht im Rückgabetyp nicht auf, deshalb braucht sie keine Angabe. Der Compiler vergibt ihr eine eigene, von 'a unabhängige Lebensdauer. Das ist der Normalfall: Annotiert wird nur, was mit dem Ergebnis zusammenhängt.
- 05 Der Rückgabewert
-> &'a strErst diese Marke macht die ganze Angabe nötig. Ohne sie könnte der Compiler nicht wissen, ob die zurückgegebene Referenz in left oder in right zeigt, und damit auch nicht, wie lange sie gültig bleibt. Genau das ist die Frage, die eine Lebensdauer beantwortet.
Wenn es nicht funktioniert
Das siehst du
Eine Funktion gibt eine Referenz zurück, und der Compiler verlangt eine Lebensdauer-Angabe (missing lifetime specifier).
Warum
Es gibt mehr als eine Eingabe-Referenz, und der Compiler kann nicht erraten, aus welcher der Rückgabewert stammt. Bei nur einer Eingabe-Referenz vergibt er die Angabe von selbst.
Was hilft
Wenn das Ergebnis aus beiden Eingaben stammen kann, markiere beide und den Rückgabewert mit demselben Parameter. Stammt es immer nur aus einer, markiere allein diese, dann ist der Rückgabewert nur an sie gebunden und nicht zusätzlich an die kürzere zweite.
Das siehst du
Der Compiler meldet, ein geliehener Wert lebe nicht lange genug, und zeigt auf eine Variable, die in einem inneren Block angelegt wurde.
Warum
Die Referenz soll den Block überleben, in dem der Wert selbst liegt. Am Ende des Blocks wird der Wert freigegeben, die Referenz zeigte danach ins Leere.
Was hilft
Den Wert eine Ebene weiter außen anlegen, damit er länger lebt als die Referenz auf ihn. Wenn das nicht geht, gib statt einer Referenz den Wert selbst zurück, also einen eigenen String statt eines Ausschnitts.
Das siehst du
Eine Struktur bekommt ein Feld mit einer Referenz, und plötzlich verlangt jede Verwendung dieser Struktur einen zusätzlichen Parameter.
Warum
Sobald eine Struktur eine Referenz speichert, wird sie selbst über eine Lebensdauer parametrisiert. Der Parameter zieht sich durch jede Funktion, jede weitere Struktur und jeden Trait, in dem sie vorkommt.
Was hilft
In fast allen Fällen ist es billiger, den Besitz zu übernehmen, also einen eigenen String statt eines Ausschnitts und einen eigenen Vec statt einer geliehenen Folge. Referenzen in Feldern lohnen sich nur bei kurzlebigen Hilfstypen, etwa einem Parser, der über einen Puffer läuft und ihn nicht überlebt.
Das siehst du
Ein Aufruf verlangt die statische Lebensdauer, obwohl die Daten offensichtlich vorhanden sind, etwa beim Starten eines Threads oder einer Aufgabe.
Warum
Die statische Zusage bedeutet nicht, dass der Wert ewig lebt, sondern dass er keine Referenz auf etwas Kurzlebigeres enthält. Ein Thread kann länger laufen als die Funktion, die ihn gestartet hat, deshalb darf er nichts mitnehmen, was vorher endet.
Was hilft
Die Daten in den Thread hineingeben statt sie auszuleihen, also mit move übertragen und bei Bedarf vorher klonen. Für geteilte Daten über Threads hinweg Arc verwenden, das erfüllt die Bedingung, weil der Wert lebt, solange irgendein Arc darauf zeigt. Wenn der Thread garantiert vor den Daten endet, erlauben begrenzte Threads über std::thread::scope auch das Ausleihen.
Das siehst du
Eine Methode gibt eine Referenz zurück, die Signatur wird angenommen, aber im Rumpf heißt es, ein Wert lebe nicht lange genug, oder das Ergebnis lässt sich an der Aufrufstelle kürzer verwenden als erwartet.
Warum
Bei Methoden mit einer Referenz auf sich selbst vergibt der Compiler die Lebensdauer des Rückgabewerts automatisch aus dieser Referenz, auch wenn das Ergebnis in Wahrheit aus einem Parameter stammt. Er fragt deshalb nicht nach, sondern bindet still an das falsche Objekt, und der Fehler taucht eine Ebene später auf.
Was hilft
Den betreffenden Parameter und den Rückgabewert mit demselben Lebensdauer-Parameter markieren, den du an der Methode einführst. Damit überschreibst du die dritte Regel ausdrücklich. Stammt das Ergebnis tatsächlich aus dem Objekt, lässt du die Angabe weg und verlässt dich auf die Regel.
Fünf Fragen, und du weißt, ob eine Angabe fehlt
- 01 Gibt die Funktion überhaupt eine Referenz zurück? Sonst brauchst du keine Angabe.
- 02 Kommt die Referenz nur aus einer Eingabe? Dann ergänzt der Compiler sie selbst.
- 03 Steht eine Referenz auf sich selbst in der Signatur? Dann erbt die Ausgabe deren Dauer.
- 04 Speichert eine Struktur eine Referenz? Dann wird die Struktur selbst parametrisiert.
- 05 Startet ein Thread oder eine abgegebene Aufgabe? Dann verlangt der Compiler in der Regel die statische Zusage.
Was du danach ohne Ausprobieren entscheidest
Es gibt genau drei Regeln, nach denen der Compiler ergänzt, und drei Situationen, in denen er es nicht kann. Alles andere ist Folgeentscheidung: Besitz oder Referenz, Index oder Zeiger, geteilter Besitz oder Durchreichen.
Die drei Elisionsregeln kennen
Jeder Referenzparameter bekommt eine eigene Lebensdauer. Gibt es genau eine Eingabe-Lebensdauer, geht sie an alle Ausgaben. Steht eine Referenz auf sich selbst in der Signatur, geht deren Lebensdauer an alle Ausgaben. Was danach noch offen ist, musst du schreiben, und was nicht offen bleibt, schreibst du besser nicht.
Erkennen, wann eine Angabe wirklich fehlt
Hingeschrieben wird eine Lebensdauer an zwei Stellen: wenn eine zurückgegebene Referenz aus mehr als einer Eingabe stammen könnte, und wenn ein Typ selbst eine Referenz speichert. Der dritte Fall, die statische Zusage bei Threads und Aufgaben, ist keine Annotation, sondern eine Bedingung an den Typ, und sie wird auch anders gelöst. Wenn keine Referenz zurückkommt, ist die Frage gar nicht gestellt.
Besitz statt Referenz im Feld
Für Typen, die länger leben als ein einzelner Funktionsaufruf, ist der Besitz die Voreinstellung. Ein eigener String oder Vec im Feld kostet eine Speicheranforderung und erspart dir den Parameter, der sich sonst durch das halbe Modul zieht.
Die statische Lebensdauer richtig lesen
Sie bedeutet nicht ewig, sondern dass der Wert keine Referenz auf etwas Kurzlebigeres enthält. Ein eigener String erfüllt das, ein Ausschnitt aus einem lokalen Puffer nicht. Ein Thread verlangt sie, weil er länger laufen kann als die Funktion, die ihn gestartet hat.
Index statt Zeiger bei Geflechten
Bei Bäumen und Graphen ist die Antwort nie eine Annotation, sondern ein anderer Aufbau: alle Knoten in einem Vec, Kanten als Zahlen. Das erspart dir sämtliche Lebensdauer-Fragen und macht die Struktur nebenbei serialisierbar.
Geteilten Besitz statt Ausleihen über Grenzen
Wenn mehrere Stellen dieselben Daten über Thread-Grenzen hinweg brauchen, beantwortet Arc die Haltbarkeitsfrage ohne jede Annotation: Der Wert lebt, solange irgendein Arc darauf zeigt.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Was die Marke tatsächlich beschreibt
Eine Lebensdauer ist ein Name für einen Bereich des Programms. Der Compiler setzt an jeder Aufrufstelle den konkreten Bereich ein, der alle Bedingungen erfüllt. Tragen zwei Parameter dieselbe Marke, muss der eingesetzte Bereich zu beiden passen, also nimmt der Compiler den kürzeren. Genau daraus folgt die häufigste Überraschung: Das Ergebnis lässt sich nur dort verwenden, wo auch die kurzlebigere der beiden Eingaben noch gültig ist, selbst wenn es tatsächlich in die langlebigere zeigt.
Der Ausweg ist keine Umschreibung, sondern eine ehrlichere Signatur. Wenn das Ergebnis immer aus der ersten Eingabe stammt, bekommen beide Eingaben eigene Parameter, und nur die erste teilt ihren mit dem Rückgabewert. Der Compiler weiß dann, dass die zweite Eingabe nichts zur Haltbarkeit beiträgt, und die Einschränkung an der Aufrufstelle fällt weg.
Ein Detail, das die Sache entspannt: Lebensdauern existieren nur während des Übersetzens. Sie erzeugen keinen Code und keine Daten, das fertige Programm ist identisch mit einem, das ohne Angaben auskommt. Was etwas kostet, ist die Alternative, für die man sich entscheidet, um sie zu vermeiden, also die Speicheranforderung für einen eigenen String.
Elision: die drei Regeln, nach denen der Compiler ergänzt
Regel eins: Jeder Referenzparameter, bei dem keine Angabe steht, bekommt eine eigene. Regel zwei: Gibt es danach genau eine Eingabe-Lebensdauer, wird sie allen offenen Ausgaben zugewiesen. Regel drei: Ist einer der Parameter eine Referenz auf das Objekt selbst, wird deren Lebensdauer allen offenen Ausgaben zugewiesen, und zwar unabhängig davon, wie viele andere Referenzparameter es gibt. Bleibt danach eine Ausgabe ohne Zuweisung, bekommst du die Meldung über die fehlende Angabe.
Diese drei Regeln erklären die gesamte Alltagserfahrung. Eine Funktion mit einem Textausschnitt hinein und einem hinaus braucht nie eine Angabe, weil Regel zwei greift. Eine Methode, die einen Verweis auf ein eigenes Feld zurückgibt, braucht nie eine, weil Regel drei greift. Und die Funktion mit zwei Eingaben und einer Ausgabe braucht immer eine, weil nach Regel eins zwei Lebensdauern im Spiel sind und keine weitere Regel entscheidet.
Wichtig ist die Grenze: Diese Regeln gelten für Signaturen von Funktionen und Methoden, nicht für Strukturdefinitionen. Ein Referenzfeld verlangt immer einen ausgeschriebenen Parameter, es gibt dort nichts zu ergänzen. Wenn du nur ausdrücken willst, dass eine Lebensdauer vorhanden ist, ohne ihr einen Namen zu geben, gibt es dafür die anonyme Schreibweise mit dem Unterstrich, und sie liest sich besser als ein erfundener Buchstabe.
Referenzen in Strukturen und warum sie ansteckend sind
Sobald ein Feld eine Referenz enthält, ist die Struktur an die geliehenen Daten gebunden und kann sie nicht überleben. Der Parameter erscheint danach in jeder Funktionssignatur, die den Typ berührt, in jeder Trait-Implementierung und in jeder weiteren Struktur, die ihn als Feld enthält. Zusätzlich fällt eine ganze Reihe von Möglichkeiten weg: Ein solcher Typ lässt sich nicht in einen Thread schicken, der länger lebt, und nicht in einem langlebigen Behälter ablegen.
Richtig ist das Muster bei kurzlebigen Hilfstypen. Ein Parser, der über einen Puffer läuft und Ausschnitte daraus zurückgibt, spart damit das Kopieren jedes einzelnen Felds, und er wird ohnehin freigegeben, bevor der Puffer verschwindet. Eine Sicht auf einen Datensatz, die nur für die Dauer einer Auswertung existiert, fällt in dieselbe Kategorie.
Falsch ist es bei Fachtypen, Konfiguration und allem, was gespeichert, verschickt oder über eine Modulgrenze gereicht wird. Wenn du zwischen beiden Welten sitzt, gibt es einen Mittelweg: einen Typ, der entweder ausleiht oder besitzt und sich erst dann für eine eigene Kopie entscheidet, wenn tatsächlich geändert wird. Die Standardbibliothek bringt ihn mit, und er ist an genau den Stellen sinnvoll, an denen die meisten Aufrufe nur lesen.
Die statische Lebensdauer ist keine Ewigkeit
Es gibt zwei Verwendungen, die regelmäßig verwechselt werden. Als Bedingung an einen Typ bedeutet die statische Lebensdauer: Dieser Typ enthält keine Referenzen mit kürzerer Haltbarkeit. Ein eigener String erfüllt das, obwohl er in zwei Zeilen freigegeben wird, denn er leiht sich nichts aus. Als Lebensdauer einer Referenz bedeutet sie dagegen: Diese Daten liegen für die gesamte Laufzeit des Programms bereit, etwa ein Textliteral, das im Programm selbst steht.
In der Praxis begegnet dir die Bedingung beim Starten von Threads und beim Übergeben von Aufgaben an eine Laufzeitumgebung. Der Grund ist immer derselbe: Der Compiler weiß nicht, wann der Thread endet, also darf nichts hineinwandern, das vorher verschwindet. Die Lösung ist fast nie eine Annotation, sondern eine Entscheidung über den Besitz, also Daten mit move übertragen, vorher klonen oder über Arc teilen.
Die Ausnahme sind begrenzte Threads. Sie garantieren, dass alle gestarteten Threads vor dem Ende des umschließenden Bereichs zusammengeführt werden, und deshalb darf innerhalb dieses Bereichs ausgeliehen werden. Das ist der saubere Weg, wenn mehrere Threads gemeinsam über einen vorhandenen Puffer laufen sollen, ohne dass jeder eine eigene Kopie bekommt.
Die Alternativen zur Annotation
Erstens Besitz übernehmen. Das ist die Antwort auf geschätzt neun von zehn Lebensdauer-Fragen im Anwendungscode. Der Preis ist eine Speicheranforderung pro Wert, und der ist an den meisten Stellen nicht messbar. Wer daran zweifelt, sollte messen statt schätzen, denn die Vermutung, dass eine Kopie teuer ist, hält im Profil selten stand.
Zweitens Indizes statt Referenzen. Für alles, was auf sich selbst zeigt, also Bäume, Graphen, verkettete Strukturen und Objekte mit Elternverweis. Die Knoten liegen in einem Vec, die Kanten sind Zahlen. Damit verschwindet die Lebensdauer-Frage vollständig, und die Struktur lässt sich nebenbei speichern und übertragen.
Drittens geteilter Besitz über Rc innerhalb eines Threads und Arc über Threads hinweg. Und für den seltenen Fall, dass eine Struktur wirklich auf ihre eigenen Daten zeigen muss, gibt es fertige Bibliotheken, die diese Konstruktion gekapselt anbieten. Die sind in jedem Fall die bessere Antwort als ein selbst geschriebener unsafe-Block, weil die Zusicherungen dort geprüft und dokumentiert sind.
Dazu passende Kurse
Weil sich diese Entscheidung an eigenem Code schneller klärt als an Beispielen aus dem Buch, gibt es bei cmt Rust-Kurse für Fortgeschrittene .
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Sehr angenehmes, gut vorbereitetes Seminar, kann ich nur empfehlen.
Häufige Fragen
Kostet eine Lebensdauer-Angabe Laufzeit?
Wann nehme ich die anonyme Schreibweise statt eines Namens?
Warum lässt sich mein Ergebnis nur im inneren Block verwenden?
Brauche ich Lebensdauern, wenn ich asynchron schreibe?
Passt thematisch dazu
Lebensdauern sind die dritte Stufe nach Besitz und Ausleihen, deshalb lohnt sich vorher ein Blick auf das Eigentumsmodell in seinen Grundzügen .
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Rust-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Lebensdauern an einer echten Signatur durchgehen
Im Rust-Aufbaukurs bei cmt nimmst du Signaturen aus deinem eigenen Projekt auseinander und entscheidest pro Fall zwischen Annotation und Besitz, statt beides auszuprobieren.