Intelligente Zeiger wie normale Referenzen behandeln mit dem Merkmal (trait) Deref
Durch die Implementierung des Merkmals Deref
kann man das Verhalten des
Dereferenzierungsoperators (dereference operator) *
(nicht zu verwechseln
mit dem Multiplikations- oder Stern-Operator (glob operator)) anpassen. Indem
du Deref
so implementierst, dass ein intelligenter Zeiger wie eine reguläre
Referenz behandelt werden kann, kannst du Programmcode schreiben, der mit
Referenzen arbeitet, und diesen Programmcode auch mit intelligenten Zeigern
verwenden.
Schauen wir uns zunächst an, wie der Dereferenzierungsoperator mit regulären
Referenzen arbeitet. Dann werden wir versuchen, einen benutzerdefinierten Typ
zu definieren, der sich wie Box<T>
verhält, und herausfinden, warum der
Dereferenzierungsoperator nicht wie eine Referenz für unseren neu definierten
Typ funktioniert. Wir werden untersuchen, wie die Implementierung des Merkmals
Deref
es intelligenten Zeigern ermöglicht, auf ähnliche Weise wie Referenzen
zu funktionieren, dann sehen wir uns an wie wir mit Rusts automatischer
Umwandlung (deref coercion) mit Referenzen oder intelligenten Zeigern arbeiten
können.
Hinweis: Es gibt einen großen Unterschied zwischen dem Typ
MyBox<T>
, den wir gerade erstellen, und dem echten TypBox<T>
: Unsere Version speichert ihre Daten nicht auf dem Haldenspeicher (heap). In diesem Beispiel konzentrieren wir uns aufDeref
, daher ist es weniger wichtig, wo die Daten tatsächlich gespeichert sind als das zeigerähnliche Verhalten.
Dem Zeiger zum Wert folgen
Eine reguläre Referenz ist eine Art Zeiger, und eine Möglichkeit, sich einen
Zeiger als Pfeil vorzustellen, der auf einen Wert zeigt, der an einer anderen
Stelle gespeichert ist. In Codeblock 15-6 erstellen wir eine Referenz auf einen
i32
-Wert und verwenden dann den Dereferenzierungsoperator, um der Referenz
zum Wert zu folgen:
Dateiname: src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
Codeblock 15-6: Einen Dereferenzierungsoperator verwenden
um einer Referenz auf einen i32
-Wert zu folgen
Die Variable x
enthält den i32
-Wert 5
. Wir weisen y
eine Referenz auf
x
zu. Wir können sicherstellen, dass x
gleich 5
ist. Wenn wir jedoch eine
Aussage über den Wert y
machen möchten, müssen wir *y
verwenden, um der
Referenz auf den Wert zu folgen, auf den sie zeigt (daher Dereferenzierung).
Sobald wir y
dereferenzieren, haben wir Zugriff auf den Zahlenwert, auf den
y
zeigt, und können ihn mit 5
vergleichen.
Wenn wir stattdessen versuchen würden, assert_eq!(5, y);
zu schreiben, würden
wir diesen Fehler beim Kompilieren erhalten:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider dereferencing here
--> file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/macros/mod.rs:46:35
|
46| if !(*left_val == **right_val) {
| +
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Das Vergleichen einer Zahl mit einer Referenz auf eine Zahl ist nicht zulässig, da es sich um verschiedene Typen handelt. Wir müssen den Dereferenzierungsoperator verwenden um der Referenz auf den Wert zu folgen, auf den sie zeigt.
Box<T>
wie eine Referenz verwenden
Wir können den Programmcode in Codeblock 15-6 neu schreiben, um anstelle einer
Referenz Box<T>
zu verwenden. Wie Codeblock 15-7 zeigt, funktioniert der
Dereferenzierungsoperator:
Dateiname: src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Codeblock 15-7: Using the dereference operator on a
Box<i32>
Der Hauptunterschied zwischen Codeblock 15-7 und 15-6 besteht darin, dass wir
hier y
als Instanz einer Box<T>
festlegen, das auf einen kopierten Wert von
x
zeigt, und nicht als Referenz, die auf den Wert x
zeigt. In der letzten
Zusicherung (assertion) können wir den Dereferenzierungsoperator verwenden um
dem Zeiger in Box<T>
auf die gleiche Weise zu folgen, wie wir es getan haben,
als y
eine Referenz war. Als Nächstes werden wir ergründen, was das Besondere
an Box<T>
ist, wodurch der Dereferenzierungsoperator verwendet werden kann,
indem wir unseren eigenen Box-Typ definieren.
Einen eigenen intelligenten Zeiger definieren
Erstellen wir einen intelligenten Zeiger, der dem von der Standardbibliothek
bereitgestellten Typ Box<T>
ähnelt, um zu erfahren, wie sich intelligente
Zeiger standardmäßig anders als Referenzen verhalten. Anschließend sehen wir
uns an, wie man die Möglichkeit zur Verwendung des Dereferenzierungsoperators
hinzufügen kann.
Der Typ Box<T>
wird letztendlich als Tupel-Struktur (tuple struct) mit einem
Element definiert. Codeblock 15-8 definiert den Typ MyBox<T>
auf die gleiche
Weise. Wir werden auch eine Funktion new
definieren, analog zu Box<T>
.
Dateiname: src/main.rs
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
Codeblock 15-8: Definition des Type MyBox<T>
Wir definieren eine Struktur mit dem Namen MyBox
und deklarieren einen
generischen Parameter T
, da unser Typ Werte jedes beliebigen Typs enthalten
können soll. Der Typ MyBox
ist eine Tupelstruktur mit einem Element vom Typ
T
. Die Funktion MyBox::new
verwendet einen Parameter vom Typ T
und gibt
eine MyBox
-Instanz zurück, die den übergebenen Wert enthält.
Versuchen wir, die Funktion main
in Codeblock 15-7 zu Codeblock 15-8
hinzuzufügen und sie so zu ändern, dass der von uns definierte Typ MyBox<T>
anstelle von Box<T>
verwendet wird. Der Programmcode in Codeblock 15-9 wird
nicht kompilieren, da Rust nicht weiß, wie er MyBox
dereferenzieren kann.
Dateiname: src/main.rs
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Codeblock 15-9: Versuch, MyBox<T>
auf die gleiche Weise
wie Box<T>
und Referenzen zu benutzen
Hier ist der Kompilierfehler den wir erhalten:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Unser Typ MyBox<T>
kann nicht dereferenziert werden, da wir diese Fähigkeit
für unseren Typ nicht implementiert haben. Um eine Dereferenzierung mit dem
Operator *
zu ermöglichen, implementieren wir das Merkmal Deref
.
Implementieren des Merkmals Deref
Wie in „Ein Merkmal für einen Typ implementieren“ in Kapitel 10
beschrieben, müssen wir zur Implementierung eines Merkmals Implementierungen
für die erforderlichen Methoden des Merkmals bereitstellen. Das von der
Standardbibliothek bereitgestellte Merkmal Deref
erfordert die
Implementierung einer Methode namens deref
, die self
ausleiht (borrow) und
eine Referenz auf die beinhalteten Daten zurückgibt. Codeblock 15-10 enthält
eine Implementierung von Deref
, um die Definition von MyBox
zu ergänzen:
Dateiname: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Codeblock 15-10: Deref
auf MyBox<T>
implementieren
Die Syntax type Target = T;
definiert einen assoziierten Typ, den das Merkmal
Deref
verwenden soll. Assoziierte Typen sind eine andere Art, einen
generischen Parameter zu deklarieren, aber darüber musst du dir vorerst noch
keine Gedanken machen; in Kapitel 20 werden wir sie ausführlicher behandeln.
Wir füllen den Rumpf der Methode deref
mit &self.0
, damit deref
eine
Referenz auf den Wert zurückgibt, auf den wir mit dem Operator *
zugreifen
wollen. Erinnere dich an „Verwenden von Tupel-Strukturen ohne benannte Felder
um verschiedene Typen zu erzeugen“ in Kapitel 5, wo .0
auf
den ersten Wert in einer Tupel-Struktur zugreift. Die Funktion main
in
Codeblock 15-9, die *
für den Wert MyBox<T>
aufruft, kompiliert nun und die
Zusicherungen werden erfüllt!
Ohne das Merkmal Deref
kann der Compiler nur &
-Referenzen dereferenzieren.
Die Methode deref
gibt dem Compiler die Möglichkeit, einen Wert eines
beliebigen Typs zu verwenden, der Deref
implementiert, und die Methode
deref
aufzurufen, um eine &
-Referenz zu erhalten, die er dereferenzieren
kann.
Als wir in Codeblock 15-9 *y
eingegeben haben, hat Rust hinter den Kulissen
tatsächlich diesen Programmcode ausgeführt:
*(y.deref())
Rust ersetzt den Operator *
durch einen Aufruf der Methode deref
und dann
durch eine einfache Dereferenzierung, sodass wir nicht darüber nachdenken
müssen, ob wir die Methode deref
aufrufen müssen oder nicht. Mit dieser
Rust-Funktionalität können wir Code schreiben, der unabhängig davon
funktioniert, ob wir eine reguläre Referenz haben oder einen Typ, der Deref
implementiert.
Der Grund, warum die Methode deref
eine Referenz auf einen Wert zurückgibt
und die einfache Dereferenzierung außerhalb der Klammern in *(y.deref())
weiterhin erforderlich ist, hat mit der Eigentümerschaft (ownership) zu tun.
Wenn die Methode deref
den Wert direkt anstelle einer Referenz auf den Wert
zurückgibt, wird der Wert aus self
herausverschoben. Wenn wir den
Dereferenzierungsoperator verwenden, wollen wir meistens, wie auch hier, nicht
die Eigentümerschaft des inneren Wertes von MyBox<T>
übernehmen.
Beachte, dass der *
-Operator durch einen Aufruf der Methode deref
und dann
einem Aufruf des *
-Operators ersetzt wird. Da die Ersetzung des *
-Operators
nicht unendlich rekursiv ist, erhalten wir Daten vom Typ i32
, die mit der 5
in assert_eq!
in Codeblock 15-9 übereinstimmen.
Implizite automatische Umwandlung mit Funktionen und Methoden
Die automatische Umwandlung (deref coercion) wandelt eine Referenz auf einen
Typ, der das Merkmal Deref
implementiert, in eine Referenz auf einen anderen
Typ um. Zum Beispiel kann die automatische Umwandlung &String
in &str
konvertieren, da String
das Merkmal Deref
implementiert, sodass &str
zurückgegeben wird. Die automatische Umwandlung ist eine Bequemlichkeit, die
Rust auf Argumente für Funktionen und Methoden anwendet, und funktioniert nur
bei Typen, die das Merkmal Deref
implementieren. Die automatische Umwandlung
erfolgt automatisch, wenn wir eine Referenz auf den Wert eines bestimmten Typs
als Argument an eine Funktion oder Methode übergeben, die nicht dem
Parametertyp in der Funktion oder Methodendefinition übereinstimmt. Eine Folge
von Aufrufen der Methode deref
konvertiert den von uns angegebenen Typ in den
Typ, den der Parameter benötigt.
Rust wurde um die automatische Umwandlung erweitert, damit Programmierer, die
Funktions- und Methodenaufrufe schreiben, nicht so viele explizite
Referenzierungen und Dereferenzierungen mit &
und *
angeben müssen. Mit der
Funktionalität der automatischen Umwandlung können wir auch Programmcode
schreiben, der sowohl für Referenzen als auch für intelligente Zeiger geeignet
ist.
Um die automatische Umwandlung in Aktion zu sehen, verwenden wir den in
Codeblock 15-8 definierten Typ MyBox<T>
sowie die Implementierung von
Deref
, die wir in Codeblock 15-10 hinzugefügt haben. Codeblock 15-11 zeigt
die Definition einer Funktion mit einen Zeichenketten-Anteilstyp (string slice)
Parameter.
Dateiname: src/main.rs
fn hello(name: &str) { println!("Hallo {name}!"); } fn main() {}
Codeblock 15-11: Eine Funktion hello
mit dem Parameter
name
vom Typ &str
Wir können die Funktion hello
mit einem Zeichenketten-Anteilstyp als Argument
aufrufen, wie zum Beispiel hello("Rust");
. Die automatischer Umwandlung
ermöglicht es, hello
mit einer Referenz auf einen Wert vom Typ
MyBox<String>
aufzurufen, wie Codeblock 15-12 zeigt.
Dateiname: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hallo {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
Codeblock 15-12: hello
mit einer Referenz auf einen
MyBox<String>
-Wert, der aufgrund automatischer Umwandlung funktioniert
Hier rufen wir die Funktion hello
mit dem Argument &m
auf, das auf einen
MyBox<String>
-Wert referenziert. Da wir in Codeblock 15-10 das Merkmal
Deref
für MyBox<T>
implementiert haben, kann Rust &MyBox<String>
durch
Aufrufen von deref
in &String
verwandeln. Die Standardbibliothek bietet
eine Implementierung von Deref
auf String
, die einen
Zeichenketten-Anteilstyp zurückgibt. Dies kann man in der API-Dokumentation für
Deref
nachlesen. Rust ruft erneut deref
auf, um &String
in &str
umzuwandeln, was der Definition der Funktion hello
entspricht.
Wenn Rust keine automatische Umwandlung implementiert hätte, müssten wir den
Programmcode in Codeblock 15-13 anstelle des Programmcodes in 15-12 schreiben,
um hello
mit einem Wert vom Typ &MyBox<String>
aufzurufen.
Dateiname: src/main.rs
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hallo {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
Codeblock 15-13: Programmcode den wir schreiben müssten wenn Rust keine automatische Umwandlung hätte
Das (*m)
dereferenziert Mybox<String>
zu einem String
. Dann nehmen &
und [..]
einen Anteilstyp des String
, der gleich der gesamten Zeichenkette
ist, um der Signatur von hello
zu entsprechen. Der Programmcode ohne
automatische Umwandlung ist mit all den Symbolen schwerer zu lesen, zu
schreiben und zu verstehen. Durch die automatische Umwandlung kann Rust diese
Konvertierung automatisch für uns durchführen.
Wenn das Merkmal Deref
für die beteiligten Typen definiert ist, analysiert
Rust die Typen und verwendet Deref::deref
so oft wie nötig, um eine Referenz
zu erhalten, die dem Typ des Parameters entspricht. Wie oft Deref::deref
eingefügt werden muss, wird zur Kompilierzeit ermittelt, sodass zur Laufzeit
kein Nachteil durch die Nutzung der automatischen Umwandlung entsteht!
Wie die automatische Umwandlung mit Veränderbarkeit umgeht
Ähnlich wie du das Merkmal Deref
verwendest, um den *
-Operator bei
unveränderbaren Referenzen zu überschreiben, kannst du das Merkmal DerefMut
verwenden, um den *
-Operator bei veränderbaren Referenzen zu überschreiben.
Rust wendet die automatische Umwandlung bei Typen und Merkmalsimplementierungen in folgenden drei Fällen an:
- Von
&T
zu&U
, wennT: Deref<Target=U>
- Von
&mutT
zu&mutU
, wennT: DerefMut<Target=U>
- Von
&mutT
zu&U
, wennT: Deref<Target=U>
Die ersten beiden Fälle sind identisch, mit der Ausnahme, dass der zweite die
Veränderbarkeit implementiert. Der erste Fall besagt, dass wenn man einen &T
hat und T
Deref
für einen Typ U
implementiert hat, man transparent &U
erhalten kann. Der zweite Fall besagt, dass die gleiche automatische Umwandlung
bei veränderbaren Referenzen erfolgt.
Der dritte Fall ist schwieriger: Rust wird auch eine veränderbare Referenz in eine unveränderbare umwandeln. Das Gegenteil ist jedoch nicht möglich: Unveränderbare Referenzen werden niemals zu veränderbaren gemacht. Wenn man eine veränderbare Referenz hat, muss diese veränderbare Referenz aufgrund der Ausleihregeln (borrowing rules) die einzige Referenz auf diese Daten sein (anderenfalls würde das Programm nicht kompilieren). Das Konvertieren einer veränderbaren Referenz in eine unveränderbare verstößt niemals gegen die Ausleihregeln. Das Konvertieren einer unveränderbaren Referenz in eine veränderbare Referenz, würde erfordern, dass die ursprüngliche unveränderbare Referenz die einzige unveränderbare Referenz auf diese Daten ist, aber die Ausleihregeln garantieren dies nicht. Daher kann Rust nicht davon ausgehen, dass die Konvertierung einer unveränderbaren Referenz in eine veränderbare Referenz möglich ist.