Referenzen mit Lebenszeiten validieren

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Validating References With Lifetimes. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir die Lebenszeiten besprechen und wie sie gewährleisten, dass Referenzen so lange gültig sind, wie nötig. Obwohl Lebenszeiten möglicherweise unvertraut vorkommen, werden wir die gängigen Arten behandeln, wie du die Lebenszeitssyntax antreffen könntest, um dich mit dem Konzept vertraut zu machen.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} rust/integer_types -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} rust/string_type -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} rust/function_syntax -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} rust/expressions_statements -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} rust/method_syntax -.-> lab-100414{{"Referenzen mit Lebenszeiten validieren"}} end

Validating References with Lifetimes

Lebenszeiten sind eine weitere Art von Generics, die wir bereits verwendet haben. Anstatt sicherzustellen, dass ein Typ das Verhalten hat, das wir wollen, gewährleisten Lebenszeiten, dass Referenzen so lange gültig sind, wie wir sie benötigen.

Ein Detail, das wir in "References and Borrowing" nicht besprochen haben, ist, dass jede Referenz in Rust eine Lebenszeit hat, die der Bereich ist, für den diese Referenz gültig ist. Die meiste Zeit sind Lebenszeiten implizit und werden inferiert, genauso wie die meiste Zeit Typen inferiert werden. Wir müssen nur dann Typen annotieren, wenn mehrere Typen möglich sind. Auf ähnliche Weise müssen wir Lebenszeiten annotieren, wenn die Lebenszeiten von Referenzen auf einige verschiedene Weise zusammenhängen können. Rust erfordert, dass wir die Beziehungen mit generischen Lebenszeitparametern annotieren, um sicherzustellen, dass die tatsächlich verwendeten Referenzen zur Laufzeit definitiv gültig sein werden.

Das Annotieren von Lebenszeiten ist sogar kein Begriff, den die meisten anderen Programmiersprachen haben, daher wird dies unvertraut vorkommen. Obwohl wir in diesem Kapitel die Lebenszeiten nicht vollständig behandeln werden, werden wir die gängigen Arten besprechen, wie du die Lebenszeitssyntax antreffen könntest, damit du mit dem Konzept vertraut wirst.

Preventing Dangling References with Lifetimes

Das Hauptziel von Lebenszeiten ist es, dangling references (verhängende Referenzen) zu vermeiden, die dazu führen, dass ein Programm auf Daten verweist, die nicht die Daten sind, auf die es verweisen soll. Betrachten Sie das Programm in Listing 10-16, das einen äußeren und einen inneren Bereich hat.

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

  5 println!("r: {r}");
}

Listing 10-16: Ein Versuch, eine Referenz zu verwenden, deren Wert außer Gültigkeitsbereich ist

Hinweis: Die Beispiele in Listing 10-16, 10-17 und 10-23 deklarieren Variablen ohne ihnen einen Anfangswert zuzuweisen, sodass der Variablennamen im äußeren Bereich existiert. Auf den ersten Blick mag dies mit Rusts fehlender Nullwerte in Konflikt stehen. Wenn wir jedoch versuchen, eine Variable zu verwenden, bevor wir ihr einen Wert zuweisen, erhalten wir einen Kompilierfehler, was zeigt, dass Rust tatsächlich keine Nullwerte zulässt.

Der äußere Bereich deklariert eine Variable namens r ohne Anfangswert [1], und der innere Bereich deklariert eine Variable namens x mit dem Anfangswert 5 [2]. Innerhalb des inneren Bereichs versuchen wir, den Wert von r als Referenz auf x zu setzen [3]. Dann endet der innere Bereich [4], und wir versuchen, den Wert in r auszugeben [5]. Dieser Code wird nicht kompilieren, weil der Wert, auf den r verweist, außer Gültigkeitsbereich ist, bevor wir versuchen, ihn zu verwenden. Hier ist die Fehlermeldung:

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

