Smart Pointer behandeln wie reguläre Referenzen

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Treating Smart Pointers Like Regular References With Deref. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir untersuchen, wie das Implementieren des Deref-Traits es ermöglicht, Smart Pointer wie reguläre Referenzen zu behandeln, und wie das Deref-Zwangseigenschaft von Rust es ermöglicht, mit Referenzen oder Smart Pointern zu arbeiten.

Smart Pointer wie reguläre Referenzen behandeln mit Deref

Das Implementieren des Deref-Traits ermöglicht es Ihnen, das Verhalten des Dereferenzierungsoperators * (nicht zu verwechseln mit dem Multiplikations- oder Glob-Operator) anzupassen. Indem Sie Deref so implementieren, dass ein Smart Pointer wie eine reguläre Referenz behandelt werden kann, können Sie Code schreiben, der auf Referenzen operiert, und diesen Code auch mit Smart Pointern verwenden.

Schauen wir uns zunächst an, wie der Dereferenzierungsoperator mit regulären Referenzen funktioniert. Dann werden wir versuchen, einen benutzerdefinierten Typ zu definieren, der wie Box<T> verhält, und sehen, warum der Dereferenzierungsoperator nicht wie eine Referenz auf unseren neu definierten Typ funktioniert. Wir werden untersuchen, wie das Implementieren des Deref-Traits es ermöglicht, dass Smart Pointer auf ähnliche Weise wie Referenzen funktionieren. Dann werden wir uns das Deref-Zwangseigenschaft von Rust und wie es uns ermöglicht, mit Referenzen oder Smart Pointern zu arbeiten, ansehen.

Hinweis: Es gibt einen großen Unterschied zwischen dem MyBox<T>-Typ, den wir im folgenden erstellen werden, und dem echten Box<T>: Unsere Version wird nicht seine Daten auf dem Heap speichern. Wir konzentrieren dieses Beispiel auf Deref, daher ist es weniger wichtig, wo die Daten tatsächlich gespeichert werden, als das pointerähnliche Verhalten.

Dem Zeiger auf den Wert folgen

Eine reguläre Referenz ist eine Art Zeiger, und eine Möglichkeit, einen Zeiger zu verstehen, ist als Pfeil auf einen an einem anderen Ort gespeicherten Wert. In Listing 15-6 erstellen wir eine Referenz auf einen i32-Wert und verwenden dann den Dereferenzierungsoperator, um dem Zeiger auf den Wert zu folgen.

Dateiname: src/main.rs

fn main() {
  1 let x = 5;
  2 let y = &x;

  3 assert_eq!(5, x);
  4 assert_eq!(5, *y);
}

Listing 15-6: Verwendung des Dereferenzierungsoperators, um einem Zeiger auf einen i32-Wert zu folgen

Die Variable x enthält einen i32-Wert 5 [1]. Wir setzen y gleich einer Referenz auf x [2]. Wir können feststellen, dass x gleich 5 ist [3]. Wenn wir jedoch eine Aussage über den Wert in y machen möchten, müssen wir *y verwenden, um dem Zeiger auf den Wert zu folgen, auf den er zeigt (d. h. dereferenzieren), damit der Compiler den tatsächlichen Wert vergleichen kann [4]. Nachdem wir y dereferenziert haben, haben wir Zugang zum ganzzahligen Wert, auf den y zeigt, den wir mit 5 vergleichen können.

Wenn wir versuchen würden, assert_eq!(5, y); zu schreiben, würden wir diesen Kompilierungsfehler erhalten:

error[E0277]: kann `{integer}` nicht mit `&{integer}` vergleichen
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ keine Implementierung für `{integer} ==
&{integer}`
  |
  = Hilfe: Das Trait `PartialEq<&{integer}>` ist für `{integer}` nicht implementiert

Das Vergleichen einer Zahl und einer Referenz auf eine Zahl ist nicht möglich, da es sich um verschiedene Typen handelt. Wir müssen den Dereferenzierungsoperator verwenden, um dem Zeiger auf den Wert zu folgen, auf den er zeigt.

Verwendung von Box<T> wie einer Referenz

Wir können den Code in Listing 15-6 umschreiben, um Box<T> statt einer Referenz zu verwenden; der Dereferenzierungsoperator, der auf der Box<T> in Listing 15-7 verwendet wird, funktioniert auf die gleiche Weise wie der Dereferenzierungsoperator, der auf der Referenz in Listing 15-6 verwendet wird.

