Borrowing und Lifetime in Rust durch Beispiele
1. Überblick
Borrowing und Lifetime sind zwei Konzepte, mit denen Rust-Anfänger am meisten gegen den Compiler kämpfen. Dieser Artikel erklärt Borrowing (&T, &mut T) und Lifetime Annotation ('a) anhand vertrauter Domain-Beispiele wie Order und Customer. Nachdem du diesen Artikel gelesen hast, kannst du die Fehlermeldungen des Borrow Checkers lesen, weißt, wann du Lifetime Annotation explizit schreiben musst, und wann der Compiler sie selbst ableiten kann.
2. Ownership kurz aufgefrischt
Bevor wir zum Borrowing kommen, musst du folgendes bedenken: Jeder Wert in Rust hat nur einen Besitzer. Wenn du einen Wert als Wert übergibst, wird das Ownership verschoben.
struct Customer {
name: String,
}
fn print_customer(customer: Customer) {
println!("Kunde: {}", customer.name);
}
fn main() {
let customer = Customer { name: String::from("Nguyễn Văn A") };
print_customer(customer);
// println!("{}", customer.name); // Fehler: customer wurde verschoben
}
print_customer empfängt Customer als Wert, also wird customer in die Funktion verschoben. Wenn du den Kommentar entfernst, erhältst du einen Fehler:
error[E0382]: borrow of moved value: `customer`
Borrowing wurde erfunden, um genau dieses Problem zu lösen: einer anderen Funktion Daten “ausleihen”, ohne das Ownership abgeben zu müssen.
3. Immutable Borrowing: &T
Verwende &T, wenn du nur Daten lesen musst, nicht verändern.
struct Order {
items: Vec<f64>,
}
fn calculate_total(order: &Order) -> f64 {
order.items.iter().sum()
}
fn main() {
let order = Order { items: vec![100.0, 250.0, 50.0] };
let total = calculate_total(&order);
println!("Summe: {}", total);
println!("Anzahl Artikel: {}", order.items.len()); // order kann noch verwendet werden
}
Summe: 400
Anzahl Artikel: 3
calculate_total empfängt &Order – eine unveränderliche Referenz. order gehört immer noch zu main, die Funktion “leiht” es sich nur temporär zum Lesen. Deshalb kannst du order nach dem Funktionsaufruf normal verwenden. Du kannst gleichzeitig mehrere &Order haben, solange keine &mut Order dazwischen ist.
4. Mutable Borrowing: &mut T
Wenn du Daten durch eine Referenz verändern musst, verwendest du &mut T.
struct Order {
items: Vec<f64>,
discount_percent: f64,
}
fn apply_discount(order: &mut Order, percent: f64) {
order.discount_percent = percent;
}
fn main() {
let mut order = Order { items: vec![100.0, 200.0], discount_percent: 0.0 };
apply_discount(&mut order, 10.0);
println!("Rabatt: {}%", order.discount_percent);
}
Rabatt: 10%
Beachte zwei Dinge: Die Variable order muss mit mut deklariert werden, und der Funktionsaufruf muss &mut order übergeben. Anders als &T kann es nur eine &mut T zur gleichen Zeit geben, und es darf keine anderen &T gleichzeitig existieren.
5. Borrow Checker: die Regel „ein Besitzer, viele Leser oder ein Schreiber”
Die Kernregel: Bei einem Datum kannst du zu jedem Zeitpunkt entweder mehrere &T oder ein &mut T haben, aber nicht beides gleichzeitig.
fn main() {
let mut order = Order { items: vec![100.0, 200.0], discount_percent: 0.0 };
for item in &order.items {
order.items.push(*item); // Fehler: order.items wird von der Schleife unveränderlich ausgeliehen
}
}
for item in &order.items erstellt eine unveränderliche Referenz, die während der ganzen Schleife lebt. In der Schleife braucht order.items.push(...) eine veränderliche Referenz auf dieselben Daten → Konflikt. Der Compiler sagt:
error[E0502]: cannot borrow `order.items` as mutable because it is
also borrowed as immutable
Die häufige Lösung: Sammle die Werte, die hinzugefügt werden sollen, in einem temporären Vec, und führe dann extend nach der Schleife aus – das trennt die zwei Borrows in zwei verschiedene Zeitpunkte.
Hinweis: Der Borrow Checker analysiert nach „tatsächlicher Lebensdauer” (non-lexical lifetimes), nicht nach Block
{}. Wenn sich zwei Borrows zeitlich nicht überlappen, sind sie gültig, auch wenn sie in derselben Funktion sind.
6. Was ist Lifetime?
Lifetime ist die Art, wie Rust ausdrückt, „wie lange diese Referenz gültig bleibt”, damit der Compiler sicherstellen kann, dass du eine Referenz nicht verwendest, nachdem die ursprünglichen Daten gedroppt wurden. Die meiste Zeit musst du Lifetime nicht explizit schreiben – der Compiler leitet sie ab. Aber wenn eine Funktion mehrere Referenzen empfängt und eine Referenz zurückgibt, musst du dem Compiler mitteilen, welcher Parameter die Quelle dieser Rückgabe ist.
6.1. Lifetime Annotation auf Funktionen
fn longest_name<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let customer1 = String::from("Nguyễn Văn A");
let customer2 = String::from("Trần Thị B dài hơn");
let longest = longest_name(&customer1, &customer2);
println!("Längster Name: {}", longest);
}
Längster Name: Trần Thị B dài hơn
'a erstellt keine neue Lebensdauer, es ist nur ein Name zum Verbinden: a, b und der Rückgabewert müssen alle mindestens so lange leben wie 'a. Der Compiler weiß zur Laufzeit nicht, ob die Funktion a oder b zurückgibt, also erzwingt er dass beide Eingaben und die Ausgabe die gleiche Lebensdauer haben, um in allen Fällen sicher zu sein.
Wenn du es falsch schreibst – gibst eine Referenz zurück, die nicht mit der deklarierten Lebensdauer verbunden ist:
fn longest_name_broken<'a>(a: &'a str, _b: &str) -> &'a str {
let local = String::from("temp");
&local // Fehler: local wird am Ende der Funktion gedroppt
}
error[E0515]: cannot return value referencing local variable `local`
6.2. Lifetime in Structs
Structs, die Referenzen enthalten, brauchen auch Lifetime Annotation, weil Rust sicherstellen muss, dass der Struct nicht länger lebt als die Daten, die er ausleiht.
struct Customer {
name: String,
}
struct OrderSummary<'a> {
customer: &'a Customer,
total: f64,
}
fn build_summary<'a>(customer: &'a Customer, total: f64) -> OrderSummary<'a> {
OrderSummary { customer, total }
}
fn main() {
let customer = Customer { name: String::from("Lê Văn C") };
let summary = build_summary(&customer, 500.0);
println!("{} - Summe: {}", summary.customer.name, summary.total);
}
OrderSummary<'a> besitzt Customer nicht, es hält nur eine Referenz. Die Lebensdauer 'a garantiert, dass summary nicht länger leben kann als customer. Wenn du customer vor der Verwendung von summary droppen lässt, blockiert der Compiler dies zur Compile-Zeit – kein Dangling Pointer kann bis zur Laufzeit durchsickern.
6.3. Wann du Lifetime nicht schreiben musst (Elision)
Rust hat 3 Regeln zum automatischen Ableiten von Lifetimes (Lifetime Elision), die auf die meisten Funktionen anwendbar sind:
- Jeder Parameter, der eine Referenz ist, bekommt seine eigene Lebensdauer.
- Wenn die Funktion genau eine Eingabe-Lebensdauer hat, wird sie der Ausgabe zugewiesen.
- Wenn es
&selfoder&mut selfgibt, wird die Lebensdauer vonselfder Ausgabe zugewiesen.
fn first_word(text: &str) -> &str {
// Regel 2: nur 1 Eingabe-Lebensdauer -> wird automatisch der Ausgabe zugewiesen
match text.find(' ') {
Some(index) => &text[..index],
None => text,
}
}
Diese Funktion braucht nicht 'a zu schreiben, weil sie nur einen Referenz-Parameter hat – der Compiler leitet automatisch ab, dass die Ausgabe von text ausgeliehen wird. Du musst Lifetime nur manuell schreiben, wenn es mehr als einen Referenz-Eingabe-Parameter gibt und die Ausgabe nicht in die Fälle 2 oder 3 fällt.
7. Praktisches Beispiel: Parser gibt Slice zurück
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(index) => &text[..index],
None => text,
}
}
fn main() {
let order_note = String::from("liefern schnell noch heute");
let word = first_word(&order_note);
println!("Erstes Wort: {}", word);
}
Erstes Wort: liefern
first_word erstellt keinen neuen String, es gibt einfach ein Slice (&str) zurück, das im Speicher von order_note liegt. Das ist ein sehr häufiges Muster in Rust-Parsern: Vermeidung zusätzlicher Allocationen, nur “Zeigen” auf die Originaldaten. Die Lebensdauer garantiert, dass word nicht länger leben kann als order_note.
Versuche es falsch zu schreiben, um den Compiler bei der Arbeit zu sehen:
fn first_word_broken() -> &str {
let text = String::from("liefern schnell heute");
first_word(&text) // text wird gedroppt, wenn die Funktion endet
}
error[E0106]: missing lifetime specifier
Der Compiler lehnt die Kompilierung ab, weil es keine Möglichkeit gibt, eine gültige Lebensdauer für die Ausgabe zuzuweisen – text wird definitiv vor der Verwendung des Rückgabewerts am Aufrufort sterben.
Tests für first_word
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gibt_erstes_wort_zurueck() {
let note = "liefern schnell heute";
assert_eq!(first_word(note), "liefern");
}
#[test]
fn gibt_ganzen_satz_zurueck_ohne_leerzeichen() {
let note = "versand";
assert_eq!(first_word(note), "versand");
}
}
Führe cargo test aus:
running 2 tests
test tests::gibt_erstes_wort_zurueck ... ok
test tests::gibt_ganzen_satz_zurueck_ohne_leerzeichen ... ok
8. Häufige Fehler
- Referenz auf lokale Variable zurückgeben: Die Variable wird am Ende der Funktion gedroppt, aber die Referenz “lebt” noch am Aufrufort → der Compiler blockiert dies mit einem Lifetime-Fehler, niemals im Runtime.
&Tzu lange behalten, dann&mut Tausleihen: Oft passiert, wenn man das Ergebnis von.iter()in einer Variable speichert und dann den Originalstruct im gleichen Scope mutiert. Lösung: Den Scope des unveränderlichen Borrows mit{}Blöcken oder durch Trennung von Lese- und Schreiblogik in zwei Schritte verengen.clone()missbrauchen, um den Borrow Checker zu vermeiden:clone()kompiliert immer, kostet aber Zeit für das Kopieren der Daten. Verwende es nur, wenn du wirklich eine unabhängige Kopie brauchst, nicht als “Feuerlösch”-Lösung für Borrow-Fehler.- Übermäßige Lifetime Annotation schreiben: Wenn eine Funktion nur einen Referenz-Eingabe-Parameter hat, schreib nicht manuell
'a– die Elision-Regel kümmert sich darum, zusätzliches Schreiben macht den Code nur unlesbar.
Hinweis: Wenn du feststellen musst, dass du
'statichinzufügst, um den Code zu kompilieren, halte inne und überdenke dein Design.'staticerzwingt, dass Daten während des gesamten Programms leben, was normalerweise ein Zeichen dafür ist, dass du das echte Ownership-Problem vermeidest, statt es zu lösen.
9. Fazit
Borrowing lässt dich Daten ausleihen, ohne das Ownership abgeben zu müssen, mit der Regel „viele Leser oder ein Schreiber”, die der Borrow Checker zur Compile-Zeit überprüft. Lifetime ist die Sprache, in der Compiler und du eine gemeinsame Basis schaffen, wie lange eine Referenz gültig bleibt, besonders wichtig, wenn eine Funktion eine Referenz zurückgibt oder ein Struct Referenzen enthält. In den meisten Fällen leitet der Compiler Lifetime automatisch durch Elision-Regeln ab; du musst es nur manuell schreiben, wenn es mehr als einen Referenz-Parameter gibt und unklar ist, woher die Ausgabe kommt.