Borrowing in Rust
Auf der Seite über Ownership bist du auf ein Problem gestoßen: Wenn du einen String an eine Funktion übergibst, wird er verschoben (Move), und du kannst ihn danach nicht mehr benutzen. Werte ständig hin und her zu schieben (oder zu klonen) wäre mühsam. Die Lösung von Rust heißt Borrowing (Ausleihen).
Borrowing bedeutet: Du lässt anderen Code einen Wert benutzen, ohne ihm das Ownership zu geben – so, wie du einer Freundin ein Buch leihst. Sie kann es lesen und gibt es dir dann zurück. Es gehört die ganze Zeit dir.
Referenzen: Ausleihen mit &
Abschnitt betitelt „Referenzen: Ausleihen mit &“Eine Referenz erstellst du, indem du & vor einen Wert schreibst. Mit einer Referenz kann eine Funktion die Daten ansehen, ohne dass sie ihr gehören:
fn print_length(s: &String) { // leiht sich s aus, besitzt es aber nicht println!("Länge: {}", s.len());}
fn main() { let text = String::from("hello"); print_length(&text); // text an die Funktion ausleihen println!("Gehört immer noch mir: {text}"); // ✅ text ist hier weiterhin gültig}Ausgabe:
Länge: 5Gehört immer noch mir: helloDas Wort "hello" bleibt hier absichtlich auf Englisch: len() zählt Bytes, und mit Umlauten (wie in „Grüße“) käme eine andere Zahl heraus, als du vielleicht erwartest.
&text bedeutet: „Diese Funktion darf sich text ausleihen.“ Weil die Funktion den Wert nur ausgeliehen hat, bleibt das Ownership bei main, und du kannst text danach weiter benutzen. Vergleiche das mit der Ownership-Seite: Dort hat derselbe Code ohne & einen Fehler ausgelöst.
Veränderbare Referenzen: ausleihen, um etwas zu ändern
Abschnitt betitelt „Veränderbare Referenzen: ausleihen, um etwas zu ändern“Eine einfache &-Referenz ist nur lesend – anschauen ja, anfassen nein. Wenn eine Funktion den ausgeliehenen Wert ändern soll, brauchst du eine veränderbare Referenz mit &mut:
fn add_excitement(s: &mut String) { s.push_str("!!!");}
fn main() { let mut text = String::from("hallo"); add_excitement(&mut text); // veränderbar ausleihen println!("{text}"); // hallo!!!}Ausgabe:
hallo!!!Hier müssen drei Dinge zusammenpassen: text ist mit mut deklariert, die Funktion verlangt einen &mut String, und beim Aufruf übergibst du &mut text. Nur wenn alle drei stimmen, kannst du einen ausgeliehenen Wert ändern.
Die Regeln für Borrowing
Abschnitt betitelt „Die Regeln für Borrowing“Rust achtet bei Referenzen auf zwei Regeln. Sie verhindern sogenannte Data Races (Datenwettläufe): eine ganze Gruppe von Fehlern, bei denen zwei Codestellen gleichzeitig auf dieselben Daten zugreifen und sie dabei beschädigen.
Zu jedem Zeitpunkt darfst du entweder
- beliebig viele nur lesende Referenzen (
&) haben oder - genau eine veränderbare Referenz (
&mut),
aber niemals beides gleichzeitig.
Kurz gesagt: viele Leser oder ein Schreiber, nie beides zusammen. Dieser Code verstößt gegen die Regel:
fn main() { let mut text = String::from("hallo");
let r1 = &text; // eine nur lesende Ausleihe let r2 = &mut text; // ❌ veränderbar ausleihen geht nicht, solange r1 existiert
println!("{r1} {r2}");}Der Compiler hält dich auf:
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutableDie Meldung bedeutet: „text kann nicht veränderbar ausgeliehen werden, weil es bereits unveränderbar ausgeliehen ist.“ Diese Prüfung übernimmt der Borrow Checker, ein Teil des Compilers.
Wann endet ein Borrow?
Abschnitt betitelt „Wann endet ein Borrow?“Vielleicht erwartest du, dass ein Borrow bis zur schließenden } seines Scopes dauert. Das stimmt nicht. Ein Borrow dauert nur bis zu der Stelle, an der die Referenz zuletzt benutzt wird. Danach darf der Wert wieder ausgeliehen werden, sogar veränderbar.
Das fehlerhafte Beispiel von oben kannst du also reparieren, indem du mit r1 fertig bist, bevor du r2 erstellst:
fn main() { let mut text = String::from("hallo");
let r1 = &text; println!("Lese: {r1}"); // letzte Benutzung von r1, hier endet der Borrow
let r2 = &mut text; // ✅ in Ordnung: niemand liest mehr r2.push_str(", Welt");
println!("{text}");}Ausgabe:
Lese: hallohallo, WeltDie Regel lautet weiterhin „viele Leser oder ein Schreiber“. Rust prüft sie nur über den Abschnitt, in dem jede Referenz tatsächlich benutzt wird. Der Leser r1 ist fertig, wenn der Schreiber r2 auftaucht. Und r2 ist fertig, bevor das letzte println! wieder text liest. Nichts überschneidet sich, also ist Rust zufrieden.
So behebst du einen Borrow-Fehler am häufigsten: Ändere die Reihenfolge. Erledige erst alles Lesen und dann das Schreiben (oder umgekehrt), statt beides zu vermischen.
Warum das wichtig ist
Abschnitt betitelt „Warum das wichtig ist“Diese Regeln werden komplett beim Kompilieren geprüft. Zur Laufzeit kosten sie also nichts. So kann Rust „keine Data Races“ versprechen und trotzdem so schnell sein wie C – die Sicherheit ist bewiesen, bevor das Programm überhaupt startet.
Übungen
Abschnitt betitelt „Übungen“Probier jede Aufgabe zuerst selbst, im Rust Playground oder in einem lokalen Projekt. Wenn sich der Compiler beschwert, lies die Fehlermeldung: Meistens zeigt sie genau, wo sich die Borrows überschneiden.
1. Zählen, ohne wegzunehmen
Abschnitt betitelt „1. Zählen, ohne wegzunehmen“Schreib eine Funktion total_letters, die sich einen Vec<String> ausleiht und zurückgibt, wie viele Buchstaben alle Wörter zusammen haben. Nach dem Aufruf soll main den Vektor immer noch ausgeben können.
Tipp: Der Parametertyp ist &Vec<String>, und du rufst die Funktion mit &words auf. Lauf mit for word in list durch den ausgeliehenen Vektor und zähl word.len() zusammen.
Lösung anzeigen
fn total_letters(list: &Vec<String>) -> usize { let mut total = 0; for word in list { total += word.len(); } total}
fn main() { let words = vec![String::from("borrow"), String::from("check")]; let count = total_letters(&words); // den Vektor ausleihen println!("{count} Buchstaben"); println!("{:?} gehört immer noch main", words);}Ausgabe:
11 Buchstaben["borrow", "check"] gehört immer noch mainDie Wörter "borrow" und "check" bleiben auf Englisch, weil len() Bytes zählt. Ein Umlaut würde zwei Bytes belegen und die Zahl verfälschen.
Die Funktion muss sich die Wörter nur ansehen, also reicht ein nur lesender &-Borrow. Würdest du stattdessen list: Vec<String> schreiben, würde der Vektor in die Funktion verschoben, und das letzte println! ließe sich nicht kompilieren.
2. Eine Einkaufsliste
Abschnitt betitelt „2. Eine Einkaufsliste“Schreib zwei Funktionen: add_item, die einen Artikel zu einer Einkaufsliste hinzufügt, und print_list, die alle Artikel ausgibt. Füge drei Artikel hinzu und gib dann die Liste aus.
Tipp: Eine Funktion ändert die Liste, die andere liest sie nur. Welche braucht &mut Vec<String>, und welcher reicht &Vec<String>? Aus einem &str machst du mit String::from(item) einen String.
Lösung anzeigen
fn add_item(list: &mut Vec<String>, item: &str) { list.push(String::from(item));}
fn print_list(list: &Vec<String>) { for item in list { println!("- {item}"); }}
fn main() { let mut list = Vec::new(); add_item(&mut list, "Milch"); add_item(&mut list, "Brot"); add_item(&mut list, "Äpfel"); print_list(&list);}Ausgabe:
- Milch- Brot- Äpfeladd_item ändert die Liste, bekommt also einen &mut-Borrow, und list in main muss mut sein. print_list liest nur, deshalb reicht ein einfaches &. Jeder &mut list-Borrow endet, sobald der Aufruf zurückkehrt. Darum kann der nächste Aufruf die Liste wieder ausleihen.
3. Zwei Schreiber auf einmal
Abschnitt betitelt „3. Zwei Schreiber auf einmal“Dieses Programm lässt sich nicht kompilieren. Repariere es so, dass es Punktestand: 10 Punkte ausgibt, und behalte dabei sowohl first als auch second.
fn main() { let mut message = String::from("Punktestand:"); let first = &mut message; let second = &mut message; first.push_str(" 10"); second.push_str(" Punkte"); println!("{message}");}error[E0499]: cannot borrow `message` as mutable more than once at a time --> src/main.rs:4:18 |3 | let first = &mut message; | ------------ first mutable borrow occurs here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | first.push_str(" 10"); | ----- first borrow later used hereDie Meldung bedeutet: „message kann nicht mehr als einmal gleichzeitig veränderbar ausgeliehen werden.“ Der Compiler zeigt dir den ersten Borrow, den zweiten und die Stelle, an der der erste noch benutzt wird.
Tipp: immer nur ein Schreiber. Sieh dir oben Wann endet ein Borrow? an: Kannst du mit first fertig sein, bevor second erstellt wird?
Lösung anzeigen
fn main() { let mut message = String::from("Punktestand:");
let first = &mut message; first.push_str(" 10"); // letzte Benutzung von first: hier endet sein Borrow
let second = &mut message; second.push_str(" Punkte");
println!("{message}");}Ausgabe:
Punktestand: 10 PunkteIm Original sollte first noch benutzt werden (in Zeile 5), als second in Zeile 4 erstellt wurde. Das sind zwei Schreiber gleichzeitig. Wenn du first.push_str nach oben ziehst, ist first fertig, bevor second existiert. So gibt es immer nur ein &mut auf einmal.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Borrowing (
&) lässt Code einen Wert benutzen, ohne das Ownership zu übernehmen – du bekommst ihn zurück. - Eine einfache
&-Referenz ist nur lesend. Mit&mutkannst du den ausgeliehenen Wert ändern. - Um über eine Referenz etwas zu ändern, brauchen die Variable, der Parameter und der Aufruf alle
mutbzw.&mut. - Die Regel: viele Leser oder ein Schreiber, nie beides gleichzeitig.
- Ein Borrow endet bei der letzten Benutzung der Referenz. Deshalb behebt oft schon eine andere Reihenfolge im Code einen Borrow-Fehler.
- Das alles wird beim Kompilieren geprüft und kostet zur Laufzeit nichts.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was bewirkt ein & vor einem Wert, wenn du ihn an eine Funktion übergibst?
2. Welche Kombination ist laut den Borrowing-Regeln gleichzeitig erlaubt?
3. Was brauchst du, um einen Wert über eine Referenz zu ändern?
4. Warum kosten die Borrowing-Regeln in Rust zur Laufzeit keine Leistung?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Referenzen und Borrowing – Das Rust-Buch: das offizielle Kapitel, auch zu hängenden Referenzen und warum Rust sie gar nicht erst entstehen lässt.
- Borrowing – Rust by Example (auf Englisch): ausführbare Beispiele für
&-Borrows, mit Folgeseiten zu veränderbaren Borrows und Aliasing. - Non-lexical lifetimes – The Edition Guide (auf Englisch): die Änderung, durch die Borrows bei ihrer letzten Benutzung enden statt erst an der schließenden Klammer.
- Der Slice-Typ – Das Rust-Buch: nur einen Teil eines Strings oder Arrays ausleihen – der natürliche nächste Schritt nach Referenzen.
reference– Doku der Standardbibliothek (auf Englisch): was&Tund&mut Tgenau sind und welche Traits sie implementieren.