Zum Inhalt springen
Rust Anfรคnger 30 min

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.

Aktualisiert:
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

  1. Jeder Wert hat genau einen Owner (Besitzer).
  2. Es gibt zu jedem Zeitpunkt nur einen Owner.
  3. 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?

  • String besitzt Speicher auf dem Heap.
  • Eine einfache Kopie ist gefaehrlich - wer wuerde den Speicher freigeben?
  • Also moved Rust die Ownership von s1 zu s2. s1 ist 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:

  1. Du kannst beliebig viele unveraenderliche Borrows (&T) gleichzeitig haben, oder
  2. genau einen veraenderlichen Borrow (&mut T) - aber nicht beides gleichzeitig.
  3. 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 ungueltig
  • Copy: 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.

Zurรผck zum Rust Kurs