Die Fehlermeldung sagt, dass die Variable x "nicht lange genug lebt". Der Grund ist, dass x außer Gültigkeitsbereich sein wird, wenn der innere Bereich am Ende der Zeile 7 endet. Aber r ist immer noch gültig für den äußeren Bereich; da sein Bereich größer ist, sagen wir, dass es "länger lebt". Wenn Rust diesen Code funktionieren ließe, würde r auf einen Speicher verweisen, der freigegeben wurde, als x außer Gültigkeitsbereich ging, und alles, was wir mit r versuchen würden, würde nicht richtig funktionieren. Wie bestimmt Rust also, dass dieser Code ungültig ist? Es verwendet einen Borrow-Checker.

The Borrow Checker

Der Rust-Compiler hat einen Borrow-Checker, der die Bereiche vergleicht, um zu bestimmen, ob alle Entleihen gültig sind. Listing 10-17 zeigt denselben Code wie Listing 10-16, aber mit Anmerkungen, die die Lebenszeiten der Variablen anzeigen.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

Listing 10-17: Anmerkungen zu den Lebenszeiten von r und x, benannt 'a und 'b respective

Hier haben wir die Lebenszeit von r mit 'a und die Lebenszeit von x mit 'b annotiert. Wie Sie sehen können, ist der innere 'b-Block viel kleiner als der äußere 'a-Lebenszeit-Block. Zur Compile-Zeit vergleicht Rust die Größe der beiden Lebenszeiten und sieht, dass r eine Lebenszeit von 'a hat, aber dass es auf Speicher mit einer Lebenszeit von 'b verweist. Das Programm wird abgelehnt, weil 'b kürzer als 'a ist: Das Objekt der Referenz lebt nicht so lange wie die Referenz.

Listing 10-18 behebt den Code, sodass er keinen verhängenden Verweis hat und ohne Fehler kompiliert.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Listing 10-18: Eine gültige Referenz, weil die Daten eine längere Lebenszeit als die Referenz haben

Hier hat x die Lebenszeit 'b, die in diesem Fall größer als 'a ist. Dies bedeutet, dass r x referenzieren kann, weil Rust weiß, dass die Referenz in r immer gültig sein wird, solange x gültig ist.

Jetzt, da Sie wissen, wo die Lebenszeiten von Referenzen sind und wie Rust Lebenszeiten analysiert, um sicherzustellen, dass Referenzen immer gültig sind, werden wir die generischen Lebenszeiten von Parametern und Rückgabewerten im Kontext von Funktionen untersuchen.

Generic Lifetimes in Functions

Wir werden eine Funktion schreiben, die die längere von zwei String-Slices zurückgibt. Diese Funktion wird zwei String-Slices entgegennehmen und einen einzelnen String-Slice zurückgeben. Nachdem wir die longest-Funktion implementiert haben, sollte der Code in Listing 10-19 The longest string is abcd ausgeben.

Dateiname: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

Listing 10-19: Eine main-Funktion, die die longest-Funktion aufruft, um die längere von zwei String-Slices zu finden

Beachten Sie, dass wir möchten, dass die Funktion String-Slices entgegennimmt, die Referenzen sind, statt Strings, weil wir nicht möchten, dass die longest-Funktion die Eigentumsgewalt über ihre Parameter übernimmt. Lesen Sie "String Slices as Parameters" für eine detailliertere Diskussion darüber, warum die Parameter, die wir in Listing 10-19 verwenden, die richtigen sind.

Wenn wir versuchen, die longest-Funktion wie in Listing 10-20 zu implementieren, wird sie nicht kompilieren.

Dateiname: src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-20: Eine Implementierung der longest-Funktion, die die längere von zwei String-Slices zurückgibt, aber noch nicht kompiliert

Stattdessen erhalten wir den folgenden Fehler, der sich auf Lebenszeiten bezieht:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

Der Hilfetext zeigt, dass der Rückgabetyp einen generischen Lebenszeitparameter benötigt, weil Rust nicht wissen kann, auf welche der beiden Referenzen (x oder y) sich der zurückgegebene Wert bezieht. Tatsächlich wissen wir es auch nicht, weil der if-Block im Körper dieser Funktion eine Referenz auf x zurückgibt und der else-Block eine Referenz auf y!