Dateiname: src/main.rs

fn main() {
    let x = 5;
  1 let y = Box::new(x);

    assert_eq!(5, x);
  2 assert_eq!(5, *y);
}

Listing 15-7: Verwendung des Dereferenzierungsoperators auf einer Box<i32>

Der Hauptunterschied zwischen Listing 15-7 und Listing 15-6 besteht darin, dass wir hier y als eine Instanz einer Box setzen, die auf einen kopierten Wert von x zeigt, anstatt als eine Referenz, die auf den Wert von x zeigt [1]. In der letzten Prüfung [2] können wir den Dereferenzierungsoperator verwenden, um der Box auf den Zeiger zu folgen, genauso wie wir es getan haben, als y eine Referenz war. Als nächstes werden wir untersuchen, was an Box<T> besonders ist, das es uns ermöglicht, den Dereferenzierungsoperator zu verwenden, indem wir unseren eigenen Box-Typ definieren.

Definieren unseres eigenen Smart Pointers

Lassen Sie uns einen Smart Pointer ähnlich dem von der Standardbibliothek bereitgestellten Box<T>-Typ erstellen, um zu erfahren, wie Smart Pointer standardmäßig anders als Referenzen verhalten. Dann werden wir uns ansehen, wie man die Möglichkeit erhält, den Dereferenzierungsoperator zu verwenden.

Der Box<T>-Typ wird letztendlich als Tuple-Struktur mit einem Element definiert, daher definiert Listing 15-8 einen MyBox<T>-Typ auf die gleiche Weise. Wir definieren auch eine new-Funktion, um der auf Box<T> definierten new-Funktion zu entsprechen.

Dateiname: src/main.rs

 1 struct MyBox<T>(T);

impl<T> MyBox<T> {
  2 fn new(x: T) -> MyBox<T> {
      3 MyBox(x)
    }
}

Listing 15-8: Definieren eines MyBox<T>-Typs

Wir definieren eine Struktur namens MyBox und deklarieren einen generischen Parameter T [1], weil wir wollen, dass unser Typ Werte beliebiger Typen aufnimmt. Der MyBox-Typ ist eine Tuple-Struktur mit einem Element vom Typ T. Die MyBox::new-Funktion nimmt einen Parameter vom Typ T [2] und gibt eine MyBox-Instanz zurück, die den übergebenen Wert enthält [3].

Lassen Sie uns versuchen, die main-Funktion in Listing 15-7 zu Listing 15-8 hinzuzufügen und sie zu ändern, um den von uns definierten MyBox<T>-Typ anstelle von Box<T> zu verwenden. Der Code in Listing 15-9 wird nicht kompilieren, weil Rust nicht weiß, wie man MyBox dereferenziert.

Dateiname: src/main.rs

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-9: Versuch, MyBox<T> auf die gleiche Weise zu verwenden wie Referenzen und Box<T>

Hier ist der resultierende Kompilierungsfehler:

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

Unser MyBox<T>-Typ kann nicht dereferenziert werden, weil wir diese Fähigkeit für unseren Typ nicht implementiert haben. Um die Dereferenzierung mit dem *-Operator zu ermöglichen, implementieren wir das Deref-Trait.

Implementieren des Deref-Traits

Wie in "Implementing a Trait on a Type" besprochen, müssen wir für die erforderlichen Methoden eines Traits Implementierungen bereitstellen, um es zu implementieren. Das von der Standardbibliothek bereitgestellte Deref-Trait erfordert, dass wir eine Methode namens deref implementieren, die self entleiht und einen Verweis auf die innere Daten zurückgibt. Listing 15-10 enthält eine Implementierung von Deref, die zur Definition von MyBox<T> hinzugefügt werden soll.

Dateiname: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
  1 type Target = T;

    fn deref(&self) -> &Self::Target {
      2 &self.0
    }
}

Listing 15-10: Implementieren von Deref auf MyBox<T>

Die Syntax type Target = T; [1] definiert einen assoziierten Typ für das Deref-Trait, um ihn zu verwenden. Assoziierte Typen sind eine etwas andere Art, einen generischen Parameter zu deklarieren, aber Sie müssen sich hierfür vorerst nicht kümmern; wir werden sie im Kapitel 19 im Detail behandeln.

