Erweiterte Rust-Traits-Exploration

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 Advanced Traits. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir uns mit den fortgeschritteneren Details von Traits befassen, die zuvor in "Traits: Defining Shared Behavior" behandelt wurden, jetzt dass du eine bessere Vorstellung von Rust hast.

Fortgeschrittene Traits

Wir haben zuerst Traits in "Traits: Defining Shared Behavior" behandelt, aber wir haben die fortgeschritteneren Details nicht diskutiert. Jetzt, da du mehr über Rust weißt, können wir uns auf die Feinheiten eingehen.

Assoziierte Typen

Assoziierte Typen verbinden einen Typ-Platzhalter mit einem Trait, sodass die Trait-Methodendefinitionen diese Platzhaltertypen in ihren Signaturen verwenden können. Der Implementierer eines Traits wird den konkreten Typ angeben, der anstelle des Platzhaltertyps für die spezifische Implementierung verwendet werden soll. Auf diese Weise können wir einen Trait definieren, der einige Typen verwendet, ohne genau zu wissen, welche diese Typen sind, bis der Trait implementiert wird.

Wir haben die meisten der fortgeschrittenen Funktionen in diesem Kapitel als selten benötigt beschrieben. Assoziierte Typen befinden sich dazwischen: Sie werden seltener verwendet als die Funktionen, die im Rest des Buches erklärt werden, aber häufiger als viele der anderen in diesem Kapitel diskutierten Funktionen.

Ein Beispiel für einen Trait mit einem assoziierten Typ ist der Iterator-Trait, den die Standardbibliothek bereitstellt. Der assoziierte Typ heißt Item und steht für den Typ der Werte, über die der Typ, der das Iterator-Trait implementiert, iteriert. Die Definition des Iterator-Traits ist wie in Listing 19-12 gezeigt.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Listing 19-12: Die Definition des Iterator-Traits, der einen assoziierten Typ Item hat

Der Typ Item ist ein Platzhalter, und die Definition der next-Methode zeigt, dass sie Werte vom Typ Option<Self::Item> zurückgeben wird. Implementierer des Iterator-Traits werden den konkreten Typ für Item angeben, und die next-Methode wird eine Option zurückgeben, die einen Wert dieses konkreten Typs enthält.

Assoziierte Typen können wie ein ähnliches Konzept zu Generics erscheinen, in dem letztere uns ermöglichen, eine Funktion zu definieren, ohne anzugeben, welche Typen sie verarbeiten kann. Um den Unterschied zwischen den beiden Konzepten zu untersuchen, betrachten wir eine Implementierung des Iterator-Traits für einen Typ namens Counter, der angibt, dass der Item-Typ u32 ist:

Dateiname: src/lib.rs

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        --snip--

Diese Syntax scheint der von Generics vergleichbar zu sein. Warum definieren wir den Iterator-Trait also nicht einfach mit Generics, wie in Listing 19-13 gezeigt?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

Listing 19-13: Eine hypothetische Definition des Iterator-Traits mit Generics

Der Unterschied besteht darin, dass wir bei der Verwendung von Generics wie in Listing 19-13 die Typen in jeder Implementierung annotieren müssen; da wir auch Iterator<``String``> für Counter oder irgendeinen anderen Typ implementieren können, könnten wir für Counter mehrere Implementierungen von Iterator haben. Mit anderen Worten, wenn ein Trait einen generischen Parameter hat, kann es für einen Typ mehrfach implementiert werden, wobei die konkreten Typen der generischen Typparameter jeweils geändert werden. Wenn wir die next-Methode auf Counter verwenden, müssten wir Typ-Annotationen angeben, um anzuzeigen, welche Implementierung von Iterator wir verwenden möchten.