Wenn wir diese Funktion definieren, wissen wir nicht, welche konkreten Werte in diese Funktion übergeben werden, also wissen wir nicht, ob der if-Fall oder der else-Fall ausgeführt wird. Wir wissen auch nicht die konkreten Lebenszeiten der Referenzen, die übergeben werden, also können wir nicht wie in Listings 10-17 und 10-18 die Bereiche betrachten, um zu bestimmen, ob die Referenz, die wir zurückgeben, immer gültig ist. Der Borrow-Checker kann dies auch nicht bestimmen, weil er nicht weiß, wie die Lebenszeiten von x und y zur Lebenszeit des Rückgabewerts zusammenhängen. Um diesen Fehler zu beheben, werden wir generische Lebenszeitparameter hinzufügen, die die Beziehung zwischen den Referenzen definieren, damit der Borrow-Checker seine Analyse durchführen kann.

Lifetime Annotation Syntax

Lebenszeitannotationen ändern nicht, wie lange die einzelnen Referenzen existieren. Stattdessen beschreiben sie die Beziehungen zwischen den Lebenszeiten mehrerer Referenzen zueinander, ohne die Lebenszeiten zu beeinflussen. Genauso wie Funktionen beliebige Typen akzeptieren können, wenn die Signatur einen generischen Typparameter angibt, können Funktionen auch Referenzen mit jeder Lebenszeit akzeptieren, indem sie einen generischen Lebenszeitparameter angeben.

