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 Vectoren 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 Strings 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();
}