Klonen und Verschieben

In Rust können wir durch

#![allow(unused)]
fn main() {
let s = "Hallo Welt";
}

der Variable s die Zeichenkette "Hallo Welt" zuweisen. Der Wert von s ist ein Literal, das Teil des Kompilats wird. Dieser lässt sich nicht ändern. Wenn wir den Geltungsbereich von s verlassen wird auch kein Speicher freigegeben, da der Wert von s nicht dynamisch allokiert wurde. Um in Rust mit Zeichenketten wie "Hallo Welt" zu arbeiten, die nicht zur Kompilierzeit feststehen, gibt es die Möglichkeit den Typ String zu verwenden, der von der Standardbibliothek implementiert wird. Per

#![allow(unused)]
fn main() {
let s = String::from("Hallo Welt");
}

kann man eine Zeichenkette erzeugen. Strings legen neben einem Verwaltungsoverhead wie Vecs ihre Daten hauptsächlich auf dem Heap an. Und ebenfalls wird der Speicher eines Strings aufgeräumt, wenn die Variable den Geltungsbereich verlässt. Strings können geändert werden.

#![allow(unused)]
fn main() {
let mut s = String::from("Hallo ");
s.push_str("Welt");
println!("{s}");
}

Move

Wenn eine Stringvariable einer anderen Variable zugewiesen wird, passiert ein sogenannter Move. Das heißt, der Speicher auf dem Heap bleibt erhalten und nur der verwaltende Speicher auf dem Stack ändert sich. Dazu schauen wir uns das folgende Beispiel an.

#![allow(unused)]
fn main() {
let s = String::from("Wort");
}

Nehmen wir an, die eigentlich Zeichenkette Wort befinde sich auf dem Heap an der Speicheradresse 0x9ffe4edb6a34. Auf dem Stack werden folgende Daten für s abgelegt.

| address | name               | value  |
| ------- | ------------------ | -----  |
|         |                    |        |
| 0x7060  | Heap Adresse von s | 0x9004 |
| 0x7058  | Kapazität von s    | 4      |
| 0x7050  | Länge von s        | 4      |

Nun weisen wir die Variable s der Variablen t zu.

#![allow(unused)]
fn main() {
let s = String::from("Wort");
let t = s;
println!("{t}");
}

Nun entspricht der Wert von t dem obigen Wert von s.

| address | name               | value  |
| ------- | ------------------ | -----  |
|         |                    |        |
| 0x7060  | Heap Adresse von t | 0x9004 |
| 0x7058  | Kapazität von t    | 4      |
| 0x7050  | Länge von t        | 4      |

| 0x7048  | Heap Adresse von s | ?      |
| 0x7040  | Kapazität von s    | ?      |
| 0x7038  | Länge von s        | ?      |

Die Variable s ist nicht mehr gültig. Das bedeutet, dass ein weiterer Zugriff auf s zu einem Kompilierfehler führt.

#![allow(unused)]
fn main() {
// kompiliert nicht
let s = String::from("Wort");
let t = s;
println!("{s}");
}

Auch bei Funktions- und Methodenparametern wird der Teil auf dem Stack kopiert und der Teil auf dem Heap verschoben.

#![allow(unused)]
fn main() {
// kompiliert nicht
fn f(x: String) {
    println!("x is {x}");
}
let s = String::from("Wort");
f(s);
println!("s is {s}");
}

Primitive Typen werden direkt kopiert und nicht verschoben, da sie keinen Heapspeicher belegen. Dementsprechend bleiben sie auch nach Zuweisung oder Übergabe an eine Funktion verwendbar.

#![allow(unused)]
fn main() {
fn f(x: i32) {
    println!("x is {x}");
}
let a = 23;
let b = a;
f(a);
println!("a is {a}");
println!("b is {b}");
}

Klonen von Daten auf dem Heap

Strings und auch Vecs implementieren eine Methode clone. Den obigen Schnipsel können wir entsprechend erweitern.

#![allow(unused)]
fn main() {
fn f(x: String) {
    println!("x is {x}");
}
let s = String::from("Wort");

let t = s.clone();
//           |
// erstellt eine Kopie

f(t);
println!("s is {s}");
}

Die Daten des Strings s auf dem Heap werden kopiert und von s2 verwendet. Die Methode clone kann also zu signifikantem Ressourcenverbrauch führen.

Übrigens verfügen alle primitiven Typen ebenfalls über die Methode clone. Es ist üblicherweise nicht nötig, diese aufzurufen, da entsprechende Variablen eh kopiert und nicht verschoben werden.

Methodenaufrufe

Betrachten wir nochmal das Beispiel aus dem vorherigen Kapitel.

#![allow(unused)]
fn main() {
// kompiliert nicht
struct Point {
    x: f64,
    y: f64
}
impl Point {
    fn length(self) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
let v = Point{x: 0.1, y: 0.2};
let length = v.length();
let l_again = v.length();
}

Dieses Beispiel kompiliert nicht, weil wir bei Methoden mit self-Paremeter die Instanz durch den Funktionsaufruf verschieben. Die Methode übernimmt Ownership über die Instanz. Denn self ist nur eine Kurzform. Wir können äquivalent folgende Signatur verwenden.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
    z: f64
}
impl Point {
    fn length(self: Point) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
let v = Point{x: 0.1, y: 0.2};
let length = v.length();
println!("{length}");
}

Wir können v.length() also kein zweites Mal aufrufen, da die Instanz verschoben wurde. Es sollte jedoch kein Problem sein einen Point zu kopieren, da alle Felder einfach kopiert werden können. In solchen Fällen können Verhaltensweiten von Typen von ihren Felder abgeleitet werden. Dazu versehen wir unseren Typen mit dem Attribut derive und spezifizieren, welche Verhaltensweisen von den Feldern abgeleitet werden sollen. In diesem Fall wird durch Verwendung von

#[derive(Copy, Clone)]

unser Typ kopierbar und implementiert ohne unser Zutun die Methode clone.

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
    z: f64
}
impl Point {
    fn length(self: Point) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
let v = Point{x: 0.1, y: 0.2};
let length = v.length();
println!("{length}");
}

Uns werden noch weitere Verhaltensweisen begegnen. Diese Verhaltensweisen werden in Rust Traits genannt.