Lebenszeitannotationen haben eine etwas ungewöhnliche Syntax: Die Namen von Lebenszeitparametern müssen mit einem Apostroph (') beginnen und sind normalerweise alle in Kleinbuchstaben und sehr kurz, ähnlich wie generische Typen. Die meisten Leute verwenden den Namen 'a für die erste Lebenszeitannotation. Wir platzieren die Lebenszeitparameterannotationen nach dem & einer Referenz, wobei ein Leerzeichen zwischen der Annotation und dem Typ der Referenz platziert wird.

Hier sind einige Beispiele: Eine Referenz auf einen i32 ohne Lebenszeitparameter, eine Referenz auf einen i32, der einen Lebenszeitparameter namens 'a hat, und eine mutable Referenz auf einen i32, die ebenfalls die Lebenszeit 'a hat.

&i32        // eine Referenz
&'a i32     // eine Referenz mit expliziter Lebenszeit
&'a mut i32 // eine mutable Referenz mit expliziter Lebenszeit

Eine Lebenszeitannotation alleine hat nicht viel Bedeutung, da die Annotations dazu dienen sollen, Rust zu sagen, wie die generischen Lebenszeitparameter mehrerer Referenzen zueinander zusammenhängen. Betrachten wir, wie die Lebenszeitannotationen in Bezug aufeinander im Kontext der longest-Funktion stehen.

Lifetime Annotations in Function Signatures

Um Lebenszeitannotationen in Funktionssignaturen zu verwenden, müssen wir die generischen Lebenszeit-Parameter innerhalb von spitzen Klammern zwischen der Funktionsnamen und der Parameterliste deklarieren, genauso wie wir es mit generischen Typ-Parametern getan haben.

Wir möchten, dass die Signatur die folgende Einschränkung ausdrückt: Die zurückgegebene Referenz wird so lange gültig sein, wie beide Parameter gültig sind. Dies ist die Beziehung zwischen den Lebenszeiten der Parameter und dem Rückgabewert. Wir werden die Lebenszeit 'a nennen und dann sie zu jeder Referenz hinzufügen, wie in Listing 10-21 gezeigt.

Dateiname: src/main.rs

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-21: Die longest-Funktionsdefinition, die angibt, dass alle Referenzen in der Signatur die gleiche Lebenszeit 'a haben

Dieser Code sollte kompilieren und das Ergebnis liefern, das wir erwarten, wenn wir ihn mit der main-Funktion in Listing 10-19 verwenden.

Die Funktionssignatur sagt jetzt Rust, dass für eine bestimmte Lebenszeit 'a die Funktion zwei Parameter annimmt, von denen beide String-Slices sind, die mindestens so lange leben wie die Lebenszeit 'a. Die Funktionssignatur sagt auch Rust, dass der String-Slice, der von der Funktion zurückgegeben wird, mindestens so lange leben wird wie die Lebenszeit 'a. Im praktischen Einsatz bedeutet das, dass die Lebenszeit der von der longest-Funktion zurückgegebenen Referenz die gleiche ist wie die kleinere der Lebenszeiten der Werte, auf die die Funktionsargumente verweisen. Diese Beziehungen sind das, was wir möchten, dass Rust bei der Analyse dieses Codes verwendet.

Denken Sie daran, dass wir bei der Angabe der Lebenszeitparameter in dieser Funktionssignatur die Lebenszeiten von Werten, die übergeben oder zurückgegeben werden, nicht ändern. Stattdessen geben wir an, dass der Borrow-Checker alle Werte ablehnen soll, die diesen Einschränkungen nicht entsprechen. Beachten Sie, dass die longest-Funktion nicht genau wissen muss, wie lange x und y leben werden, sondern nur, dass ein bestimmter Bereich für 'a eingesetzt werden kann, der dieser Signatur entspricht.

Wenn Lebenszeiten in Funktionen annotiert werden, gehen die Annotations in die Funktionssignatur, nicht in den Funktionskörper. Die Lebenszeitannotationen werden zum Teil des Vertrags der Funktion, ähnlich wie die Typen in der Signatur. Dass Funktionssignaturen den Lebenszeitvertrag enthalten, bedeutet, dass die Analyse, die der Rust-Compiler durchführt, einfacher sein kann. Wenn es ein Problem mit der Art, wie eine Funktion annotiert oder aufgerufen wird, können die Compilerfehler auf den Teil unseres Codes und die Einschränkungen genauer verweisen. Wenn der Rust-Compiler stattdessen mehr Schlussfolgerungen über das, was wir als Beziehung der Lebenszeiten beabsichtigen, ziehen würde, könnte der Compiler nur auf einen Gebrauch unseres Codes viele Schritte von der Ursache des Problems hinweisen.

Wenn wir konkrete Referenzen an longest übergeben, ist die konkrete Lebenszeit, die für 'a eingesetzt wird, der Teil des Bereichs von x, der mit dem Bereich von y überlappt. Mit anderen Worten, die generische Lebenszeit 'a erhält die konkrete Lebenszeit, die gleich der kleineren der Lebenszeiten von x und y ist. Da wir die zurückgegebene Referenz mit dem gleichen Lebenszeitparameter 'a annotiert haben, wird die zurückgegebene Referenz auch für die Dauer der kleineren der Lebenszeiten von x und y gültig sein.

Schauen wir uns an, wie die Lebenszeitannotationen die longest-Funktion einschränken, indem wir Referenzen mit unterschiedlichen konkreten Lebenszeiten übergeben. Listing 10-22 ist ein einfaches Beispiel.

Dateiname: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

Listing 10-22: Verwendung der longest-Funktion mit Referenzen auf String-Werte, die unterschiedliche konkrete Lebenszeiten haben

In diesem Beispiel ist string1 bis zum Ende des äußeren Bereichs gültig, string2 ist bis zum Ende des inneren Bereichs gültig und result verweist auf etwas, das bis zum Ende des inneren Bereichs gültig ist. Führen Sie diesen Code aus, und Sie werden sehen, dass der Borrow-Checker zustimmt; er wird kompilieren und ausgeben The longest string is long string is long.

Als nächstes versuchen wir ein Beispiel, das zeigt, dass die Lebenszeit der Referenz in result die kleinere Lebenszeit der beiden Argumente sein muss. Wir verschieben die Deklaration der result-Variablen außerhalb des inneren Bereichs, lassen aber die Zuweisung des Werts an die result-Variable innerhalb des Bereichs mit string2 stehen. Dann verschieben wir die println!, die result verwendet, außerhalb des inneren Bereichs, nachdem der innere Bereich beendet ist. Der Code in Listing 10-23 wird nicht kompilieren.

Dateiname: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

Listing 10-23: Versuch, result nach dem Auslaufen von string2 zu verwenden

Wenn wir diesen Code versuchen, zu kompilieren, erhalten wir diesen Fehler:

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value
does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

Der Fehler zeigt, dass für result gültig zu sein für die println!-Anweisung, string2 bis zum Ende des äußeren Bereichs gültig sein müsste. Rust weiß das, weil wir die Lebenszeiten der Funktionsparameter und Rückgabewerte mit dem gleichen Lebenszeitparameter 'a annotiert haben.

Als Menschen können wir diesen Code betrachten und sehen, dass string1 länger als string2 ist und daher result eine Referenz auf string1 enthalten wird. Da string1 noch nicht außer Gültigkeitsbereich ist, wird eine Referenz auf string1 auch für die println!-Anweisung gültig sein. Der Compiler kann jedoch nicht sehen, dass die Referenz in diesem Fall gültig ist. Wir haben Rust gesagt, dass die Lebenszeit der von der longest-Funktion zurückgegebenen Referenz die gleiche ist wie die kleinere der Lebenszeiten der übergebenen Referenzen. Daher verbietet der Borrow-Checker den Code in Listing 10-23 als möglicherweise eine ungültige Referenz.

Versuchen Sie, weitere Experimente zu entwerfen, die die Werte und Lebenszeiten der Referenzen variieren, die an die longest-Funktion übergeben werden, und wie die zurückgegebene Referenz verwendet wird. Machen Sie Hypothesen darüber, ob Ihre Experimente den Borrow-Checker bestehen werden, bevor Sie kompilieren; überprüfen Sie dann, ob Sie Recht haben!

Thinking in Terms of Lifetimes

Die Art, in der Sie Lebenszeitparameter angeben müssen, hängt davon ab, was Ihre Funktion macht. Beispielsweise würden wir keine Lebenszeit für den Parameter y angeben müssen, wenn wir die Implementierung der longest-Funktion ändern würden, um immer den ersten Parameter statt den längsten String-Slice zurückzugeben. Der folgende Code wird kompilieren:

Dateiname: src/main.rs

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Wir haben einen Lebenszeitparameter 'a für den Parameter x und den Rückgabetyp angegeben, aber nicht für den Parameter y, weil die Lebenszeit von y keine Beziehung zur Lebenszeit von x oder zum Rückgabewert hat.

Wenn eine Referenz von einer Funktion zurückgegeben wird, muss der Lebenszeitparameter für den Rückgabetyp mit dem Lebenszeitparameter eines der Parameter übereinstimmen. Wenn die zurückgegebene Referenz nicht auf einen der Parameter verweist, muss sie auf einen innerhalb dieser Funktion erstellten Wert verweisen. Dies wäre jedoch ein verhängender Verweis, da der Wert am Ende der Funktion außer Gültigkeitsbereich geht. Betrachten Sie diese versuchte Implementierung der longest-Funktion, die nicht kompilieren wird:

Dateiname: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Hier, obwohl wir einen Lebenszeitparameter 'a für den Rückgabetyp angegeben haben, wird diese Implementierung fehlschlagen, weil die Lebenszeit des Rückgabewerts überhaupt keine Beziehung zur Lebenszeit der Parameter hat. Hier ist die Fehlermeldung, die wir erhalten:

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

Das Problem ist, dass result außer Gültigkeitsbereich geht und am Ende der longest-Funktion bereinigt wird. Wir versuchen auch, eine Referenz auf result aus der Funktion zurückzugeben. Es gibt keine Möglichkeit, dass wir Lebenszeitparameter angeben, die den verhängenden Verweis ändern würden, und Rust lässt uns keinen verhängenden Verweis erstellen. In diesem Fall wäre die beste Lösung, einen eigenen Datentyp zurückzugeben, anstatt eine Referenz, sodass die aufrufende Funktion dann für das Bereinigen des Werts verantwortlich ist.

Letztendlich ist die Lebenszeitsyntax dazu da, die Lebenszeiten verschiedener Parameter und Rückgabewerte von Funktionen zu verbinden. Wenn sie verbunden sind, hat Rust genug Informationen, um sicherheitsrelevante Operationen zu ermöglichen und Operationen zu verbieten, die verhängende Zeiger erzeugen oder anderweitig die Arbeitsspeichersicherheit verletzen.

Lifetime Annotations in Struct Definitions

Bisher haben alle von uns definierten Structs nur eigene Typen gespeichert. Wir können Structs definieren, um Referenzen zu speichern, aber in diesem Fall müssten wir eine Lebenszeitannotation für jede Referenz in der Struct-Definition hinzufügen. Listing 10-24 hat einen Struct namens ImportantExcerpt, der einen String-Slice speichert.

Dateiname: src/main.rs

1 struct ImportantExcerpt<'a> {
  2 part: &'a str,
}

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
       .split('.')
       .next()
       .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Listing 10-24: Ein Struct, der eine Referenz speichert und eine Lebenszeitannotation erfordert

