Besitz

Dieser Guide ist einer von dreien, der Rusts Ownership-System. präsentiert. Dies ist eines von Rusts einzigartigen und verlockenden Features mit denen Rust Entwickler vertraut sein sollten. Durch Ownership [engl.: Besitz] erreicht Rust sein größtes Ziel, die Speichersicherheit. Es gibt ein paar verschiedene Konzepte, jedes mit seinem eigenen Kapitel:

  • Besitz, das was du gerade liest.
  • Ausleihen, und das assozierte Feature ‘Referenzen’
  • Lebensdauer, ein fortgeschrittenes Konzept des Ausleihens.

Diese drei Kapitel sind verwandt und deswegen in dieser Reihenfolge zu lesen. Du wirst alle drei benötigen um das Ownership-System vollständig zu verstehen.

Meta

Bevor wir in die Details gehen gibt es zwei wichtige Hinweise über das Ownership-System.

Rust hat einen Fokus auf Sicherheit und Geschwindigkeit. Es erfüllt diese Ziele durch viele "kostenfreie Abstraktionen" [‘zero-cost abstractions’], was bedeutet, dass in Rust die Kosten so niedrig wie möglich sind um diese Abstraktionen funktionieren zu lassen. Jegliche Analyse über die wie in diesem Guide sprechen wird zur Kompilierzeit ausgeführt. Du zahlst für diese Features keine Extrakosten zur Laufzeit.

Jedoch hat dieses System einen gewissen Preis: Die Lernkurve. Viele neue Rust Nutzer erleben etwas, was wir "mit dem borrow checker kämpfen" nennen, wobei dann Rust verweigert ein Programm zu kompilieren, bei dem der Author denkt, dass es korrekt ist. Das passiert häufig, da das mentale Modell des Programmierers von Ownership nicht den eigentlichen Regeln entspricht, die Rust implementiert. Du wirst wahrscheinlich zuerst etwas ähnliches erleben. Die guten Nachricht ist aber: Erfahrenere Rust Entwickler berichten, dass, sobald sie eine Zeit mit den Regeln des Ownership-Systems gearbeitet haben, sie immer weniger mit dem borrow checker kämpfen müssen.

Mit diesem Wissen, lass uns über Besitz lernen.

Besitz

Variablenbindungen haben eine bestimmte Eigenschaft in Rust: Sie ‘besitzen’ das woran sie gebunden sind. Das bedeutet, dass Rust die gebundene Ressource freigibt, wenn eine Bindung den Scope verlässt. Zum Beispiel:

# #![allow(unused_variables)]
#fn main() {
fn foo() {
    let v = vec![1, 2, 3];
}

#}

Wenn v in den Scope eingeführt wird, dann wird ein neuer Vec<T> erzeugt. In diesem Fall alloziert der Vektor auch Speicher auf dem Heap für die ersten drei Elemente. Wenn v dann am Ende von foo den Scope verlässt, räumt Rust alles was mit dem Vektor zu tun hat auf, sogar den auf dem Heap allozierten Speicher. Dies passiert deterministisch am Ende des Scopes.

Move Semantik

Es gibt jedoch noch ein paar mehr Feinheiten hier: Rust stellt sicher, dass es genau eine Bindung an eine bestimmte Ressource gibt. Zum Beispiel, wenn wir einen Vektor haben, können wir ihn einer anderen Bindung zuweisen:

# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3];

let v2 = v;

#}

Aber, wenn wir versuchen v danach zu verwenden, bekommen wir einen Fehler:

# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3];

let v2 = v;

println!("v[0] is: {}", v[0]);

#}

Der Fehler sieht so aus:

error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
                        ^

Etwas ähnliches passiert, wenn wir eine Funktion definieren, welche etwas in Besitz nimmt und dann versuchen etwas zu verwenden, nachdem wir es ihr als Argument übergeben haben:

# #![allow(unused_variables)]
#fn main() {
fn take(v: Vec<i32>) {
    // what happens here isn’t important.
}

let v = vec![1, 2, 3];

take(v);

println!("v[0] is: {}", v[0]);

#}

