Unser I/O-Projekt verbessern

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 Improving Our I/O Project. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir untersuchen, wie Iteratoren verwendet werden können, um die Implementierung der Config::build-Funktion und der search-Funktion im I/O-Projekt aus Kapitel 12 zu verbessern.

Verbesserung unseres I/O-Projekts

Mit diesen neuen Kenntnissen über Iteratoren können wir das I/O-Projekt aus Kapitel 12 verbessern, indem wir Iteratoren verwenden, um Stellen im Code klarer und prägnanter zu gestalten. Schauen wir uns an, wie Iteratoren unsere Implementierung der Config::build-Funktion und der search-Funktion verbessern können.

Entfernen eines Klons mithilfe eines Iterators

In Listing 12-6 haben wir Code hinzugefügt, der einen Slice von String-Werten nahm und eine Instanz der Config-Struktur erzeugte, indem er in den Slice indizierte und die Werte klonierte, was es der Config-Struktur ermöglichte, diese Werte zu besitzen. In Listing 13-17 haben wir die Implementierung der Config::build-Funktion wiedergegeben, wie sie in Listing 12-23 war.

Dateiname: src/lib.rs

impl Config {
    pub fn build(
        args: &[String]
    ) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

Listing 13-17: Wiederholung der Config::build-Funktion aus Listing 12-23

Damals sagten wir, dass wir uns nicht um die ineffizienten clone-Aufrufe kümmern müssen, da wir sie später entfernen würden. Nun ist diese Zeit gekommen!

Wir brauchten hier clone, weil wir in dem Parameter args einen Slice mit String-Elementen haben, aber die build-Funktion besitzt args nicht. Um die Eigentumsgewalt einer Config-Instanz zurückzugeben, mussten wir die Werte aus den query- und filename-Feldern von Config klonen, damit die Config-Instanz ihre Werte besitzen kann.

Mit unseren neuen Kenntnissen über Iteratoren können wir die build-Funktion ändern, um die Eigentumsgewalt eines Iterators als Argument zu übernehmen, anstatt einen Slice zu entleihen. Wir werden die Iteratorfunktionalität verwenden, anstatt den Code, der die Länge des Slices überprüft und in bestimmte Positionen indiziert. Dies wird klären, was die Config::build-Funktion tut, da der Iterator die Werte zugreifen wird.

Sobald Config::build die Eigentumsgewalt des Iterators übernimmt und auf Indexoperationen verzichtet, die etwas entleihen, können wir die String-Werte aus dem Iterator in Config verschieben, anstatt clone aufzurufen und eine neue Allokation zu machen.

Direkte Verwendung des zurückgegebenen Iterators

Öffnen Sie die Datei src/main.rs Ihres I/O-Projekts, die ungefähr so aussehen sollte:

Dateiname: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    --snip--
}

Wir werden zunächst den Anfang der main-Funktion, wie er in Listing 12-24 war, in den Code von Listing 13-18 umändern, der diesmal einen Iterator verwendet. Dies wird nicht kompilieren, bis wir auch Config::build aktualisieren.

Dateiname: src/main.rs

fn main() {
    let config =
        Config::build(env::args()).unwrap_or_else(|err| {
            eprintln!("Problem parsing arguments: {err}");
            process::exit(1);
        });

    --snip--
}

Listing 13-18: Übergeben des Rückgabewerts von env::args an Config::build

Die env::args-Funktion gibt einen Iterator zurück! Anstatt die Iteratorwerte in einem Vektor zu sammeln und dann einen Slice an Config::build zu übergeben, geben wir jetzt die Eigentumsgewalt des von env::args zurückgegebenen Iterators direkt an Config::build weiter.

Als nächstes müssen wir die Definition von Config::build aktualisieren. In der Datei src/lib.rs Ihres I/O-Projekts ändern wir die Signatur von Config::build so, dass sie wie in Listing 13-19 aussieht. Dies wird immer noch nicht kompilieren, weil wir den Funktionskörper aktualisieren müssen.

Dateiname: src/lib.rs

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        --snip--

Listing 13-19: Aktualisierung der Signatur von Config::build, um einen Iterator zu erwarten

Die Standardbibliothekdokumentation zur env::args-Funktion zeigt, dass der Typ des Iterators, den sie zurückgibt, std::env::Args ist und dass dieser Typ das Iterator-Trait implementiert und String-Werte zurückgibt.