Dieser Struct hat das einzelne Feld part, das einen String-Slice speichert, der eine Referenz ist [2]. Wie bei generischen Datentypen deklarieren wir den Namen des generischen Lebenszeitparameters innerhalb von spitzen Klammern nach dem Namen des Structs, damit wir den Lebenszeitparameter im Körper der Struct-Definition verwenden können [1]. Diese Annotation bedeutet, dass eine Instanz von ImportantExcerpt nicht länger existieren kann als die Referenz, die sie in ihrem part-Feld hält.

Die main-Funktion hier erstellt eine Instanz des ImportantExcerpt-Structs [5], der eine Referenz auf den ersten Satz der von der Variable novel [3] besitzten String [4] hält. Die Daten in novel existieren vor der Erstellung der ImportantExcerpt-Instanz. Darüber hinaus geht novel erst außer Gültigkeitsbereich, nachdem die ImportantExcerpt außer Gültigkeitsbereich ist, sodass die Referenz in der ImportantExcerpt-Instanz gültig ist.

Lifetime Elision

Sie haben gelernt, dass jede Referenz eine Lebenszeit hat und dass Sie Lebenszeitparameter für Funktionen oder Structs angeben müssen, die Referenzen verwenden. Allerdings hatten wir in Listing 4-9 eine Funktion, die erneut in Listing 10-25 gezeigt wird, die ohne Lebenszeitannotationen kompilierte.