Wir füllen den Körper der deref-Methode mit &self.0 aus, sodass deref einen Verweis auf den Wert zurückgibt, auf den wir mit dem *-Operator zugreifen möchten [2]; erinnern Sie sich aus "Using Tuple Structs Without Named Fields to Create Different Types", dass .0 auf den ersten Wert in einer Tuple-Struktur zugreift. Die main-Funktion in Listing 15-9, die * auf den MyBox<T>-Wert aufruft, kompiliert jetzt, und die Assertions werden bestanden!

Ohne das Deref-Trait kann der Compiler nur &-Referenzen dereferenzieren. Die deref-Methode gibt dem Compiler die Möglichkeit, einen Wert beliebigen Typs, der Deref implementiert, zu nehmen und die deref-Methode aufzurufen, um einen &-Verweis zu erhalten, auf den er weiß, wie er ihn dereferenziert.

Als wir in Listing 15-9 *y eingegeben haben, hat Rust hinter den Kulissen tatsächlich diesen Code ausgeführt:

*(y.deref())

Rust ersetzt den *-Operator mit einem Aufruf der deref-Methode und anschließend einem einfachen Dereferenzieren, sodass wir nicht darüber nachdenken müssen, ob wir die deref-Methode aufrufen müssen oder nicht. Diese Rust-Funktion ermöglicht es uns, Code zu schreiben, der unabhängig davon funktioniert, ob wir eine reguläre Referenz oder einen Typ haben, der Deref implementiert.

Der Grund, warum die deref-Methode einen Verweis auf einen Wert zurückgibt und dass das einfache Dereferenzieren außerhalb der Klammern in *(y.deref()) immer noch erforderlich ist, hat mit dem Besitzsystem zu tun. Wenn die deref-Methode den Wert direkt statt eines Verweises auf den Wert zurückgäbe, würde der Wert aus self bewegt werden. Wir möchten in diesem Fall oder in den meisten Fällen, in denen wir den Dereferenzierungsoperator verwenden, nicht die Eigentumsgewalt über den inneren Wert in MyBox<T> ergreifen.

Beachten Sie, dass der *-Operator nur einmal mit einem Aufruf der deref-Methode und anschließend einem Aufruf des *-Operators ersetzt wird, jedes Mal, wenn wir in unserem Code einen * verwenden. Da die Substitution des *-Operators nicht endlos rekursiert, erhalten wir schließlich Daten vom Typ i32, die mit dem 5 in assert_eq! in Listing 15-9 übereinstimmen.

Implizite Deref-Zwangsumwandlungen mit Funktionen und Methoden

Deref-Zwangsumwandlung wandelt eine Referenz auf einen Typ, der das Deref-Trait implementiert, in eine Referenz auf einen anderen Typ um. Beispielsweise kann die Deref-Zwangsumwandlung &String in &str umwandeln, weil String das Deref-Trait so implementiert, dass es &str zurückgibt. Die Deref-Zwangsumwandlung ist eine Bequemlichkeit, die Rust bei Argumenten von Funktionen und Methoden vornimmt und funktioniert nur für Typen, die das Deref-Trait implementieren. Sie geschieht automatisch, wenn wir eine Referenz auf einen Wert eines bestimmten Typs als Argument an eine Funktion oder Methode übergeben, die nicht mit dem Parametertyp in der Funktions- oder Methodendefinition übereinstimmt. Eine Folge von Aufrufen der deref-Methode wandelt den von uns bereitgestellten Typ in den Typ um, den das Parameter benötigt.

Die Deref-Zwangsumwandlung wurde in Rust hinzugefügt, damit Programmierer, die Funktions- und Methodenaufrufe schreiben, nicht so viele explizite Referenzen und Dereferenzierungen mit & und * hinzufügen müssen. Die Deref-Zwangsumwandlungsfunktion ermöglicht es uns auch, mehr Code zu schreiben, der sowohl für Referenzen als auch für Smart Pointer funktioniert.

Um die Deref-Zwangsumwandlung an einem Beispiel zu sehen, verwenden wir den in Listing 15-8 definierten MyBox<T>-Typ sowie die in Listing 15-10 hinzugefügte Implementierung von Deref. Listing 15-11 zeigt die Definition einer Funktion, die einen String-Slice-Parameter hat.

Dateiname: src/main.rs

fn hello(name: &str) {
    println!("Hello, {name}!");
}

Listing 15-11: Eine hello-Funktion mit dem Parameter name vom Typ &str

Wir können die hello-Funktion mit einem String-Slice als Argument aufrufen, z. B. hello("Rust");. Die Deref-Zwangsumwandlung ermöglicht es, hello mit einer Referenz auf einen Wert vom Typ MyBox<String> aufzurufen, wie in Listing 15-12 gezeigt.