Mit assoziierten Typen müssen wir die Typen nicht annotieren, weil wir einen Trait für einen Typ nicht mehrfach implementieren können. In Listing 19-12 mit der Definition, die assoziierte Typen verwendet, können wir nur einmal wählen, was der Typ von Item sein wird, da es nur eine impl Iterator for Counter geben kann. Wir müssen nicht überall dort angeben, dass wir einen Iterator von u32-Werten möchten, wo wir next auf Counter aufrufen.

Assoziierte Typen werden auch zum Vertrag des Traits: Implementierer des Traits müssen einen Typ angeben, um für den assoziierten Typ-Platzhalter zu stehen. Assoziierte Typen haben oft einen Namen, der beschreibt, wie der Typ verwendet wird, und es ist eine gute Praxis, den assoziierten Typ in der API-Dokumentation zu dokumentieren.

Standardgenerische Typparameter und Operatorüberladung

Wenn wir generische Typparameter verwenden, können wir einen Standardkonkreten Typ für den generischen Typ angeben. Dies eliminiert die Notwendigkeit für die Implementierer eines Traits, einen konkreten Typ anzugeben, wenn der Standardtyp funktioniert. Sie geben einen Standardtyp an, wenn Sie einen generischen Typ mit der <PlatzhalterTyp=KonkreterTyp>-Syntax deklarieren.

Ein großartiges Beispiel für eine Situation, in der diese Technik nützlich ist, ist die Operatorüberladung, bei der Sie das Verhalten eines Operators (wie +) in bestimmten Situationen anpassen.

Rust erlaubt es Ihnen nicht, eigene Operatoren zu erstellen oder beliebige Operatoren zu überladen. Sie können jedoch die Operationen und die zugehörigen Traits in std::ops überladen, indem Sie die mit dem Operator assoziierten Traits implementieren. Beispielsweise überladen wir in Listing 19-14 den +-Operator, um zwei Point-Instanzen zusammenzuzählen. Wir tun dies, indem wir den Add-Trait auf einer Point-Struktur implementieren.

Dateiname: src/main.rs

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

Listing 19-14: Implementieren des Add-Traits, um den +-Operator für Point-Instanzen zu überladen

Die add-Methode addiert die x-Werte von zwei Point-Instanzen und die y-Werte von zwei Point-Instanzen, um eine neue Point zu erstellen. Der Add-Trait hat einen assoziierten Typ namens Output, der den Typ bestimmt, der von der add-Methode zurückgegeben wird.

Der Standardgenerische Typ in diesem Code befindet sich innerhalb des Add-Traits. Hier ist seine Definition:

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

Dieser Code sollte generell vertraut sein: ein Trait mit einer Methode und einem assoziierten Typ. Der neue Teil ist Rhs=Self: Diese Syntax wird als Standardtypparameter bezeichnet. Der generische Typparameter Rhs (Abkürzung für "right-hand side") definiert den Typ des rhs-Parameters in der add-Methode. Wenn wir keinen konkreten Typ für Rhs angeben, wenn wir den Add-Trait implementieren, wird der Typ von Rhs standardmäßig auf Self gesetzt, was der Typ sein wird, für den wir Add implementieren.

Als wir Add für Point implementiert haben, haben wir den Standardwert für Rhs verwendet, weil wir zwei Point-Instanzen addieren wollten. Schauen wir uns ein Beispiel an, bei dem wir den Add-Trait implementieren, bei dem wir den Rhs-Typ anpassen möchten, anstatt den Standardwert zu verwenden.

Wir haben zwei Structs, Millimeters und Meters, die Werte in unterschiedlichen Einheiten speichern. Diese dünne Umhüllung eines vorhandenen Typs in einem anderen Struct wird als Newtype-Pattern bezeichnet, das wir im Abschnitt "Verwendung des Newtype-Patterns, um externe Traits auf externe Typen zu implementieren" im Detail beschreiben. Wir möchten die Werte in Millimetern zu den Werten in Metern addieren und die Implementierung von Add so konfigurieren, dass die Umrechnung korrekt durchgeführt wird. Wir können Add für Millimeters mit Meters als Rhs implementieren, wie in Listing 19-15 gezeigt.

