Deep Dive into Rust Lifetimes: Borrow Checking und Memory Management
Emily Parker
Product Engineer · Leapcell

Was ist eine Lifetime?
Definition von Lifetime
In Rust hat jede Referenz eine Lifetime, die den Zeitraum darstellt, in dem der Wert, auf den die Referenz verweist, im Speicher existiert (es kann auch als der Bereich von Codezeilen betrachtet werden, in dem die Referenz gültig bleibt). Lifetimes stellen sicher, dass Referenzen während ihrer gesamten Lebensdauer gültig bleiben. Sie existieren, um die Gültigkeit von Referenzen zu gewährleisten.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Im obigen Code hat die Funktion longest
zwei Eingabeparameter, die beide Referenzen auf String-Slices sind. Sie hat auch einen Rückgabewert, der eine Referenz auf einen String-Slice ist. Da Rust stark auf Speichersicherheit fokussiert ist, werden Lifetimes eingeführt, um die Gültigkeit von Referenzen zu gewährleisten. Um zu überprüfen, ob die zurückgegebene Referenz gültig ist, müssen wir zuerst ihre Lifetime bestimmen. Aber wie bestimmen wir sie?
Rust kann die Lifetimes von Funktionsparametern und Rückgabewerten automatisch ableiten, was in späteren Abschnitten behandelt wird. Diese Ableitung ist jedoch nicht universell; Rust kann Lifetimes nur in drei spezifischen Szenarien ableiten. Der obige Code fällt nicht unter diese Fälle. In solchen Situationen müssen wir die Lifetimes manuell annotieren. Ohne explizite Annotationen kann Rusts Borrow Checker die Lifetime des Rückgabewertes nicht bestimmen und somit die Gültigkeit der Referenz nicht überprüfen.
Betrachten wir den Code erneut: Der Rückgabewert stammt von den Parametern. Wäre es ausreichend sicherzustellen, dass der Rückgabewert die gleiche Lifetime wie die Parameter hat? Zumindest innerhalb des Gültigkeitsbereichs des Funktionsaufrufs würde dies sicherstellen, dass die Referenz gültig bleibt. Da es jedoch zwei Parameter gibt, können sich ihre Lifetimes unterscheiden. Mit welcher sollte der Rückgabewert assoziiert werden? Die Lösung ist einfach: Der Rückgabewert sollte die gleiche Lifetime wie der Parameter mit der kürzesten Lebensdauer haben. Auf diese Weise bleibt der Rückgabewert mindestens so lange gültig, wie beide Parameter gültig sind. Somit bedeutet die Annotation 'a
im obigen Code, dass die Lifetime des Rückgabewerts der Schnittmenge der Lifetimes beider 'a
-Parameter entspricht. Dies stellt sicher, dass die Lifetime des Rückgabewerts wohldefiniert ist, sodass Rust überprüfen kann, ob seine Referenz gültig ist.
Lifetime und Speicherverwaltung
Rust verwaltet den Speicher mithilfe von Lifetimes. Wenn eine Variable ihren Gültigkeitsbereich verlässt, wird der von ihr belegte Speicher freigegeben. Wenn eine Referenz auf Speicher verweist, der bereits freigegeben wurde, wird sie zu einer hängenden Referenz, und der Versuch, sie zu verwenden, führt zu einem Kompilierungsfehler.
fn main() { let r; { let x = 5; r = &x; } println!("r: {}", r); }
Im obigen Code wird die Variable x
freigegeben, wenn sie ihren Gültigkeitsbereich verlässt, aber die Variable r
enthält immer noch eine Referenz darauf. Dies erzeugt eine hängende Referenz. Der Rust-Compiler erkennt dieses Problem und gibt eine Fehlermeldung aus.
Warum werden Lifetimes benötigt?
Verhindern von hängenden Referenzen und Gewährleistung der Speichersicherheit
Wie bereits erwähnt, verwendet Rust Lifetimes, um hängende Referenzen zu verhindern. Der Compiler überprüft die Lifetimes aller Referenzen im Code, um sicherzustellen, dass sie während ihrer gesamten Lebensdauer gültig bleiben.
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Im obigen Code gibt die Funktion longest
eine Referenz auf einen String-Slice zurück. Der Compiler überprüft, ob die Lifetime des Rückgabewertes gültig ist. Wenn der Rückgabewert eine hängende Referenz wäre, würde der Compiler einen Fehler generieren.
Hier ist ein weiteres Beispiel, das zeigt, wie Rust die Speichersicherheit durch Lifetimes gewährleistet:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } 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); }
In diesem Code definieren wir eine Funktion namens longest
, die zwei String-Slices als Parameter entgegennimmt und einen String-Slice zurückgibt. Die Funktion verwendet den Lifetime-Parameter 'a
, um die Beziehung zwischen den Lifetimes der Eingabeparameter und des Rückgabewerts anzugeben.
In der Funktion main
erstellen wir zwei String-Variablen, string1
und string2
, und übergeben ihre Slices an longest
. Da longest
verlangt, dass die Eingabeparameter und der Rückgabewert die gleiche Lifetime haben, überprüft der Compiler, ob die Slices diese Anforderung erfüllen. Hier hat string2
eine kürzere Lifetime als string1
, sodass der Compiler einen Fehler meldet und warnt, dass der Rückgabewert eine hängende Referenz enthalten könnte. Dieser Mechanismus gewährleistet die Speichersicherheit.
Lifetime-Syntax
Lifetime-Annotationen
In Funktionsdefinitionen können Lifetime-Parameter mithilfe von spitzen Klammern annotiert werden. Lifetime-Parameternamen müssen mit einem Apostroph beginnen, z. B. 'a
.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Im obigen Code hat die Funktion longest
zwei Eingabeparameter, die beide Referenzen auf String-Slices sind. Diese Referenzen haben einen Lifetime-Parameter 'a
, der angibt, dass sie die gleiche Lifetime haben müssen. Der Rückgabewert hat ebenfalls einen Lifetime-Parameter 'a
, was bedeutet, dass seine Lifetime mit der der Eingabeparameter übereinstimmt.
Lifetime-Elisionsregeln
In vielen Fällen kann der Rust-Compiler Referenz-Lifetimes automatisch ableiten, sodass Sie Lifetime-Annotationen weglassen können.
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
In diesem Fall kann der Compiler die Lifetimes der Parameter und des Rückgabewerts nicht bestimmen. Da der Rückgabewert vom Vergleich der beiden Parameter abhängt, kann der Compiler nicht ableiten, welche Lifetime des Parameters verwendet werden soll.
Wenn der Compiler die Lifetime des Rückgabewertes der Funktion nicht bestimmen kann, gibt er einen Fehler aus und fordert den Entwickler auf, Lifetime-Parameter explizit anzugeben. Zum Beispiel können wir die Funktion longest
wie folgt modifizieren:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Hier gibt der Lifetime-Parameter 'a
an, dass die Eingabeparameter und der Rückgabewert die gleiche Lifetime haben müssen. Dies ermöglicht es dem Compiler zu überprüfen, ob die Funktionsargumente die Lifetime-Constraints erfüllen, und stellt sicher, dass der Rückgabewert keine hängende Referenz enthält.
In vielen Fällen kann der Rust-Compiler Lifetimes jedoch automatisch ableiten. Rust wendet eine Reihe von Lifetime-Elisionsregeln an, um die richtigen Lifetimes abzuleiten. Diese Regeln lauten wie folgt:
- Jeder Referenzparameter erhält seinen eigenen Lifetime-Parameter. Zum Beispiel wird
fn foo(x: &i32)
infn foo<'a>(x: &'a i32)
konvertiert. - Wenn eine Funktion einen einzelnen Eingabe-Lifetime-Parameter hat, wird diese Lifetime allen Ausgabe-Lifetime-Parametern zugewiesen. Zum Beispiel wird
fn foo<'a>(x: &'a i32) -> &i32
infn foo<'a>(x: &'a i32) -> &'a i32
konvertiert. - Wenn eine Funktion mehrere Eingabe-Lifetime-Parameter hat, aber einer davon
&self
oder&mut self
ist, erhält der Rückgabewert die Lifetime vonself
. Zum Beispiel wirdfn foo(&self, x: &i32) -> &i32
infn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32
konvertiert.
Mit diesen Regeln kann der Rust-Compiler Lifetimes in vielen Fällen automatisch ableiten. In komplexen Szenarien benötigt der Compiler jedoch möglicherweise weiterhin explizite Lifetime-Annotationen.
Anwendungsfälle von Lifetimes
Funktionsparameter und Rückgabewerte
Wenn die Eingabeparameter oder Rückgabewerte einer Funktion Referenzen enthalten, müssen Lifetimes verwendet werden, um die Gültigkeit dieser Referenzen sicherzustellen.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Im obigen Code hat die Funktion longest
zwei Eingabeparameter, die beide Referenzen auf String-Slices sind. Diese Referenzen haben einen Lifetime-Parameter 'a
, was bedeutet, dass sie die gleiche Lifetime haben müssen. Der Rückgabewert der Funktion hat auch einen Lifetime-Parameter 'a
, der angibt, dass seine Lifetime mit den Eingabeparametern übereinstimmt.
Struct-Definitionen
Wenn eine Struct Referenzen enthält, müssen Lifetimes verwendet werden, um die Gültigkeit der Referenzen sicherzustellen.
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence }; }
Im obigen Code enthält die Struct ImportantExcerpt
eine Referenz auf einen String-Slice. Diese Referenz hat einen Lifetime-Parameter 'a
, der angibt, dass sie eine wohldefinierte Lifetime haben muss. Um hängende Referenzen zu verhindern, muss der String-Slice die gleiche Lifetime wie die Struct haben, um sicherzustellen, dass der String-Slice auch gültig ist, solange die Struct gültig ist.
Erweiterte Verwendung von Lifetimes
Lifetime-Subtyping und Polymorphismus
Rust unterstützt Lifetime-Subtyping und Polymorphismus. Lifetime-Subtyping bedeutet, dass eine Lifetime eine Teilmenge einer anderen sein kann.
fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
In diesem Beispiel hat der erste Eingabeparameter eine Lifetime 'a
, während der zweite Eingabeparameter keine explizite Lifetime-Annotation hat. Dies bedeutet, dass der zweite Eingabeparameter eine beliebige Lifetime haben kann und den Rückgabewert nicht beeinflusst.
Statische Lifetime
Rust hat eine spezielle Lifetime namens 'static
, die angibt, dass eine Referenz für die gesamte Dauer des Programms gültig ist.
let s: &'static str = "I have a static lifetime.";
In diesem Beispiel ist die Variable s
eine Referenz auf einen String-Slice mit einer statischen Lifetime, was bedeutet, dass sie während der gesamten Programmausführung gültig bleibt.
Lifetimes und der Borrow Checker
Rolle des Borrow Checkers
Der Compiler von Rust enthält einen Borrow Checker, der sicherstellt, dass alle Referenzen die Borrowing-Regeln einhalten. Wenn die Regeln verletzt werden, generiert der Compiler einen Fehler.
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; println!("{}, {}, and {}", r1, r2, r3); }
In diesem Code hat die Variable s
sowohl unveränderliche Referenzen (r1
und r2
) als auch eine veränderliche Referenz (r3
) im selben Gültigkeitsbereich. Dies verstößt gegen die Borrowing-Regeln von Rust. Der Compiler erkennt dieses Problem und generiert einen Fehler.
Lifetime-Prüfungen stellen sicher, dass Referenzen während ihrer gesamten Existenz gültig bleiben. Dieselbe Lifetime zu haben bedeutet jedoch nicht unbedingt, dass Borrowing erlaubt ist. Die Borrowing-Regeln von Rust berücksichtigen sowohl die Gültigkeit der Lifetime als auch die Mutabilitätsbeschränkungen.
Im obigen Code verstoßen r1
, r2
und r3
, obwohl sie die gleiche Lifetime haben, gegen die Borrowing-Regeln von Rust, da sie versuchen, sowohl unveränderliche als auch veränderliche Referenzen auf dieselbe Variable innerhalb desselben Gültigkeitsbereichs zu erstellen. Gemäß den Borrowing-Regeln von Rust:
- Sie können mehrere unveränderliche Referenzen auf eine Variable gleichzeitig haben.
- Sie können eine veränderliche Referenz haben, aber keine anderen Referenzen (veränderlich oder unveränderlich) gleichzeitig.
Dies gewährleistet die Speichersicherheit und verhindert Data Races.
Einschränkungen von Lifetimes
Obwohl Rust Lifetimes verwendet, um den Speicher zu verwalten und die Sicherheit zu gewährleisten, haben Lifetimes auch einige Einschränkungen. In einigen Fällen kann der Compiler beispielsweise die richtigen Lifetimes nicht automatisch ableiten, was explizite Annotationen vom Programmierer erfordert. Dies kann die Belastung der Entwickler erhöhen und die Lesbarkeit des Codes verringern.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Language-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen mit einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Einblicke.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