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. String
s legen neben einem Verwaltungsoverhead wie Vec
s ihre Daten
hauptsächlich auf dem Heap an. Und ebenfalls
wird der Speicher eines String
s 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
String
s und auch Vec
s 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.