Dateiname: src/lib.rs

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Listing 19-15: Implementieren des Add-Traits auf Millimeters, um Millimeters und Meters zu addieren

Um Millimeters und Meters zu addieren, geben wir impl Add<Meters> an, um den Wert des Rhs-Typparameters festzulegen, anstatt den Standardwert von Self zu verwenden.

Sie werden Standardtypparameter auf zwei Hauptweisen verwenden:

  1. Um einen Typ zu erweitern, ohne vorhandenen Code zu brechen
  2. Um in bestimmten Fällen eine Anpassung zu ermöglichen, die die meisten Benutzer nicht benötigen werden

Der Add-Trait der Standardbibliothek ist ein Beispiel für den zweiten Zweck: Normalerweise werden Sie zwei ähnliche Typen addieren, aber der Add-Trait bietet die Möglichkeit, darüber hinaus anzupassen. Die Verwendung eines Standardtypparameters in der Add-Trait-Definition bedeutet, dass Sie die zusätzliche Parameter in der Regel nicht angeben müssen. Mit anderen Worten, ein bisschen Implementierungsaufwand ist nicht erforderlich, was es einfacher macht, den Trait zu verwenden.

Der erste Zweck ist ähnlich wie der zweite, nur umgekehrt: Wenn Sie einem bestehenden Trait einen Typparameter hinzufügen möchten, können Sie ihm einen Standardwert geben, um die Funktionalität des Traits zu erweitern, ohne den vorhandenen Implementierungscode zu brechen.

Die Klärung von Methoden mit demselben Namen

In Rust hindert nichts daran, dass ein Trait eine Methode mit demselben Namen wie eine Methode eines anderen Traits hat, und Rust verhindert auch nicht, dass Sie beide Traits auf einem Typ implementieren. Es ist auch möglich, eine Methode direkt auf dem Typ mit demselben Namen wie Methoden aus Traits zu implementieren.

Wenn Sie Methoden mit demselben Namen aufrufen, müssen Sie Rust mitteilen, welche Sie verwenden möchten. Betrachten Sie den Code in Listing 19-16, in dem wir zwei Traits, Pilot und Wizard, definiert haben, die beide eine Methode namens fly haben. Anschließend implementieren wir beide Traits auf einem Typ Human, auf dem bereits eine Methode namens fly implementiert ist. Jede fly-Methode macht etwas anderes.

Dateiname: src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

Listing 19-16: Zwei Traits werden definiert, um eine fly-Methode zu haben, und werden auf dem Human-Typ implementiert, und eine fly-Methode wird direkt auf Human implementiert.

Wenn wir fly auf einer Instanz von Human aufrufen, wählt der Compiler standardmäßig die Methode aus, die direkt auf dem Typ implementiert ist, wie in Listing 19-17 gezeigt.

Dateiname: src/main.rs

fn main() {
    let person = Human;
    person.fly();
}

Listing 19-17: Aufrufen von fly auf einer Instanz von Human

Wenn Sie diesen Code ausführen, wird *waving arms furiously* gedruckt, was zeigt, dass Rust die direkt auf Human implementierte fly-Methode aufgerufen hat.

Um die fly-Methoden aus dem Pilot-Trait oder dem Wizard-Trait aufzurufen, müssen wir eine etwas explizitere Syntax verwenden, um anzugeben, welche fly-Methode wir meinen. Listing 19-18 demonstriert diese Syntax.

Dateiname: src/main.rs

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Listing 19-18: Angeben, welche fly-Methode eines Traits wir aufrufen möchten

Das Angabe des Traitnamens vor dem Methodennamen klärt für Rust auf, welche fly-Implementierung wir aufrufen möchten. Wir könnten auch Human::fly(&person) schreiben, was der in Listing 19-18 verwendeten person.fly() entspricht, aber dies ist etwas länger zu schreiben, wenn wir keine Klärung benötigen.

