Ownership & Borrowing
Ownership ist das Herzstueck von Rust und der Grund, warum Rust Speichersicherheit ohne Garbage Collector garantieren kann. Verstehe die drei Regeln, Moves und References.
Inhaltsverzeichnis
Ownership & Borrowing
Ownership ist das definierende Konzept von Rust. Wenn du es verstehst, macht der Rest der Sprache plotzlich Sinn. Am Anfang wirst du gegen den Compiler kaempfen - das ist normal. Der Moment, wo es โklickโ macht, ist unvergleichlich.
Das Problem: Speicher verwalten
Andere Sprachen loesen Speicherverwaltung so:
- C/C++: Du machst es von Hand. Fehler fuehren zu Crashes und Sicherheitsluecken.
- Java/Python/JavaScript: Garbage Collector - bequem, aber langsamer und nicht vorhersagbar.
Rust geht einen dritten Weg: Der Compiler prueft zur Kompilierzeit, wer wann welchen Speicher besitzt. Laufzeit-Kosten: Null.
Die drei Ownership-Regeln
- Jeder Wert hat genau einen Owner (Besitzer).
- Es gibt zu jedem Zeitpunkt nur einen Owner.
- Wenn der Owner den Scope verlaesst, wird der Wert freigegeben.
Beispiel: Ein String
fn main() {
let s = String::from("Hallo");
println!("{}", s);
} // hier wird `s` freigegeben
Der String lebt solange, wie s im Scope ist. Am Ende ruft Rust automatisch drop(s) auf - du siehst es nicht, aber es passiert.
Move-Semantik
Das hier ist der erste โRust-Momentโ:
let s1 = String::from("Hallo");
let s2 = s1; // s1 wurde nach s2 "gemoved"
println!("{}", s1); // FEHLER: value borrowed after move
Was ist passiert?
Stringbesitzt Speicher auf dem Heap.- Eine einfache Kopie ist gefaehrlich - wer wuerde den Speicher freigeben?
- Also moved Rust die Ownership von
s1zus2.s1ist danach ungueltig.
Warum nicht einfach kopieren?
Weil das teuer waere. Fuer einen grossen String wuerde die komplette Heap-Zuteilung verdoppelt. Rust zwingt dich, explizit zu sein:
let s1 = String::from("Hallo");
let s2 = s1.clone(); // explizite (teure) Kopie
println!("{} und {}", s1, s2); // beide funktionieren
Was ist mit Zahlen?
let x = 5;
let y = x;
println!("{} und {}", x, y); // funktioniert!
Zahlen implementieren das Copy-Trait. Sie liegen komplett auf dem Stack, sind billig zu kopieren, und es gibt keinen Heap-Speicher, der verwaltet werden muesste. Deshalb werden sie kopiert, nicht gemoved.
Copy-Typen: alle primitiven Typen (i32, f64, bool, char), Tupel nur aus Copy-Typen.
Ownership bei Funktionsaufrufen
fn main() {
let s = String::from("Hallo");
consume(s); // s wird gemoved
println!("{}", s); // FEHLER
}
fn consume(s: String) {
println!("{}", s);
} // hier wird s freigegeben
Das ist oft unhandlich. Du willst Funktionen aufrufen, ohne Ownership abzugeben.
Die Loesung: References (&)
Eine Reference ist ein Zeiger auf einen Wert, ohne Ownership zu uebernehmen:
fn main() {
let s = String::from("Hallo");
laenge(&s); // nur eine Reference uebergeben
println!("{}", s); // s lebt weiter
}
fn laenge(s: &String) -> usize {
s.len()
}
Das heisst Borrowing - โausleihenโ. Du bekommst Zugriff, bleibst aber nicht der Besitzer.
Mutable References
Standard-References sind read-only. Fuer Schreibzugriff brauchst du &mut:
fn anhaengen(s: &mut String) {
s.push_str(", Welt!");
}
fn main() {
let mut s = String::from("Hallo");
anhaengen(&mut s);
println!("{}", s); // "Hallo, Welt!"
}
Die Borrowing-Regeln
Hier wird es etwas streng - aber die Regeln sind der Grund, warum Rust keine Data Races hat:
- Du kannst beliebig viele unveraenderliche Borrows (
&T) gleichzeitig haben, oder - genau einen veraenderlichen Borrow (
&mut T) - aber nicht beides gleichzeitig. - References muessen immer gueltig sein.
Was das bedeutet
let mut s = String::from("Hallo");
let r1 = &s; // OK
let r2 = &s; // OK - mehrere Reads gleichzeitig
println!("{}, {}", r1, r2);
let r3 = &mut s; // ab hier OK, weil r1/r2 nicht mehr genutzt werden
r3.push_str("!");
Folgendes geht nicht:
let mut s = String::from("Hallo");
let r1 = &s;
let r2 = &mut s; // FEHLER: kann nicht mut borrowen, wenn r1 noch aktiv
println!("{}, {}", r1, r2);
Das verhindert ganze Kategorien von Concurrency-Bugs.
Dangling References - abgefangen
Rust verhindert, dass du eine Reference auf einen nicht mehr existierenden Wert hast:
fn dangle() -> &String { // FEHLER
let s = String::from("x");
&s
} // s wird hier freigegeben - aber die Reference wuerde weiterleben
Der Compiler erlaubt das schlicht nicht.
Praktisches Beispiel
fn gross_machen(text: &mut String) {
*text = text.to_uppercase();
}
fn zaehlen(text: &String) -> usize {
text.chars().count()
}
fn main() {
let mut name = String::from("rust");
println!("Laenge: {}", zaehlen(&name)); // 4 - nur lesen
gross_machen(&mut name); // schreiben
println!("{}", name); // "RUST"
}
Was du mitnehmen solltest
- Jeder Wert hat einen Owner, und davon gibt es nur einen
Move: Ownership wird uebertragen, die alte Variable ist ungueltigCopy: einfache Typen werden kopiert (Stack)&T: Ausleihen zum Lesen - beliebig viele gleichzeitig&mut T: Ausleihen zum Schreiben - exklusiv- Der Compiler erzwingt diese Regeln zur Kompilierzeit
Ja, das ist viel. Ja, der Compiler wird dich am Anfang nerven. Aber: Wenn dein Rust-Code kompiliert, laeuft er meistens einfach. Das ist der Deal - und er ist gut.
Im naechsten Kapitel schauen wir uns Structs, Enums und match genauer an.