Wir haben die Signatur der Config::build-Funktion aktualisiert, sodass der Parameter args einen generischen Typ mit den Trait-Bounds impl Iterator<Item = String> anstelle von &[String] hat. Dieses Verwendungsszenario der impl Trait-Syntax, über die wir in "Traits as Parameters" diskutiert haben, bedeutet, dass args irgendein Typ sein kann, der das Iterator-Typ implementiert und String-Elemente zurückgibt.

Da wir die Eigentumsgewalt von args übernehmen und args durch das Iterieren darüber mutieren werden, können wir das mut-Schlüsselwort in die Spezifikation des args-Parameters hinzufügen, um es mutierbar zu machen.

Verwendung von Iterator-Trait-Methoden anstelle von Indizierung

Als nächstes werden wir den Körper von Config::build beheben. Da args das Iterator-Trait implementiert, wissen wir, dass wir die next-Methode darauf aufrufen können! Listing 13-20 aktualisiert den Code aus Listing 12-23, um die next-Methode zu verwenden.

Dateiname: src/lib.rs

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

Listing 13-20: Änderung des Körpers von Config::build, um Iterator-Methoden zu verwenden

Denken Sie daran, dass der erste Wert im Rückgabewert von env::args der Name des Programms ist. Wir möchten diesen ignorieren und zum nächsten Wert gelangen, daher rufen wir zuerst next auf und tun nichts mit dem Rückgabewert. Dann rufen wir next auf, um den Wert zu erhalten, den wir in das query-Feld von Config einfügen möchten. Wenn next Some zurückgibt, verwenden wir eine match, um den Wert zu extrahieren. Wenn es None zurückgibt, bedeutet das, dass nicht genug Argumente angegeben wurden, und wir geben frühzeitig einen Err-Wert zurück. Wir tun das Gleiche für den filename-Wert.

Codeverständlichkeit durch Iteratoradapter

Wir können auch Iteratoren im search-Funktion unseres I/O-Projekts nutzen, die hier in Listing 13-21 wiedergegeben ist, wie sie in Listing 12-19 war.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Listing 13-21: Die Implementierung der search-Funktion aus Listing 12-19

Wir können diesen Code auf eine kürzere Weise mit Iteratoradaptermethoden schreiben. Dadurch vermeiden wir auch, einen mutablen Zwischenvektor results zu haben. Der funktionale Programmierungstil bevorzugt es, den Umfang des mutablen Zustands zu minimieren, um den Code klarer zu machen. Das Entfernen des mutablen Zustands könnte eine zukünftige Verbesserung ermöglichen, um die Suche parallel durchzuführen, da wir nicht mehr die konkurrierende Zugriffe auf den results-Vektor verwalten müssten. Listing 13-22 zeigt diese Änderung.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    contents
     .lines()
     .filter(|line| line.contains(query))
     .collect()
}

Listing 13-22: Verwendung von Iteratoradaptermethoden in der Implementierung der search-Funktion

Denken Sie daran, dass der Zweck der search-Funktion darin besteht, alle Zeilen in contents zurückzugeben, die den query enthalten. Ähnlich wie das filter-Beispiel in Listing 13-16 verwendet dieser Code den filter-Adapter, um nur die Zeilen zu behalten, für die line.contains(query) true zurückgibt. Wir sammeln dann die übereinstimmenden Zeilen in einem anderen Vektor mit collect. Viel einfacher! Fühlen Sie sich frei, die gleiche Änderung auch in der search_case_insensitive-Funktion zu machen, um Iteratormethoden zu verwenden.

Auswahl zwischen Schleifen und Iteratoren

Die nächste logische Frage ist, welche Stil Sie in Ihrem eigenen Code wählen sollten und warum: die ursprüngliche Implementierung in Listing 13-21 oder die Version mit Iteratoren in Listing 13-22. Die meisten Rust-Programmierer bevorzugen den Iterator-Stil. Es ist zunächst etwas schwieriger, ihn zu verstehen, aber sobald Sie sich mit den verschiedenen Iteratoradaptern und ihren Funktionen vertraut gemacht haben, können Iteratoren einfacher zu verstehen sein. Anstatt sich mit den verschiedenen Teilen der Schleife und dem Erstellen neuer Vektoren herumzuschlagen, konzentriert sich der Code auf das höhere Ziel der Schleife. Dies abstrahiert einige der üblichen Codeteile, sodass es einfacher ist, die für diesen Code einzigartigen Konzepte zu erkennen, wie die Filterbedingung, die jedes Element im Iterator erfüllen muss.

Aber sind die beiden Implementierungen wirklich gleichwertig? Die intuitive Annahme wäre, dass die niedrigerebene Schleife schneller wäre. Lassen Sie uns über die Leistung sprechen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Improving Our I/O Project" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.