Wenn Sie diesen Code ausführen, wird folgendes gedruckt:

This is your captain speaking.
Up!
*waving arms furiously*

Da die fly-Methode einen self-Parameter akzeptiert, könnte Rust, wenn wir zwei Typen haben, die beide ein Trait implementieren, herausfinden, welche Traitimplementierung basierend auf dem Typ von self zu verwenden ist.

Assoziierte Funktionen, die keine Methoden sind, haben jedoch keinen self-Parameter. Wenn es mehrere Typen oder Traits gibt, die nicht-methodische Funktionen mit demselben Funktionsnamen definieren, weiß Rust nicht immer, welchen Typ Sie meinen, es sei denn, Sie verwenden die vollqualifizierte Syntax. Beispielsweise erstellen wir in Listing 19-19 ein Trait für einen Tierheim, das alle Welpen Spot nennen möchte. Wir erstellen ein Animal-Trait mit einer assoziierten nicht-methodischen Funktion baby_name. Das Animal-Trait wird für die Struktur Dog implementiert, für die wir ebenfalls direkt eine assoziierte nicht-methodische Funktion baby_name bereitstellen.

Dateiname: src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}

Listing 19-19: Ein Trait mit einer assoziierten Funktion und ein Typ mit einer assoziierten Funktion gleichen Namens, der auch das Trait implementiert

Wir implementieren den Code für das Nennen aller Welpen Spot in der assoziierten Funktion baby_name, die auf Dog definiert ist. Der Dog-Typ implementiert auch das Trait Animal, das die Eigenschaften beschreibt, die alle Tiere haben. Welpen werden Welpen genannt, und das wird in der Implementierung des Animal-Traits auf Dog in der mit dem Animal-Trait assoziierten baby_name-Funktion ausgedrückt.

In main rufen wir die Dog::baby_name-Funktion auf, die direkt die auf Dog definierte assoziierte Funktion aufruft. Dieser Code druckt folgendes aus:

A baby dog is called a Spot

Dieser Ausgabewert ist nicht der, den wir wollten. Wir möchten die baby_name-Funktion aufrufen, die Teil des Animal-Traits ist, das wir auf Dog implementiert haben, so dass der Code A baby dog is called a puppy druckt. Die Technik, die wir in Listing 19-18 verwendeten, um den Traitnamen anzugeben, hilft hier nicht; wenn wir main in den Code in Listing 19-20 ändern, erhalten wir einen Kompilierungsfehler.

Dateiname: src/main.rs

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

Listing 19-20: Versuch, die baby_name-Funktion aus dem Animal-Trait aufzurufen, aber Rust weiß nicht, welche Implementierung zu verwenden

Da Animal::baby_name keinen self-Parameter hat und es möglicherweise andere Typen gibt, die das Animal-Trait implementieren, kann Rust nicht herausfinden, welche Implementierung von Animal::baby_name wir möchten. Wir erhalten diesen Compilerfehler:

error[E0283]: type annotations needed
  --> src/main.rs:20:43
   |
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot infer
type
   |
   = note: cannot satisfy `_: Animal`

Um die Klärung zu treffen und Rust zu sagen, dass wir die Implementierung von Animal für Dog verwenden möchten, im Gegensatz zur Implementierung von Animal für einen anderen Typ, müssen wir die vollqualifizierte Syntax verwenden. Listing 19-21 demonstriert, wie die vollqualifizierte Syntax verwendet wird.

Dateiname: src/main.rs

fn main() {
    println!(
        "A baby dog is called a {}",
        <Dog as Animal>::baby_name()
    );
}

Listing 19-21: Verwendung der vollqualifizierten Syntax, um anzugeben, dass wir die baby_name-Funktion aus dem Animal-Trait als auf Dog implementiert aufrufen möchten