Dateiname: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Listing 10-25: Eine Funktion, die wir in Listing 4-9 definiert haben, die ohne Lebenszeitannotationen kompilierte, obwohl der Parameter und der Rückgabetyp Referenzen sind

Der Grund, warum diese Funktion ohne Lebenszeitannotationen kompiliert, liegt in der Geschichte: In frühen Versionen (vor 1.0) von Rust hätte dieser Code nicht kompiliert, weil jede Referenz eine explizite Lebenszeit benötigte. Damals hätte die Funktionssignatur so ausgesehen:

fn first_word<'a>(s: &'a str) -> &'a str {

Nachdem die Rust-Mannschaft viel Rust-Code geschrieben hatte, stellte sie fest, dass Rust-Programmierer in bestimmten Situationen immer wieder die gleichen Lebenszeitannotationen eingaben. Diese Situationen waren vorhersehbar und folgten einigen deterministischen Mustern. Die Entwickler haben diese Muster in den Compilercode programmiert, sodass der Borrow-Checker in diesen Situationen die Lebenszeiten ableiten und keine expliziten Annotations benötigen konnte.

Dieser Teil der Rust-Geschichte ist relevant, weil es möglich ist, dass weitere deterministische Muster auftauchen und zum Compiler hinzugefügt werden. In Zukunft könnten sogar noch weniger Lebenszeitannotationen erforderlich sein.

Die Muster, die in die Rust-Analyse von Referenzen programmiert sind, werden als Lebenszeitelisionsregeln bezeichnet. Dies sind keine Regeln, die die Programmierer befolgen müssen; es sind eine Reihe von speziellen Fällen, die der Compiler betrachtet, und wenn Ihr Code diesen Fällen entspricht, müssen Sie die Lebenszeiten nicht explizit schreiben.

Die Elisionsregeln bieten keine vollständige Inferenz. Wenn Rust die Regeln deterministisch anwendet, aber es immer noch Unklarheit darüber besteht, welche Lebenszeiten die Referenzen haben, wird der Compiler nicht raten, welche Lebenszeit die verbleibenden Referenzen haben sollten. Anstatt zu raten, gibt der Compiler Ihnen einen Fehler, den Sie durch Hinzufügen der Lebenszeitannotationen beheben können.

Lebenszeiten von Funktions- oder Methodenparametern werden als Eingangslebenszeiten bezeichnet, und Lebenszeiten von Rückgabewerten werden als Ausgangslebenszeiten bezeichnet.

