Entfernen von Duplikaten durch Extrahieren einer Funktion

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 Entfernen von Duplikaten durch Extrahieren einer Funktion. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab lernen wir, wie wir Code-Duplizierung entfernen, indem wir eine Funktion extrahieren und Generics verwenden, um auf abstrakte Typen zu operieren.

Entfernen von Duplikaten durch Extrahieren einer Funktion

Generics ermöglichen es uns, spezifische Typen durch einen Platzhalter zu ersetzen, der mehrere Typen repräsentiert, um Code-Duplizierung zu entfernen. Bevor wir uns der Generics-Syntax widmen, schauen wir uns zunächst an, wie wir Duplikate auf eine Weise entfernen, die keine generischen Typen involviert, indem wir eine Funktion extrahieren, die spezifische Werte durch einen Platzhalter ersetzt, der mehrere Werte repräsentiert. Anschließend wenden wir die gleiche Technik an, um eine generische Funktion zu extrahieren! Indem wir uns ansehen, wie man duplizierten Code erkennt, den man in eine Funktion extrahieren kann, werden wir beginnen, duplizierten Code zu erkennen, der generics verwenden kann.

Wir beginnen mit dem kurzen Programm in Listing 10-1, das die größte Zahl in einer Liste findet.

Dateiname: src/main.rs

fn main() {
  1 let number_list = vec![34, 50, 25, 100, 65];

  2 let mut largest = &number_list[0];

  3 for number in &number_list {
      4 if number > largest {
          5 largest = number;
        }
    }

    println!("The largest number is {largest}");
}

Listing 10-1: Finden der größten Zahl in einer Liste von Zahlen

Wir speichern eine Liste von ganzen Zahlen in der Variable number_list [1] und legen eine Referenz auf die erste Zahl in der Liste in einer Variable namens largest fest [2]. Anschließend iterieren wir über alle Zahlen in der Liste [3], und wenn die aktuelle Zahl größer ist als die Zahl, die in largest gespeichert ist [4], ersetzen wir die Referenz in dieser Variable [5]. Wenn die aktuelle Zahl jedoch kleiner oder gleich der bisher größten Zahl ist, ändert sich die Variable nicht, und der Code geht zur nächsten Zahl in der Liste über. Nachdem alle Zahlen in der Liste betrachtet wurden, sollte largest auf die größte Zahl verweisen, was in diesem Fall 100 ist.

Wir haben nun die Aufgabe, die größte Zahl in zwei verschiedenen Listen von Zahlen zu finden. Um dies zu tun, können wir uns entscheiden, den Code in Listing 10-1 zu duplizieren und die gleiche Logik an zwei verschiedenen Stellen im Programm zu verwenden, wie in Listing 10-2 gezeigt.

Dateiname: src/main.rs

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");
}

Listing 10-2: Code, um die größte Zahl in zwei Listen von Zahlen zu finden

Obwohl dieser Code funktioniert, ist das Duplizieren von Code langwierig und fehleranfällig. Wir müssen auch daran denken, den Code an mehreren Stellen zu aktualisieren, wenn wir ihn ändern möchten.

Um diese Duplikation zu eliminieren, werden wir eine Abstraktion erstellen, indem wir eine Funktion definieren, die auf jeder Liste von ganzen Zahlen operiert, die als Parameter übergeben wird. Diese Lösung macht unseren Code klarer und ermöglicht es uns, das Konzept des Findens der größten Zahl in einer Liste abstrakt auszudrücken.

In Listing 10-3 extrahieren wir den Code, der die größte Zahl findet, in eine Funktion namens largest. Anschließend rufen wir die Funktion auf, um die größte Zahl in den beiden Listen aus Listing 10-2 zu finden. Wir könnten auch die Funktion auf jeder anderen Liste von i32-Werten verwenden, die wir in Zukunft haben könnten.

Dateiname: src/main.rs

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {result}");
}

Listing 10-3: Abstrahierter Code, um die größte Zahl in zwei Listen zu finden

Die largest-Funktion hat einen Parameter namens list, der jede konkrete Slice von i32-Werten repräsentiert, die wir in die Funktion übergeben könnten. Folglich wird der Code, wenn wir die Funktion aufrufen, auf den spezifischen Werten ausgeführt, die wir übergeben.