Wir geben Rust eine Typanmerkung innerhalb der spitzen Klammern, was angibt, dass wir die baby_name-Methode aus dem Animal-Trait als auf Dog implementiert aufrufen möchten, indem wir sagen, dass wir den Dog-Typ für diesen Funktionsaufruf als Animal behandeln möchten. Dieser Code wird jetzt das ausgeben, was wir wollen:

A baby dog is called a puppy

Im Allgemeinen ist die vollqualifizierte Syntax wie folgt definiert:

<Type as Trait>::function(receiver_if_method, next_arg,...);

Für assoziierte Funktionen, die keine Methoden sind, gäbe es keinen receiver: es gäbe nur die Liste der anderen Argumente. Sie könnten die vollqualifizierte Syntax überall verwenden, wo Sie Funktionen oder Methoden aufrufen. Allerdings sind Sie berechtigt, beliebigen Teil dieser Syntax zu weglassen, den Rust aus anderen Informationen im Programm herausfinden kann. Sie müssen nur diese umständlichere Syntax in Fällen verwenden, in denen es mehrere Implementierungen gibt, die den gleichen Namen verwenden und Rust Hilfe benötigt, um zu identifizieren, welche Implementierung Sie aufrufen möchten.

Verwendung von Supertraits

Manchmal könnten Sie eine Trait-Definition schreiben, die von einem anderen Trait abhängt: Damit ein Typ die erste Trait implementiert, möchten Sie verlangen, dass dieser Typ auch das zweite Trait implementiert. Sie würden dies tun, um die assoziierten Elemente des zweiten Traits in Ihrer Trait-Definition nutzen zu können. Das Trait, auf das Ihre Trait-Definition zurückgreift, wird als Supertrait Ihres Traits bezeichnet.

Nehmen wir beispielsweise an, dass wir ein OutlinePrint-Trait mit einer outline_print-Methode erstellen möchten, die einen gegebenen Wert so formatiert ausgibt, dass er in Sternchen eingerahmt ist. Das heißt, wenn wir eine Point-Struktur haben, die das Standardbibliothekstrait Display implementiert, um (x, y) zu ergeben, und wir outline_print auf einer Point-Instanz aufrufen, die 1 für x und 3 für y hat, sollte es folgendes ausgeben:

**********
*        *
* (1, 3) *
*        *
**********

In der Implementierung der outline_print-Methode möchten wir die Funktionalität des Display-Traits nutzen. Daher müssen wir angeben, dass das OutlinePrint-Trait nur für Typen funktionieren wird, die auch Display implementieren und die Funktionalität bieten, die OutlinePrint benötigt. Wir können das in der Trait-Definition tun, indem wir OutlinePrint: Display angeben. Diese Technik ähnelt der Hinzufügung eines Trait-Bounds zu einem Trait. Listing 19-22 zeigt eine Implementierung des OutlinePrint-Traits.

Dateiname: src/main.rs

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

Listing 19-22: Implementierung des OutlinePrint-Traits, das die Funktionalität von Display benötigt

Da wir angegeben haben, dass OutlinePrint das Display-Trait erfordert, können wir die to_string-Funktion verwenden, die automatisch für jeden Typ implementiert wird, der Display implementiert. Wenn wir versuchten, to_string zu verwenden, ohne einen Doppelpunkt hinzuzufügen und das Display-Trait nach dem Traitnamen anzugeben, würden wir einen Fehler erhalten, der besagt, dass keine Methode namens to_string für den Typ &Self im aktuellen Gültigkeitsbereich gefunden wurde.

Schauen wir uns an, was passiert, wenn wir versuchen, OutlinePrint auf einem Typ zu implementieren, der Display nicht implementiert, wie die Point-Struktur:

Dateiname: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

Wir erhalten einen Fehler, der besagt, dass Display erforderlich ist, aber nicht implementiert ist:

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

