Borrowing
Da die Daten auf dem Heap bei einer Moveoperation erhalten bleiben und der kleine Verwaltungsteil
auf dem Stack kopiert wird, ist das Übergeben von Variablen an Funktionen effizient. Nun kann es
aber passieren, dass eine Variable auch nachdem sie von der Funktion verarbeitet wurde,
trotzdem noch verwendet werden möchte. Dazu kann sich eine Funktion eine Variable ausleihen. In
der Signatur der Funktion tritt in diesem Fall ein &
vor den Typen der Variablen. Beim Aufruf
der Funktion zeigt ein weiteres &
an, dass die Variable ausgeliehen wird (engl. borrowing).
#![allow(unused)] fn main() { fn f(x: &String) { println!("{x}"); } let s = String::from("Wort"); f(&s); println!("{s}"); }
Typen, die mit einem &
beginnen, werden auch Referenzen genannt. Denn eine Variable x
vom Typ &String
ist
eine Referenz auf den String
, den sie sich geliehen hat. Beachte, dass x
nicht der Owner des entsprechenden
Wertes ist, sondern lediglich darauf verweist. Insbesondere folgt daraus, dass das Verlassen des Scopes von x
nicht dazu führt, dass der Wert auf den x
verweist, aufgeräumt wird.
Man kann auch eine ausgeliehene Variable ändern, indem man mut &
dem Typ voranstellt. Typen, die mit mut &
beginnen,
heißen veränderliche Referenz (engl. mutable reference).
#![allow(unused)] fn main() { fn f(x: &mut String) { x.push_str("e"); } let mut s = String::from("Wort"); f(&mut s); println!("{s}"); }
Man kann veränderlichen Referenzen neue Werte zuweisen, indem man sie mit *
derefenziert.
#![allow(unused)] fn main() { fn f(x: &mut String) { *x = String::from("Worte"); } let mut s = String::from("Wort"); f(&mut s); println!("{s}"); }
Im Gegensatz zu C++ kann man in Rust zeitgleich immer nur eine veränderliche Referenz auf einen Wert haben. Sogar eine weitere nicht veränderliche Referenz ist nicht möglich. Die veränderliche Referenz könnte zu Änderung des unterliegenden Speichers führen.
#![allow(unused)] fn main() { // kompiliert nicht let mut s = String::from("Wort"); let x = &mut s; let y = &mut s; println!("{x}, {y}"); }
Dadurch werden bereits zur Kompilierzeit sogenannte Data Races verhindert. Data Races entstehen wenn in verschiedenen parallel laufenden Programmteilen zur gleichen Zeit die gleiche Variable verändert wird. Selbst wenn eine Referenz nicht veränderlich ist, besteht das gleiche Problem. Und auch das ist in Rust nicht möglich. Des Weiteren ist ein Move unmöglich, solange eine Referenz auf einen Wert existiert.
#![allow(unused)] fn main() { // kompiliert nicht let mut s = String::from("Wort"); let x = &s; let y = &mut s; println!("{x}, {y}"); }
Zwei Referenzen, die beide nicht veränderlich sind, stellen dagegen kein Problem dar. Denn wenn zwei Programmteile gleichzeitig den gleichen Wert auslesen aber nicht schreiben, ist das völlig unproblematisch.
#![allow(unused)] fn main() { let mut s = String::from("Wort"); let x = &s; let y = &s; println!("{x}, {y}"); }
Einer der Marketingslogans der Programmiersprache Rust lautet Fearless Concurrency, zu deutsch angstfreie Nebenläufigkeit. Die oben beschriebenen Restriktionen von Referenzen sind ein Baustein dieses Konzepts.
Auch Methoden können &self
als Parameter verlangen. Wir betrachten nochmal unser Vektorbeispiel.
#![allow(unused)] fn main() { struct Point { x: f64, y: f64 } impl Point { fn squared_dist_to_0(&self) -> f64 { self.x * self.x + self.y * self.y } } let p = Point{x: 0.1, y: 0.2}; let dist = v.squared_dist_to_0(); let dist = v.squared_dist_to_0(); println!("{dist}"); }
Ein mehrfacher Aufruf der Methode ist kein Problem mehr, da die Methode nicht Ownership ihrer Instanz übernommen hat sondern sich ihre Instanz nur geliehen hat.
Slices
Manchmal ist man nur an einem Teil einer Zeichenkette interessiert. Da Zeichenketten wie Vec
toren und Àrrays
zusammenhängenden Speicher verwalten, ist es Möglich, druch die &[..]
-Syntax Referenzen auf Teilbereiche
des Speichers zu verwenden.
#![allow(unused)] fn main() { let s = String::from("Hallo Welt."); let s_slice = &s[6..10]; println!("{s_slice}"); }
Das Slice &s[6..10]
verweist auf alle Zeichen von inklusive Index 6 bis exklusive Index 10.
Mit ..=
kann man auch die größere Grenze mitnehmen.
#![allow(unused)] fn main() { let s = String::from("Hallo Welt."); let s_slice = &s[6..=10]; println!("{s_slice}"); }
Auch können Grenzen komplett weggelassen werden.
Dadurch wird &s[..10]
zu Hallo Welt
und &s[6..]
zu Welt.
.
#![allow(unused)] fn main() { let s = String::from("Hallo Welt."); println!("{}", &s[..10]); println!("{}", &s[6..]); }
Eine Referenz auf einen Slice eines String
s hat in Rust den Typ &str
. Wenn wir nun eine Funktion
verwenden, deren Argumente vom Slicetyp &str
sind, können wir auch &String
übergeben. Diese werden automatisch
als &s[..]
aufgefasst.
#![allow(unused)] fn main() { fn f(s: &str) { println!("{s}"); } let s = String::from("Hallo Welt."); f(&s[6..10]); f(&s); }
Eine Funktion die &String
als Parameter hat, kann keine &str
-Parameter annehmen.
Warnung
Das Zerteilen von Zeichenketten funktioniert so erstmal nur für ASCII Zeichen. Für Unicode-Zeichen ist die Situation etwas komplizierter. Das wird Gegenstand eines späteren Kapitels sein.
Slices können durch Aufruf der Funktion to_owned
kopiert werden.
#![allow(unused)] fn main() { let v = [1, 2, 3]; let w = v[1..].to_owned(); // w ist eine Instanz von Vec let s = String::from(""); let s1 = s.to_owned(); }