Zusammenfassend betrachtet, hier sind die Schritte, die wir durchgeführt haben, um den Code von Listing 10-2 zu Listing 10-3 zu ändern:

  1. Identifizieren Sie duplizierten Code.
  2. Extrahieren Sie den duplizierten Code in den Funktionskörper und geben Sie die Eingaben und Rückgabewerte dieses Codes in der Funktionssignatur an.
  3. Aktualisieren Sie die beiden Instanzen des duplizierten Codes, um die Funktion stattdessen aufzurufen.

Als nächstes werden wir diese gleichen Schritte mit Generics verwenden, um die Code-Duplizierung zu reduzieren. Auf die gleiche Weise wie der Funktionskörper auf eine abstrakte list anstelle von spezifischen Werten operieren kann, ermöglichen Generics es Code, auf abstrakte Typen zu operieren.

Zum Beispiel hätten wir zwei Funktionen: eine, die das größte Element in einer Slice von i32-Werten findet, und eine, die das größte Element in einer Slice von char-Werten findet. Wie würden wir diese Duplikation eliminieren? Lassen Sie uns es herausfinden!

Generische Datentypen

Wir verwenden Generics, um Definitionen für Elemente wie Funktionssignaturen oder Strukturen zu erstellen, die wir dann mit vielen verschiedenen konkreten Datentypen verwenden können. Schauen wir uns zunächst an, wie man Funktionen, Strukturen, Enums und Methoden mit Generics definiert. Anschließend diskutieren wir, wie Generics die Codeleistung beeinflussen.

In Funktionsdefinitionen

Wenn wir eine Funktion definieren, die Generics verwendet, platzieren wir die Generics in der Signatur der Funktion, wo wir normalerweise die Datentypen der Parameter und des Rückgabewerts angeben würden. Dadurch wird unser Code flexibler und bietet unseren Funktionsaufrufern mehr Funktionalität, während gleichzeitig Code-Duplizierung vermieden wird.

Fortfahrend mit unserer largest-Funktion zeigt Listing 10-4 zwei Funktionen, die beide den größten Wert in einer Slice finden. Anschließend kombinieren wir diese zu einer einzigen Funktion, die Generics verwendet.

Dateiname: src/main.rs

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y','m', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
}

Listing 10-4: Zwei Funktionen, die sich nur in ihren Namen und in den Typen in ihren Signaturen unterscheiden

Die largest_i32-Funktion ist die, die wir in Listing 10-3 extrahiert haben, die den größten i32 in einer Slice findet. Die largest_char-Funktion findet den größten char in einer Slice. Die Funktionskörper haben denselben Code, also eliminieren wir die Duplikation, indem wir einen generischen Typparameter in einer einzigen Funktion einführen.

Um die Typen in einer neuen einzigen Funktion zu parametrisieren, müssen wir den Typparameter benennen, genauso wie wir es für die Wertparameter einer Funktion tun. Sie können jedes beliebige Bezeichner als Typparametername verwenden. Wir werden jedoch T verwenden, weil es nach Konvention in Rust üblich ist, Typparameternamen kurz zu wählen, oft nur einen Buchstaben, und Rusts Typennennkonvention ist CamelCase. Kurz für Typ, T ist die Standardauswahl der meisten Rust-Programmierer.

Wenn wir einen Parameter im Funktionskörper verwenden, müssen wir den Parameternamen in der Signatur deklarieren, damit der Compiler weiß, was dieser Name bedeutet. Ähnlich müssen wir den Typparameternamen in einer Funktionssignatur deklarieren, bevor wir ihn verwenden. Um die generische largest-Funktion zu definieren, platzieren wir die Typnamen-Deklarationen innerhalb von eckigen Klammern, <> zwischen dem Funktionsnamen und der Parameterliste, wie folgt:

fn largest<T>(list: &[T]) -> &T {

Wir lesen diese Definition wie folgt: Die Funktion largest ist generisch über einen bestimmten Typ T. Diese Funktion hat einen Parameter namens list, der eine Slice von Werten vom Typ T ist. Die largest-Funktion wird eine Referenz auf einen Wert vom gleichen Typ T zurückgeben.

Listing 10-5 zeigt die kombinierte largest-Funktionsdefinition, die den generischen Datentyp in ihrer Signatur verwendet. Die Liste zeigt auch, wie wir die Funktion mit einer Slice von i32-Werten oder char-Werten aufrufen können. Beachten Sie, dass dieser Code noch nicht kompilieren wird, aber wir werden ihn später in diesem Kapitel beheben.

Dateiname: src/main.rs

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y','m', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

Listing 10-5: Die largest-Funktion mit generischen Typparametern; dies kompiliert noch nicht

Wenn wir diesen Code gerade kompilieren, erhalten wir diesen Fehler:

error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

Der Hilfetext erwähnt std::cmp::PartialOrd, was ein Trait ist, und wir werden im nächsten Abschnitt über Traits sprechen. Für jetzt wissen Sie, dass dieser Fehler angibt, dass der Körper von largest nicht für alle möglichen Typen funktionieren wird, auf die T sein könnte. Da wir in der Funktion Werte vom Typ T vergleichen möchten, können wir nur Typen verwenden, deren Werte geordnet werden können. Um Vergleiche zu ermöglichen, hat die Standardbibliothek das std::cmp::PartialOrd-Trait, das Sie auf Typen implementieren können (siehe Anhang C für mehr Informationen zu diesem Trait). Indem wir der Vorschrift im Hilfetext folgen, beschränken wir die gültigen Typen für T auf nur diejenigen, die PartialOrd implementieren, und dieses Beispiel wird kompilieren, da die Standardbibliothek PartialOrd sowohl für i32 als auch für char implementiert.

In Strukturdefinitionen

Wir können auch Strukturen definieren, um einen generischen Typparameter in einem oder mehreren Feldern mit der <>-Syntax zu verwenden. Listing 10-6 definiert eine Point<T>-Struktur, um x- und y-Koordinatenwerte beliebigen Typs zu speichern.

Dateiname: src/main.rs

1 struct Point<T> {
  2 x: T,
  3 y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Listing 10-6: Eine Point<T>-Struktur, die x- und y-Werte vom Typ T speichert

Die Syntax für das Verwenden von Generics in Strukturdefinitionen ähnelt der in Funktionsdefinitionen verwendeten. Zunächst deklarieren wir den Namen des Typparameters innerhalb von eckigen Klammern direkt nach dem Namen der Struktur [1]. Anschließend verwenden wir den generischen Typ in der Strukturdefinition, wo wir sonst konkrete Datentypen angeben würden [23].

Beachten Sie, dass wir nur einen generischen Typ verwendet haben, um Point<T> zu definieren. Diese Definition besagt, dass die Point<T>-Struktur generisch über einen bestimmten Typ T ist und die Felder x und y beide diesen gleichen Typ sind, unabhängig davon, welcher Typ das ist. Wenn wir eine Instanz von Point<T> erstellen, die Werte unterschiedlicher Typen hat, wie in Listing 10-7, wird unser Code nicht kompilieren.

Dateiname: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

Listing 10-7: Die Felder x und y müssen den gleichen Typ haben, da beide den gleichen generischen Datentyp T haben.

In diesem Beispiel, wenn wir den ganzzahligen Wert 5 für x zuweisen, informieren wir den Compiler, dass der generische Typ T für diese Instanz von Point<T> ein ganzzahliger Typ sein wird. Wenn wir dann 4.0 für y angeben, das wir als gleichen Typ wie x definiert haben, erhalten wir einen Typenfehler wie diesen:

error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-
point number

Um eine Point-Struktur zu definieren, bei der x und y beide generisch sind, aber unterschiedliche Typen haben können, können wir mehrere generische Typparameter verwenden. Beispielsweise ändern wir in Listing 10-8 die Definition von Point, um generisch über die Typen T und U zu sein, wobei x vom Typ T und y vom Typ U ist.

Dateiname: src/main.rs

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Listing 10-8: Eine Point<T, U>-Struktur, die über zwei Typen generisch ist, sodass x und y Werte unterschiedlicher Typen sein können

Jetzt sind alle gezeigten Instanzen von Point erlaubt! Sie können in einer Definition so viele generische Typparameter wie Sie möchten verwenden, aber das Verwenden von mehr als ein paar macht Ihren Code schwer lesbar. Wenn Sie feststellen, dass Sie in Ihrem Code viele generische Typen benötigen, kann dies darauf hinweisen, dass Ihr Code in kleinere Teile umstrukturiert werden muss.

In Enum-Definitionen

Wie bei Strukturen können wir Enums definieren, um generische Datentypen in ihren Varianten zu speichern. Schauen wir uns noch einmal das Option<T>-Enum an, das die Standardbibliothek bereitstellt und das wir im Kapitel 6 verwendet haben:

enum Option<T> {
    Some(T),
    None,
}

Diese Definition sollte Ihnen jetzt mehr Sinn machen. Wie Sie sehen können, ist das Option<T>-Enum generisch über den Typ T und hat zwei Varianten: Some, die einen Wert vom Typ T enthält, und eine None-Variante, die keinen Wert enthält. Indem wir das Option<T>-Enum verwenden, können wir das abstrakte Konzept eines optionalen Werts ausdrücken, und da Option<T> generisch ist, können wir diese Abstraktion unabhängig vom Typ des optionalen Werts verwenden.

Enums können auch mehrere generische Typen verwenden. Die Definition des Result-Enums, das wir im Kapitel 9 verwendet haben, ist ein Beispiel:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Das Result-Enum ist generisch über zwei Typen, T und E, und hat zwei Varianten: Ok, die einen Wert vom Typ T enthält, und Err, die einen Wert vom Typ E enthält. Diese Definition macht es bequem, das Result-Enum überall zu verwenden, wo wir eine Operation haben, die erfolgreich sein kann (einen Wert eines bestimmten Typs T zurückgeben) oder fehlschlagen kann (einen Fehler eines bestimmten Typs E zurückgeben). Tatsächlich haben wir dies verwendet, um eine Datei zu öffnen in Listing 9-3, wobei T mit dem Typ std::fs::File ausgefüllt wurde, wenn die Datei erfolgreich geöffnet wurde, und E mit dem Typ std::io::Error ausgefüllt wurde, wenn es Probleme beim Öffnen der Datei gab.

Wenn Sie in Ihrem Code Situationen erkennen, in denen mehrere Struktur- oder Enum-Definitionen nur in den Typen der Werte, die sie enthalten, unterschiedlich sind, können Sie die Duplikation vermeiden, indem Sie generische Typen verwenden.

In Methodendefinitionen

Wir können Methoden auf Strukturen und Enums implementieren (wie wir es im Kapitel 5 getan haben), und auch in ihren Definitionen generische Typen verwenden. Listing 10-9 zeigt die Point<T>-Struktur, die wir in Listing 10-6 definiert haben, mit einer Methode namens x, die auf ihr implementiert ist.

Dateiname: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Listing 10-9: Implementieren einer Methode namens x auf der Point<T>-Struktur, die eine Referenz auf das x-Feld vom Typ T zurückgibt

Hier haben wir eine Methode namens x auf Point<T> definiert, die eine Referenz auf die Daten im Feld x zurückgibt.

Beachten Sie, dass wir T direkt nach impl deklarieren müssen, damit wir T verwenden können, um anzugeben, dass wir Methoden auf dem Typ Point<T> implementieren. Indem wir T als generischen Typ nach impl deklarieren, kann Rust erkennen, dass der Typ in den eckigen Klammern in Point ein generischer Typ ist und kein konkreter Typ. Wir hätten für diesen generischen Parameter einen anderen Namen wählen können als den generischen Parameter, der in der Strukturdefinition deklariert wurde, aber es ist üblich, den gleichen Namen zu verwenden. Methoden, die innerhalb eines impl geschrieben werden, das den generischen Typ deklariert, werden für jede Instanz des Typs definiert, unabhängig davon, welchen konkreten Typ schließlich für den generischen Typ eingesetzt wird.

Wir können auch Einschränkungen für generische Typen angeben, wenn wir Methoden für den Typ definieren. Wir könnten beispielsweise Methoden nur auf Point<f32>-Instanzen implementieren, statt auf Point<T>-Instanzen mit beliebigem generischem Typ. In Listing 10-10 verwenden wir den konkreten Typ f32, was bedeutet, dass wir keine Typen nach impl deklarieren.

Dateiname: src/main.rs

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Listing 10-10: Ein impl-Block, der nur auf eine Struktur mit einem bestimmten konkreten Typ für den generischen Typparameter T anwendbar ist

Dieser Code bedeutet, dass der Typ Point<f32> eine distance_from_origin-Methode haben wird; andere Instanzen von Point<T>, bei denen T kein f32-Typ ist, werden diese Methode nicht haben. Die Methode misst, wie weit unser Punkt von dem Punkt bei den Koordinaten (0.0, 0.0) entfernt ist, und verwendet mathematische Operationen, die nur für Gleitkomma-Typen verfügbar sind.

Generische Typparameter in einer Strukturdefinition stimmen nicht immer mit denen überein, die Sie in der Methodensignatur derselben Struktur verwenden. Listing 10-11 verwendet die generischen Typen X1 und Y1 für die Point-Struktur und X2 und Y2 für die mixup-Methodensignatur, um das Beispiel verständlicher zu machen. Die Methode erstellt eine neue Point-Instanz mit dem x-Wert aus der self-Point (vom Typ X1) und dem y-Wert aus der übergebenen Point (vom Typ Y2).

Dateiname: src/main.rs

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

1 impl<X1, Y1> Point<X1, Y1> {
  2 fn mixup<X2, Y2>(
        self,
        other: Point<X2, Y2>,
    ) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
  3 let p1 = Point { x: 5, y: 10.4 };
  4 let p2 = Point { x: "Hello", y: 'c' };

  5 let p3 = p1.mixup(p2);

  6 println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Listing 10-11: Eine Methode, die generische Typen verwendet, die von ihrer Strukturdefinition verschieden sind

In main haben wir eine Point definiert, die für x einen i32 hat (mit dem Wert 5) und für y einen f64 (mit dem Wert 10.4 [3]). Die Variable p2 ist eine Point-Struktur, die für x einen String-Slice hat (mit dem Wert "Hello") und für y ein char (mit dem Wert c [4]). Wenn wir mixup auf p1 mit dem Argument p2 aufrufen, erhalten wir p3 [5], das für x einen i32 haben wird, weil x von p1 stammt. Die Variable p3 wird für y ein char haben, weil y von p2 stammt. Der println!-Makroaufruf [6] wird p3.x = 5, p3.y = c ausgeben.

Zweck dieses Beispiels ist es, eine Situation zu demonstrieren, in der einige generische Parameter mit impl deklariert werden und einige mit der Methodendefinition. Hier werden die generischen Parameter X1 und Y1 nach impl deklariert [1], weil sie mit der Strukturdefinition zusammenhängen. Die generischen Parameter X2 und Y2 werden nach fn mixup deklariert [2], weil sie nur für die Methode relevant sind.

Leistung von Code mit Generics

Sie könnten sich fragen, ob es einen Laufzeitaufwand gibt, wenn Sie generische Typparameter verwenden. Die gute Nachricht ist, dass das Verwenden von generischen Typen Ihren Programm nicht langsamer laufen lässt als es mit konkreten Typen wäre.

Rust erreicht dies, indem es die Monomorphisierung des Codes mit Generics zur Compile-Zeit durchführt. Monomorphisierung ist der Prozess, bei dem generischer Code in spezifischen Code umgewandelt wird, indem die konkreten Typen eingefüllt werden, die bei der Kompilierung verwendet werden. In diesem Prozess macht der Compiler das Gegenteil der Schritte, die wir verwendet haben, um die generische Funktion in Listing 10-5 zu erstellen: Der Compiler betrachtet alle Stellen, an denen generischer Code aufgerufen wird, und generiert Code für die konkreten Typen, mit denen der generische Code aufgerufen wird.

Schauen wir uns an, wie dies funktioniert, indem wir das generische Option<T>-Enum der Standardbibliothek verwenden:

let integer = Some(5);
let float = Some(5.0);

Wenn Rust diesen Code kompiliert, führt es die Monomorphisierung durch. Während dieses Prozesses liest der Compiler die Werte, die in Option<T>-Instanzen verwendet wurden, und identifiziert zwei Arten von Option<T>: eine ist i32 und die andere ist f64. Daher erweitert es die generische Definition von Option<T> in zwei Definitionen, die auf i32 und f64 spezialisiert sind, und ersetzt damit die generische Definition mit den spezifischen.

Die monomorphe Version des Codes sieht ähnlich wie folgend aus (der Compiler verwendet andere Namen als die hier verwendeten, um die Verdeutlichung zu erleichtern):

Dateiname: src/main.rs

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Das generische Option<T> wird durch die spezifischen Definitionen ersetzt, die der Compiler erstellt hat. Da Rust generischen Code in Code kompiliert, der den Typ in jeder Instanz angibt, haben wir keinen Laufzeitaufwand für das Verwenden von Generics. Wenn der Code läuft, verhält er sich genauso wie wenn wir jede Definition manuell dupliziert hätten. Der Prozess der Monomorphisierung macht Rusts Generics extrem effizient zur Laufzeit.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Entfernen von Duplikaten durch Extrahieren einer Funktion" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.