Dateiname: src/main.rs

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Listing 15-12: Aufrufen von hello mit einer Referenz auf einen MyBox<String>-Wert, was aufgrund der Deref-Zwangsumwandlung funktioniert

Wir rufen hier die hello-Funktion mit dem Argument &m auf, was eine Referenz auf einen MyBox<String>-Wert ist. Weil wir das Deref-Trait für MyBox<T> in Listing 15-10 implementiert haben, kann Rust &MyBox<String> in &String umwandeln, indem es deref aufruft. Die Standardbibliothek liefert eine Implementierung von Deref für String, die einen String-Slice zurückgibt, und dies ist in der API-Dokumentation für Deref. Rust ruft deref erneut auf, um das &String in &str umzuwandeln, was der Definition der hello-Funktion entspricht.

Wenn Rust keine Deref-Zwangsumwandlung implementieren würde, müssten wir den Code in Listing 15-13 schreiben, anstatt den Code in Listing 15-12, um hello mit einem Wert vom Typ &MyBox<String> aufzurufen.

Dateiname: src/main.rs

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Listing 15-13: Der Code, den wir schreiben müssten, wenn Rust keine Deref-Zwangsumwandlung hätte

Die (*m) dereferenziert das MyBox<String> in eine String. Dann nehmen die & und [..] einen String-Slice der String, der gleich der gesamten Zeichenkette ist, um der Signatur von hello zu entsprechen. Dieser Code ohne Deref-Zwangsumwandlungen ist schwerer lesbar, zu schreiben und zu verstehen, da all diese Symbole beteiligt sind. Die Deref-Zwangsumwandlung ermöglicht es Rust, diese Umwandlungen für uns automatisch zu verarbeiten.

Wenn das Deref-Trait für die beteiligten Typen definiert ist, wird Rust die Typen analysieren und so oft wie nötig Deref::deref verwenden, um eine Referenz zu erhalten, die mit dem Parametertyp übereinstimmt. Die Anzahl der Mal, wie Deref::deref eingefügt werden muss, wird zur Compile-Zeit bestimmt, sodass es keine Laufzeitbelastung gibt, um von der Deref-Zwangsumwandlung Gebrauch zu machen!

Wie Deref-Zwangsumwandlung mit Veränderbarkeit interagiert

Ähnlich wie Sie das Deref-Trait verwenden, um den *-Operator für unveränderliche Referenzen zu überschreiben, können Sie das DerefMut-Trait verwenden, um den *-Operator für veränderliche Referenzen zu überschreiben.

Rust führt Deref-Zwangsumwandlungen aus, wenn es in drei Fällen Typen und Trait-Implementierungen findet:

  • Von &T zu &U, wenn T: Deref<Target=U>
  • Von &mut T zu &mut U, wenn T: DerefMut<Target=U>
  • Von &mut T zu &U, wenn T: Deref<Target=U>

Die ersten beiden Fälle sind identisch, außer dass der zweite die Veränderbarkeit implementiert. Der erste Fall besagt, dass wenn Sie eine &T haben und T Deref zu einem gewissen Typ U implementiert, Sie transparent eine &U erhalten können. Der zweite Fall besagt, dass die gleiche Deref-Zwangsumwandlung für veränderliche Referenzen erfolgt.

Der dritte Fall ist komplizierter: Rust wird auch eine veränderliche Referenz in eine unveränderliche umwandeln. Die Umkehrung ist jedoch nicht möglich: unveränderliche Referenzen werden niemals in veränderliche Referenzen umgewandelt. Aufgrund der Entlehnungsregeln, wenn Sie eine veränderliche Referenz haben, muss diese die einzige Referenz auf die Daten sein (sonst würde das Programm nicht kompilieren). Das Konvertieren einer veränderlichen Referenz in eine unveränderliche Referenz wird die Entlehnungsregeln niemals verletzen. Das Konvertieren einer unveränderlichen Referenz in eine veränderliche Referenz würde erfordern, dass die ursprüngliche unveränderliche Referenz die einzige unveränderliche Referenz auf die Daten ist, aber die Entlehnungsregeln gewährleisten dies nicht. Daher kann Rust nicht davon ausgehen, dass das Konvertieren einer unveränderlichen Referenz in eine veränderliche Referenz möglich ist.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Treating Smart Pointers Like Regular References With Deref" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.