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 Typ Box<T>: Unsere Version speichert ihre Daten nicht auf dem Haldenspeicher (heap). In diesem Beispiel konzentrieren wir uns auf Deref, 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:

  1. Von &T zu &U, wenn T: Deref<Target=U>
  2. Von &mutT zu &mutU, wenn T: DerefMut<Target=U>
  3. Von &mutT zu &U, wenn T: 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.