Der Compiler verwendet drei Regeln, um die Lebenszeiten der Referenzen zu ermitteln, wenn keine expliziten Annotations vorhanden sind. Die erste Regel gilt für Eingangslebenszeiten, und die zweiten und dritten Regeln gelten für Ausgangslebenszeiten. Wenn der Compiler am Ende der drei Regeln angelangt ist und es immer noch Referenzen gibt, für die er die Lebenszeiten nicht ermitteln kann, wird der Compiler mit einem Fehler abbrechen. Diese Regeln gelten für fn-Definitionen sowie für impl-Blöcke.

Die erste Regel ist, dass der Compiler einem jeden Parameter, der eine Referenz ist, einen Lebenszeitparameter zuweist. Mit anderen Worten, eine Funktion mit einem Parameter bekommt einen Lebenszeitparameter: fn foo<'a>(x: &'a i32); eine Funktion mit zwei Parametern bekommt zwei separate Lebenszeitparameter: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); und so weiter.

Die zweite Regel ist, dass, wenn es genau einen Eingangslebenszeitparameter gibt, diese Lebenszeit allen Ausgangslebenszeitparametern zugewiesen wird: fn foo<'a>(x: &'a i32) -> &'a i32.

Die dritte Regel ist, dass, wenn es mehrere Eingangslebenszeitparameter gibt, aber einer von ihnen &self oder &mut self ist, weil dies eine Methode ist, die Lebenszeit von self allen Ausgangslebenszeitparametern zugewiesen wird. Diese dritte Regel macht Methoden viel lesbarer und schreibbarer, weil weniger Symbole erforderlich sind.

Lassen Sie uns vorstellen, dass wir der Compiler sind. Wir werden diese Regeln anwenden, um die Lebenszeiten der Referenzen in der Signatur der first_word-Funktion in Listing 10-25 zu ermitteln. Die Signatur beginnt ohne jede Lebenszeit, die mit den Referenzen assoziiert ist:

fn first_word(s: &str) -> &str {

Dann wendet der Compiler die erste Regel an, die angibt, dass jeder Parameter seine eigene Lebenszeit bekommt. Wir werden es wie üblich 'a nennen, also sieht die Signatur jetzt so aus:

fn first_word<'a>(s: &'a str) -> &str {

Die zweite Regel tritt in Kraft, weil es genau einen Eingangslebenszeitparameter gibt. Die zweite Regel besagt, dass die Lebenszeit des einen Eingangsparameters dem Ausgangslebenszeitparameter zugewiesen wird, also sieht die Signatur jetzt so aus:

fn first_word<'a>(s: &'a str) -> &'a str {

Jetzt haben alle Referenzen in dieser Funktionssignatur Lebenszeiten, und der Compiler kann seine Analyse fortsetzen, ohne dass der Programmierer die Lebenszeiten in dieser Funktionssignatur annotieren muss.

Schauen wir uns ein weiteres Beispiel an, diesmal mit der longest-Funktion, die keine Lebenszeitparameter hatte, als wir in Listing 10-20 damit begannen:

