Referenzen mit Haltbarkeit

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
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

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
Der Aufbau im Detail

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
  1. 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.

  2. 02 Die erste geliehene Eingabe left: &'a str

    Die 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.

  3. 03 Die zweite Eingabe mit derselben Marke right: &'a str

    Hier 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.

  4. 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.

  5. 05 Der Rückgabewert -> &'a str

    Erst 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

  1. 01 Gibt die Funktion überhaupt eine Referenz zurück? Sonst brauchst du keine Angabe.
  2. 02 Kommt die Referenz nur aus einer Eingabe? Dann ergänzt der Compiler sie selbst.
  3. 03 Steht eine Referenz auf sich selbst in der Signatur? Dann erbt die Ausgabe deren Dauer.
  4. 04 Speichert eine Struktur eine Referenz? Dann wird die Struktur selbst parametrisiert.
  5. 05 Startet ein Thread oder eine abgegebene Aufgabe? Dann verlangt der Compiler in der Regel die statische Zusage.
Was du mitnimmst

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 .

Wissen prüfen

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.

Konkrete Kurse

Wo du genau das übst

Sehr angenehmes, gut vorbereitetes Seminar, kann ich nur empfehlen.
Rust Fortgeschrittenenkurs

Häufige Fragen

Kostet eine Lebensdauer-Angabe Laufzeit?
Nein. Lebensdauern existieren nur während des Übersetzens, sie erzeugen weder Code noch Daten. Das fertige Programm ist identisch mit einem, in dem der Compiler die Angaben selbst ergänzt hat. Was etwas kostet, ist die Entscheidung, die du triffst, um sie zu umgehen, also die Speicheranforderung für einen eigenen Wert statt einer Referenz.
Wann nehme ich die anonyme Schreibweise statt eines Namens?
Wenn es genau eine Lebensdauer gibt und du dem Compiler nur mitteilen willst, dass eine vorhanden ist, etwa in einem Implementierungsblock oder in einem Rückgabetyp. Sie liest sich besser als ein erfundener Buchstabe und macht sichtbar, dass hier nichts zu entscheiden war.
Warum lässt sich mein Ergebnis nur im inneren Block verwenden?
Weil beide Eingaben denselben Parameter tragen und der Compiler die kürzere der beiden tatsächlichen Lebensdauern eingesetzt hat. Wenn das Ergebnis in Wahrheit immer nur aus einer der Eingaben stammen kann, gib jeder Eingabe einen eigenen Parameter und binde die Ausgabe an den der richtigen. Danach verschwindet die Einschränkung.
Brauche ich Lebensdauern, wenn ich asynchron schreibe?
Weniger als erwartet, weil abgegebene Aufgaben ohnehin eine statische Zusage verlangen und du deshalb mit eigenen Daten oder mit Arc arbeitest. Wo sie doch auftauchen, ist bei Traits und bei Funktionen, die eine Zukunft zurückgeben, die aus ihren Argumenten ausleiht. Die Meldungen sind dort deutlich länger, und sie enthalten meistens noch Bedingungen an Traits obendrauf.

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 .

Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Rust-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

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.