Um dies zu beheben, implementieren wir Display auf Point und erfüllen die Einschränkung, die OutlinePrint erfordert, wie folgt:

Dateiname: src/main.rs

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

Anschließend wird die Implementierung des OutlinePrint-Traits auf Point erfolgreich kompilieren, und wir können outline_print auf einer Point-Instanz aufrufen, um sie innerhalb eines Sternchenrahmens anzuzeigen.

Verwendung des Newtype-Patterns, um externe Traits zu implementieren

In "Implementing a Trait on a Type" haben wir die Orphan-Regel erwähnt, die besagt, dass wir nur dann ein Trait auf einem Typ implementieren dürfen, wenn entweder das Trait oder der Typ oder beide unserem Kasten lokal sind. Es ist möglich, diese Einschränkung umgehen zu können, indem wir das Newtype-Pattern verwenden, bei dem es darum geht, einen neuen Typ in einer Tuple-Struktur zu erstellen. (Wir haben Tuple-Strukturen in "Using Tuple Structs Without Named Fields to Create Different Types" behandelt.) Die Tuple-Struktur wird ein Feld haben und eine dünne Umhüllung des Typs sein, für den wir ein Trait implementieren möchten. Dann ist der Umhüllungstyp unserem Kasten lokal, und wir können das Trait auf der Umhüllung implementieren. Newtype ist ein Begriff, der aus der Haskell-Programmiersprache stammt. Es entsteht keine Laufzeitleistungseinbuße bei der Verwendung dieses Musters, und der Umhüllungstyp wird zur Compile-Zeit weggelassen.

Als Beispiel sagen wir, dass wir Display auf Vec<T> implementieren möchten, was uns die Orphan-Regel direkt verwehrt, da das Display-Trait und der Vec<T>-Typ außerhalb unseres Kastens definiert sind. Wir können eine Wrapper-Struktur erstellen, die eine Instanz von Vec<T> enthält; dann können wir Display auf Wrapper implementieren und den Vec<T>-Wert verwenden, wie in Listing 19-23 gezeigt.

Dateiname: src/main.rs

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![
        String::from("hello"),
        String::from("world"),
    ]);
    println!("w = {w}");
}

Listing 19-23: Erstellen eines Wrapper-Typs um Vec<String>, um Display zu implementieren

Die Implementierung von Display verwendet self.0, um auf das innere Vec<T> zuzugreifen, da Wrapper eine Tuple-Struktur ist und Vec<T> das Element an Index 0 in der Tuple ist. Dann können wir die Funktionalität des Display-Typs auf Wrapper verwenden.

Der Nachteil der Verwendung dieser Technik ist, dass Wrapper ein neuer Typ ist, sodass er nicht die Methoden des Werts hat, den er enthält. Wir müssten alle Methoden von Vec<T> direkt auf Wrapper implementieren, sodass die Methoden an self.0 delegieren, was uns ermöglichen würde, Wrapper genauso wie ein Vec<T> zu behandeln. Wenn wir wollten, dass der neue Typ alle Methoden des inneren Typs hat, wäre die Implementierung des Deref-Traits auf Wrapper, um den inneren Typ zurückzugeben, eine Lösung (wir haben die Implementierung des Deref-Traits in "Treating Smart Pointers Like Regular References with Deref" diskutiert). Wenn wir nicht wollten, dass der Wrapper-Typ alle Methoden des inneren Typs hat - beispielsweise, um das Verhalten des Wrapper-Typs einzuschränken - müssten wir nur die Methoden implementieren, die wir tatsächlich möchten, manuell.

Dieses Newtype-Pattern ist auch nützlich, auch wenn es um Traits nicht geht. Lassen Sie uns den Fokus wechseln und einige fortgeschrittene Wege betrachten, um mit dem Typsystem von Rust zu interagieren.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Advanced Traits Lab abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.