Der gleiche Fehler: ‘use of moved value’. Wenn wir den Besitz an etwas übergeben, dann sagen wir, dass wir die Sache "bewegt" [moved] haben. Man braucht hier keine besondere Annotation, Rust macht das einfach standardmäßig.

Die Details

Der Grund warum die Bindung nach einem move nicht verwenden können ist subtil, aber sehr wichtig. Wenn wir solchen Code schreiben:

# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3];

let v2 = v;

#}

Die erste Zeile alloziert Speicher für das Vektor-Objekt v und für die Daten, die es enthält. Das Vektor-Objekt wird auf dem Stack gespeichert und enthält einen Zeiger auf den Inhalt ([1, 2, 3]), welcher auf dem Heap gespeichert ist. Wenn wir v zu v2 bewegen, dann wird eine Kopie dieses Zeigers für v2 erstellt. Das bedeutet, dass es zwei Zeiger gibt, die auf den Inhalt des Vektors auf dem Heap zeigen. Es würde Rusts Sicherheitsgarantien verletzen indem es ein data race ermöglicht. Deswegen verbietet Rust es v zu benutzen, nachdem wir es bewegt haben.

Es ist auch wichtig zu erwähnen, dass Optimierungen die tatsächliche Kopie der Bytes auf dem Stack entfernen können, je nach den Umständen. Also ist ein move nicht so ineffizient wie er zuerst scheint.

Copy Typen

Wir haben etabliert, dass, wenn Besitz an eine andere Bindung übertragen wird, man die Originalbindung nicht mehr verwenden lassen. Es gibt jedoch ein Trait namens Copy der dieses Verhalten ändert. Wir haben über Traits noch nicht diskutiert, aber fürs erste kannst du sie dir als eine Art Annotation eines bestimmten Types vorstellen, welche zusätzliches Verhalten hinzufügt. Zum Beispiel:

# #![allow(unused_variables)]
#fn main() {
let v = 1;

let v2 = v;

println!("v is: {}", v);

#}

In diesem Fall ist v ein i32, welcher den Copy Trait implementiert. Das bedeutet, dass genau wie bei einem move eine Kopie der Daten gemacht wird, wenn wir v nach v2 zuweisen. Aber anders als bei einem move, können wir v danach trotzdem verwenden. Das ist so, weil ein i32 keine Zeiger auf irgendwelche Daten woanders hat und somit eine vollständige Kopie ist.

Alle primitiven Typen implementieren den Copy Trait und ihr Besitz wird deswegen nicht bewegt wie man vermuten könnte, gemäß den ´Ownership Regeln´. Zum Beispiel kompilieren die folgenden beiden Codeschnipsel nur, weil i32 und bool den Copy Trait implementieren.

fn main() {
    let a = 5;

    let _y = double(a);
    println!("{}", a);
}

fn double(x: i32) -> i32 {
    x * 2
}
fn main() {
    let a = true;

    let _y = change_truth(a);
    println!("{}", a);
}

fn change_truth(x: bool) -> bool {
    !x
}

Wenn wir Typen verwendet hätten, die nicht den Copy Trait implementieren, dann würden wir einen Kompilierfehler bekommen, da wir versucht hätten einen bewegten Wert [moved value] zu verwenden.

error: use of moved value: `a`
println!("{}", a);
               ^

Wir werden im Traits Abschnitt diskutieren wie man mit seinen eigenen Typen Copy implementiert.

Mehr als Besitz

Wenn wir jedes mal den Besitz zurückgeben müssten, dann würde jede Funktion die wir schreiben so aussehen:

# #![allow(unused_variables)]
#fn main() {
fn foo(v: Vec<i32>) -> Vec<i32> {
    // do stuff with v

    // hand back ownership
    v
}

#}

Das würde sehr lästig werden. Es würde umso schlimmer werden je mehr Sachen wir in Besitz nehmen wollen:

# #![allow(unused_variables)]
#fn main() {
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);

#}

Bäh! Der Rückgabetyp, die Return-Zeile und der Funktionsaufruf sind viel zu kompliziert.

Glücklicherweise bietet uns Rust ein Feature namens "Borrowing" [engl.: Ausleihen], welches uns hilft dieses Problem zu lösen. Das ist das Thema des nächsten Abschnitts!