fn longest(x: &str, y: &str) -> &str {

Lassen Sie uns die erste Regel anwenden: Jeder Parameter bekommt seine eigene Lebenszeit. Diesmal haben wir zwei Parameter statt eines, also haben wir zwei Lebenszeiten:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Sie können sehen, dass die zweite Regel nicht zutrifft, weil es mehr als einen Eingangslebenszeitparameter gibt. Die dritte Regel trifft auch nicht zu, weil longest eine Funktion und keine Methode ist, sodass keiner der Parameter self ist. Nachdem wir alle drei Regeln durchlaufen haben, haben wir immer noch nicht herausgefunden, was die Lebenszeit des Rückgabetyps ist. Deshalb haben wir einen Fehler bekommen, als wir den Code in Listing 10-20 kompilieren wollten: Der Compiler hat die Lebenszeitelisionsregeln durchlaufen, konnte aber immer noch nicht alle Lebenszeiten der Referenzen in der Signatur ermitteln.

Da die dritte Regel eigentlich nur in Methodensignaturen gilt, werden wir im nächsten Abschnitt die Lebenszeiten in diesem Kontext betrachten, um zu verstehen, warum die dritte Regel bedeutet, dass wir Lebenszeiten in Methodensignaturen nicht so oft annotieren müssen.

Lifetime Annotations in Method Definitions

Wenn wir Methoden für einen Struct mit Lebenszeiten implementieren, verwenden wir die gleiche Syntax wie für generische Typparameter, wie in Listing 10-11 gezeigt. Wo wir die Lebenszeitparameter deklarieren und verwenden, hängt davon ab, ob sie mit den Struct-Feldern oder den Methodenparametern und Rückgabewerten zusammenhängen.

Lebenszeitsnamen für Struct-Felder müssen immer nach dem impl-Schlüsselwort deklariert und dann nach dem Struct-Namen verwendet werden, weil diese Lebenszeiten Teil des Struct-Typs sind.

In Methodensignaturen innerhalb des impl-Blocks können Referenzen entweder an die Lebenszeit von Referenzen in den Struct-Feldern gebunden sein oder unabhängig davon sein. Darüber hinaus machen die Lebenszeitelisionsregeln oftmals so, dass Lebenszeitannotationen in Methodensignaturen nicht erforderlich sind. Schauen wir uns einige Beispiele an, die den in Listing 10-24 definierten Struct namens ImportantExcerpt verwenden.

Zunächst verwenden wir eine Methode namens level, deren einziger Parameter eine Referenz auf self ist und deren Rückgabewert ein i32 ist, was keine Referenz auf etwas ist:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

Die Lebenszeitparameterdeklaration nach impl und ihre Verwendung nach dem Typnamen sind erforderlich, aber wir müssen die Lebenszeit der Referenz auf self nicht annotieren, weil die erste Elisionsregel gilt.

Hier ist ein Beispiel, in dem die dritte Lebenszeitelisionsregel zutrifft:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

Es gibt zwei Eingangslebenszeiten, daher wendet Rust die erste Lebenszeitelisionsregel an und gibt sowohl &self als auch announcement ihre eigenen Lebenszeiten. Dann, weil einer der Parameter &self ist, erhält der Rückgabetyp die Lebenszeit von &self, und alle Lebenszeiten sind berücksichtigt.

The Static Lifetime

Eine besondere Lebenszeit, über die wir sprechen müssen, ist 'static, die angibt, dass die betroffene Referenz kann für die gesamte Laufzeit des Programms existieren. Alle String-Literale haben die Lebenszeit 'static, die wir wie folgt annotieren können:

let s: &'static str = "I have a static lifetime.";

Der Text dieses Strings wird direkt im Binärprogramm gespeichert, das immer verfügbar ist. Daher ist die Lebenszeit aller String-Literale 'static.

Sie können in Fehlermeldungen Vorschläge sehen, die die Lebenszeit 'static verwenden. Bevor Sie 'static als Lebenszeit für eine Referenz angeben, überlegen Sie sich, ob die Referenz tatsächlich die gesamte Lebenszeit Ihres Programms hat und ob Sie das möchten. In den meisten Fällen resultiert eine Fehlermeldung, die die Lebenszeit 'static vorschlägt, aus einem Versuch, einen verhängenden Verweis zu erstellen oder aus einem Missmatch der verfügbaren Lebenszeiten. In solchen Fällen ist die Lösung, diese Probleme zu beheben, nicht die Lebenszeit 'static anzugeben.

Generic Type Parameters, Trait Bounds, and Lifetimes Together

Schauen wir uns kurz die Syntax an, um generische Typparameter, Trait-Bounds und Lebenszeiten in einer Funktion anzugeben!

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Dies ist die longest-Funktion aus Listing 10-21, die die längere von zwei String-Slices zurückgibt. Aber jetzt hat es einen zusätzlichen Parameter namens ann vom generischen Typ T, der mit jedem Typ ausgefüllt werden kann, der das Display-Trait implementiert, wie in der where-Klausel angegeben. Dieser zusätzliche Parameter wird mit {} gedruckt, weshalb die Display-Trait-Bound erforderlich ist. Da Lebenszeiten eine Art von Generics sind, werden die Deklarationen des Lebenszeitparameters 'a und des generischen Typparameters T in derselben Liste innerhalb der spitzen Klammern nach dem Funktionsnamen angegeben.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab Validating References With Lifetimes abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.