Referenzen und Ausleihen (borrowing)
Das Problem mit dem Tupelcode in Codeblock 4-5 ist, dass wir der aufrufenden
Funktion den String
zurückgeben müssen, damit wir den String
nach dem
Aufruf von calculate_length
weiter verwenden können, weil der String
in
calculate_length
verschoben wurde. Stattdessen können wir eine Referenz auf
den String
-Wert angeben. Eine Referenz ist wie ein Zeiger, d.h. eine
Adresse, der wir folgen können, um auf Daten zugreifen zu können, die an dieser
Adresse gespeichert sind und einer anderen Variablen gehören. Im Gegensatz zu
einem Zeiger ist bei einer Referenz garantiert, dass sie auf einen gültigen
Wert eines bestimmten Typs zeigt.
Im Folgenden siehst du, wie du eine Funktion calculate_length
definieren und
verwenden kannst, die eine Referenz auf ein Objekt als Parameter hat, anstatt
die Eigentümerschaft (ownership) des Wertes zu übernehmen:
Dateiname: src/main.rs
fn main() { let s1 = String::from("Hallo"); let len = calculate_length(&s1); println!("Die Länge von '{s1}' ist {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
Beachte, dass der gesamte Tupelcode aus der Variablendeklaration und dem
Rückgabewert der Funktion verschwunden ist. Beachte des Weiteren, dass wir
&s1
an calculate_length
übergeben und in seiner Definition &String
statt
String
steht. Das &
-Zeichen steht für eine Referenz, und sie ermöglicht
es dir, sich auf einen Wert zu beziehen, ohne dessen Eigentümerschaft zu
übernehmen. Abbildung 4-6 zeigt die Speicherdarstellung.
Anmerkung: Das Gegenteil der Referenzierung durch
&
ist die Dereferenzierung (dereferencing), die mittels Dereferenzoperator*
erfolgt. Wir werden in Kapitel 8 einige Verwendungen des Dereferenzoperators sehen und in Kapitel 15 Einzelheiten der Dereferenzierung besprechen.
Schauen wir uns den Funktionsaufruf hier genauer an:
fn main() { let s1 = String::from("Hallo"); let len = calculate_length(&s1); println!("Die Länge von '{}' ist {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
Die Syntax &s1
erlaubt es uns, eine Referenz zu erstellen, die auf den Wert
von s1
referenziert, ihn aber nicht besitzt. Da sie diesen nicht besitzt,
verfällt der Wert, auf den sie verweist, nicht, wenn die Referenz nicht mehr
benutzt wird.
Ebenso verwendet die Signatur der Funktion das Zeichen &
, um anzuzeigen, dass
der Typ des Parameters s
eine Referenz ist. Lass uns einige erklärende
Anmerkungen ergänzen:
fn main() { let s1 = String::from("Hallo"); let len = calculate_length(&s1); println!("Die Länge von '{}' ist {}.", s1, len); } fn calculate_length(s: &String) -> usize { // s ist eine Referenz // auf eine Zeichenkette s.len() } // Hier verlässt s den Gültigkeitsbereich. Aber weil es keine // Eigentümerschaft an dem hat, worauf es referenziert, passiert nichts.
Der Gültigkeitsbereich, in dem die Variable s
gültig ist, ist derselbe wie
der Gültigkeitsbereich eines Funktionsparameters, aber der Wert, auf den die
Referenz zeigt, wird nicht aufgeräumt, wenn s
nicht mehr verwendet wird, weil
s
keine Eigentümerschaft hat. Wenn Funktionen statt der tatsächlichen Werte
Referenzen als Parameter haben, brauchen wir die Werte nicht zurückzugeben, um
die Eigentümerschaft zurückzugeben, denn wir hatten nie die Eigentümerschaft.
Wir nennen den Vorgang des Erstellens einer Referenz Ausleihen (borrowing). Wenn eine Person im richtigen Leben etwas besitzt, kannst du es von ihr ausleihen. Wenn du fertig bist, musst du es zurückgeben. Es gehört dir nicht.
Was passiert nun, wenn wir versuchen, etwas zu verändern, das wir uns ausleihen? Versuche den Code in Codeblock 4-6. Achtung: Es funktioniert nicht!
Dateiname: src/main.rs
fn main() { let s = String::from("Hallo"); change(&s); } fn change(some_string: &String) { some_string.push_str(" Welt"); }
Hier ist die Fehlermeldung:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(" Welt");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
So wie Variablen standardmäßig unveränderbar sind, so sind auch Referenzen unveränderbar. Es ist uns nicht erlaubt, etwas zu verändern, auf das wir eine Referenz haben.
Veränderbare Referenzen
Wir können den Code aus Codeblock 4-6 so ändern, dass wir einen geliehenen Wert mit ein paar kleinen Änderungen ändern können, die stattdessen eine veränderbare Referenz verwenden:
Dateiname: src/main.rs
fn main() { let mut s = String::from("Hallo"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(" Welt"); }
Zuerst ändern wir s
, um mut
zu sein. Dann erstellen wir eine
veränderbare Referenz mit &mut s
, wo wir die Funktion change
aufrufen, und aktualisieren die Funktionssignatur, um eine veränderbare
Referenz mit some_string: &mut String
entgegenzunehmen. Dies macht deutlich,
dass die Funktion change
den Wert, den sie ausleiht, verändert.
Veränderbare Referenzen haben eine große Einschränkung: Wenn du eine
veränderbare Referenz auf einen Wert hast, kannst du keine andere Referenz auf
diesen Wert haben. Dieser Code versucht, zwei veränderbare Referenzen auf s
zu erstellen, und wird fehlschlagen:
Dateiname: src/main.rs
#![allow(unused)] fn main() { let mut s = String::from("Hallo"); let r1 = &mut s; let r2 = &mut s; println!("{r1}, {r2}"); }
Hier ist die Fehlermeldung:
$ cargo run
Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:6:10
|
5 | let r1 = &mut s;
| ------ first mutable borrow occurs here
6 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}");
| ---- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Dieser Fehler besagt, dass dieser Code ungültig ist, weil wir s
nicht mehr
als einmal zur gleichen Zeit als veränderbar ausleihen können. Die erste
veränderbare Ausleihe ist in r1
und muss beibehalten werden, bis sie in
println!
verwendet wird, aber zwischen dem Erstellen dieser veränderbaren
Referenz und ihrer Verwendung haben wir versucht, eine andere veränderbare
Referenz in r2
zu erstellen, der die gleichen Daten wie r1
ausleiht.
Die Beschränkung, die mehrere veränderbare Referenz auf dieselben Daten zur gleichen Zeit verhindert, erlaubt Veränderung, aber in einer sehr kontrollierten Weise. Das ist etwas, womit Rust-Neulinge zu kämpfen haben, denn in den meisten Sprachen kann man verändern wann immer man will. Diese Beschränkung hat den Vorteil, dass Rust Daten-Wettlaufsituation zur Kompilierzeit verhindern kann. Eine Daten-Wettlaufsituation (data race) ist ähnlich einer Wettlaufsituation (race condition) und tritt auf, wenn diese drei Verhaltensweisen auftreten:
- Zwei oder mehr Zeiger greifen gleichzeitig auf die gleichen Daten zu.
- Mindestens einer der Zeiger wird zum Schreiben auf die Daten verwendet.
- Es gibt keinen Mechanismus, um den Zugriff auf die Daten zu synchronisieren.
Daten-Wettlaufsituationen verursachen undefiniertes Verhalten und können schwierig zu diagnostizieren und zu beheben sein, wenn du versuchst, sie zur Laufzeit aufzuspüren; Rust verhindert dieses Problem, indem es Code mit Daten-Wettlaufsituationen gar nicht erst kompiliert!
Wie immer können wir geschweifte Klammern verwenden, um einen neuen Gültigkeitsbereich zu schaffen, der mehrere veränderbare Referenzen erlaubt, nur nicht gleichzeitige:
#![allow(unused)] fn main() { let mut s = String::from("Hallo"); { let r1 = &mut s; } // r1 verlässt hier den Gültigkeitsbereich, sodass wir // problemlos eine neue Referenz erstellen können. let r2 = &mut s; }
Rust erzwingt eine ähnliche Regel für die Kombination von veränderbaren und unveränderbaren Referenzen. Dieser Code führt zu einem Fehler:
#![allow(unused)] fn main() { let mut s = String::from("Hallo"); let r1 = &s; // kein Problem let r2 = &s; // kein Problem let r3 = &mut s; // GROSSES PROBLEM println!("{r1}, {r2} und {r3}"); }
Hier ist die Fehlermeldung:
$ cargo run
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:7:10
|
5 | let r1 = &s; // kein Problem
| -- immutable borrow occurs here
6 | let r2 = &s; // kein Problem
7 | let r3 = &mut s; // GROSSES PROBLEM
| ^^^^^^ mutable borrow occurs here
8 |
9 | println!("{r1}, {r2} und {r3}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Puh! Wir können auch keine veränderbaren Referenzen verwenden, solange wir eine unveränderbare Referenz auf denselben Wert haben.
Nutzer einer unveränderbaren Referenz erwarten nicht, dass sich die Werte dahinter plötzlich ändern! Mehrere unveränderbare Referenzen sind jedoch in Ordnung, da niemand, der die Daten nur liest, die Möglichkeit hat, das Lesen der Daten durch andere zu beeinflussen.
Beachte, dass der Gültigkeitsbereich einer Referenz dort beginnt, wo sie
eingeführt wird, und sich bis zur letzten Verwendung dieser Referenz fortsetzt.
Zum Beispiel kompiliert dieser Code, weil die letzte Verwendung der
unveränderbaren Referenzen in println!
vor der Einführung der veränderbaren
Referenz erfolgt:
#![allow(unused)] fn main() { let mut s = String::from("Hallo"); let r1 = &s; // kein Problem let r2 = &s; // kein Problem println!("{r1} und {r2}"); // r1 und r2 werden nach dieser Stelle nicht mehr verwendet let r3 = &mut s; // kein Problem println!("{r3}"); }
Die Gültigkeitsbereiche der unveränderbaren Referenzen r1
und r2
enden
nach dem println!
, wo sie zuletzt verwendet werden, d.h. bevor die
veränderbare Referenz r3
erstellt wird. Diese Gültigkeitsbereiche
überschneiden sich nicht, daher ist dieser Code zulässig: Der Compiler kann
erkennen, dass die Referenz bereits vor dem Ende des Gültigkeitsbereichs nicht
mehr verwendet wird.
Auch wenn Fehler durch Ausleihen manchmal frustrierend sein können, denke daran, dass es der Rust-Compiler ist, der frühzeitig (zur Kompilierzeit und nicht zur Laufzeit) auf einen möglichen Fehler hinweist und dir genau zeigt, wo das Problem liegt. Dann musst du nicht aufspüren, warum deine Daten nicht so sind, wie du dachtest.
Hängende Referenzen
In Sprachen mit Zeigern ist es leicht, fälschlicherweise einen hängenden Zeiger (dangling pointer) zu erzeugen, also einen Zeiger, der auf eine Stelle im Speicher verweist, die vielleicht an jemand anderem vergeben wurde, weil der Speicher freigegeben wurde, während noch ein Zeiger auf diesen Speicher bestehen bleibt. In Rust hingegen garantiert der Compiler, dass Referenzen niemals hängende Referenzen sein können: Wenn du eine Referenz auf Daten hast, stellt der Compiler sicher, dass die Daten nicht den Gültigkeitsbereich verlassen, bevor die Referenz auf die Daten dies tut.
Versuchen wir, eine hängende Referenz zu erstellen, um zu sehen wie Rust das mit einem Kompilierfehler verhindert:
Dateiname: src/main.rs
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("Hallo"); &s }
Hier ist die Fehlermeldung:
$ cargo run
Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s
| ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `playground` (bin "playground") due to 2 previous errors; 1 warning emitted
Diese Fehlermeldung bezieht sich auf eine Funktionalität, die wir noch nicht behandelt haben: Die Lebensdauer. Wir werden die Lebensdauer in Kapitel 10 im Detail besprechen. Abgesehen von den Hinweisen zur Lebensdauer enthält die Meldung den entscheidenden Hinweis, warum dieser Code nicht funktioniert:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
Lass uns einen genaueren Blick auf das werfen, was in jeder Phase unseres
dangle
-Codes geschieht:
Dateiname: src/main.rs
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // dangle gibt eine Referenz // auf eine Zeichenkette zurück let s = String::from("Hallo"); // s ist eine neue Zeichenkette &s // wir geben einen Verweis auf die Zeichenkette s zurück } // Hier verlässt s den Gültigkeitsbereich und wird verworfen. // Sein Speicherplatz wird aufgeräumt. Gefahr!
Da s
innerhalb dangle
erzeugt wird, wird s
wieder freigegeben, wenn der
Code von dangle
zu Ende ist. Aber wir haben versucht, eine Referenz darauf
zurückzugeben. Das heißt, diese Referenz würde auf einen ungültigen String
verweisen. Das ist nicht gut! Rust lässt uns das nicht tun.
Die Lösung ist, den String
direkt zurückzugeben:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("Hallo"); s }
Dies funktioniert ohne Probleme. Die Eigentümerschaft wird nach außen verschoben, und nichts wird freigegeben.
Regeln für Referenzen
Lass uns rekapitulieren, was wir über Referenzen gelernt haben:
- Zu jedem beliebigen Zeitpunkt kannst du entweder eine veränderbare Referenz oder eine beliebige Anzahl unveränderbarer Referenzen haben.
- Referenzen müssen immer gültig sein.
Als Nächstes werden wir uns mit einer anderen Art von Referenz befassen: Anteilstypen (slice).