Der Match Control Flow Construct

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 The Match Control Flow Construct. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir das leistungsstarke match-Steuerflusskonstrukt in Rust erkunden, das das Musterabgleich und die Ausführung von Code basierend auf dem gematchten Muster ermöglicht.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100399{{"Der Match Control Flow Construct"}} rust/integer_types -.-> lab-100399{{"Der Match Control Flow Construct"}} rust/function_syntax -.-> lab-100399{{"Der Match Control Flow Construct"}} rust/expressions_statements -.-> lab-100399{{"Der Match Control Flow Construct"}} rust/method_syntax -.-> lab-100399{{"Der Match Control Flow Construct"}} rust/operator_overloading -.-> lab-100399{{"Der Match Control Flow Construct"}} end

Das match-Steuerflusskonstrukt

Rust hat ein extrem leistungsstarkes Steuerflusskonstrukt namens match, das es Ihnen ermöglicht, einen Wert mit einer Reihe von Mustern zu vergleichen und dann basierend auf dem passenden Muster Code auszuführen. Muster können aus Literalwerten, Variablennamen, Platzhaltern und vielen anderen Dingen bestehen; Kapitel 18 behandelt alle verschiedenen Arten von Mustern und was sie tun. Die Stärke von match kommt von der Ausdrucksfähigkeit der Muster und der Tatsache, dass der Compiler bestätigt, dass alle möglichen Fälle behandelt werden.

Denken Sie sich einen match-Ausdruck wie eine Münzsortieranlage: Münzen gleiten entlang einer Schiene mit verschieden großen Löchern, und jede Münze fällt durch das erste Loch, das sie passt. Auf die gleiche Weise gehen Werte durch jedes Muster in einem match, und bei dem ersten Muster, das der Wert "passt", fällt der Wert in den zugehörigen Codeblock, um während der Ausführung verwendet zu werden.

Reden wir von Münzen, lassen Sie uns sie als Beispiel mit match verwenden! Wir können eine Funktion schreiben, die eine unbekannte US-Münze annimmt und, ähnlich wie die Zählmaschine, bestimmt, welche Münze es ist und ihren Wert in Cent zurückgibt, wie in Listing 6-3 gezeigt.

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Listing 6-3: Ein Enum und ein match-Ausdruck, der die Varianten des Enums als Muster verwendet

Zerlegen wir das match in der value_in_cents-Funktion. Zunächst listieren wir das match-Schlüsselwort, gefolgt von einem Ausdruck, der in diesem Fall der Wert coin ist [2]. Dies sieht sehr ähnlich aus wie ein Ausdruck, der mit if verwendet wird, aber es gibt einen großen Unterschied: mit if muss der Ausdruck einen booleschen Wert zurückgeben, hier kann er jedoch jeden beliebigen Typ zurückgeben. Der Typ von coin in diesem Beispiel ist das Coin-Enum, das wir bei [1] definiert haben.

Als nächstes kommen die match-Arme. Ein Arm hat zwei Teile: ein Muster und etwas Code. Der erste Arm hier hat ein Muster, das der Wert Coin::Penny ist, und dann den =>-Operator, der das Muster und den auszuführenden Code trennt [3]. Der Code in diesem Fall ist einfach der Wert 1. Jeder Arm wird von dem nächsten mit einem Komma getrennt.

Wenn der match-Ausdruck ausgeführt wird, wird der resultierende Wert nacheinander mit dem Muster jedes Arms verglichen. Wenn ein Muster mit dem Wert übereinstimmt, wird der zu diesem Muster gehörende Code ausgeführt. Wenn das Muster nicht mit dem Wert übereinstimmt, wird die Ausführung zum nächsten Arm fortgesetzt, ähnlich wie in einer Münzsortieranlage. Wir können so viele Arme wie wir benötigen haben: in Listing 6-3 hat unser match vier Arme.

Der mit jedem Arm assoziierte Code ist ein Ausdruck, und der resultierende Wert des Ausdrucks im passenden Arm ist der Wert, der für den gesamten match-Ausdruck zurückgegeben wird.

Wir verwenden normalerweise keine geschweiften Klammern, wenn der match-Arm-Code kurz ist, wie in Listing 6-3, wo jeder Arm einfach einen Wert zurückgibt. Wenn Sie in einem match-Arm mehrere Zeilen Code ausführen möchten, müssen Sie geschweifte Klammern verwenden, und das Komma nach dem Arm ist dann optional. Beispielsweise druckt der folgende Code "Glückspfennig!" jedes Mal, wenn die Methode mit einem Coin::Penny aufgerufen wird, gibt aber immer noch den letzten Wert des Blocks, 1, zurück:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Muster, die an Werte binden

Ein weiterer nützlicher Aspekt von match-Armen ist, dass sie an die Teile der Werte binden können, die mit dem Muster übereinstimmen. So können wir Werte aus Enum-Varianten extrahieren.

Als Beispiel ändern wir eine unserer Enum-Varianten, um Daten darin zu speichern. Zwischen 1999 und 2008 prägte die Vereinigten Staaten fünfundzwanzig-Cent-Münzen mit unterschiedlichen Designs für jede der 50 Bundesstaaten auf einer Seite. Keine anderen Münzen erhielten Designs für die Bundesstaaten, sodass nur die fünfundzwanzig-Cent-Münzen diesen zusätzlichen Wert haben. Wir können diese Information zu unserem enum hinzufügen, indem wir die Quarter-Variante ändern, um einen UsState-Wert darin zu speichern, was wir in Listing 6-4 getan haben.

#[derive(Debug)] // damit wir den Zustand in einem Moment überprüfen können
enum UsState {
    Alabama,
    Alaska,
    --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

Listing 6-4: Ein Coin-Enum, in dem die Quarter-Variante auch einen UsState-Wert speichert

Stellen Sie sich vor, dass ein Freund versucht, alle 50 Bundesstaat-Fünfundzwanzig-Cent-Münzen zu sammeln. Während wir unser Losegeld nach Münztyp sortieren, werden wir auch den Namen des Bundesstaates nennen, der mit jeder fünfundzwanzig-Cent-Münze assoziiert ist, damit, wenn es eine Münze ist, die unser Freund nicht hat, er sie zu seiner Sammlung hinzufügen kann.

Im match-Ausdruck für diesen Code fügen wir eine Variable namens state zum Muster hinzu, das Werte der Variante Coin::Quarter übereinstimmt. Wenn ein Coin::Quarter übereinstimmt, wird die Variable state an den Wert des Bundesstaates dieser fünfundzwanzig-Cent-Münze gebunden. Dann können wir state im Code für diesen Arm verwenden, wie folgt:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

Wenn wir value_in_cents(Coin::Quarter(UsState::Alaska)) aufrufen würden, wäre coin Coin::Quarter(UsState::Alaska). Wenn wir diesen Wert mit jedem der match-Arme vergleichen, stimmen keiner von ihnen überein, bis wir Coin::Quarter(state) erreichen. An diesem Punkt wird die Bindung für state der Wert UsState::Alaska sein. Wir können dann diese Bindung im println!-Ausdruck verwenden und so den inneren Bundesstaat-Wert aus der Coin-Enum-Variante für Quarter herausholen.

Matching mit Option<T>{=html}

Im vorherigen Abschnitt wollten wir den inneren T-Wert aus dem Some-Fall herausholen, wenn wir Option<T> verwenden; wir können Option<T> auch mit match behandeln, wie wir es mit dem Coin-Enum gemacht haben! Anstatt Münzen zu vergleichen, werden wir die Varianten von Option<T> vergleichen, aber die Art, wie der match-Ausdruck funktioniert, bleibt dieselbe.

Angenommen, wir möchten eine Funktion schreiben, die ein Option<i32> annimmt und, wenn ein Wert darin vorhanden ist, 1 zum Wert hinzufügt. Wenn kein Wert darin vorhanden ist, sollte die Funktion den None-Wert zurückgeben und keine weiteren Operationen ausführen.

Diese Funktion ist dank match sehr einfach zu schreiben und wird wie in Listing 6-5 aussehen.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
      1 None => None,
      2 Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

Listing 6-5: Eine Funktion, die einen match-Ausdruck auf einem Option<i32> verwendet

Betrachten wir die erste Ausführung von plus_one genauer. Wenn wir plus_one(five) aufrufen [3], hat die Variable x im Körper von plus_one den Wert Some(5). Wir vergleichen das dann mit jedem match-Arm:

None => None,

Der Wert Some(5) stimmt nicht mit dem Muster None überein [1], also gehen wir zum nächsten Arm weiter:

Some(i) => Some(i + 1),

Stimmt Some(5) mit Some(i) überein [2]? Ja, stimmt! Wir haben die gleiche Variante. Die Variable i bindet sich an den in Some enthaltenen Wert, sodass i den Wert 5 annimmt. Der Code im match-Arm wird dann ausgeführt, sodass wir 1 zum Wert von i addieren und einen neuen Some-Wert mit unserem Gesamtwert 6 darin erzeugen.

Betrachten wir nun den zweiten Aufruf von plus_one in Listing 6-5, bei dem x None ist [4]. Wir betreten den match und vergleichen ihn mit dem ersten Arm [1].

Es stimmt überein! Es gibt keinen Wert, dem etwas hinzugefügt werden soll, also stoppt das Programm und gibt den None-Wert auf der rechten Seite von => zurück. Da der erste Arm übereinstimmt, werden keine anderen Arme verglichen.

Das Zusammenführen von match und Enums ist in vielen Situationen nützlich. Sie werden dieses Muster in Rust-Code häufig sehen: match gegen ein Enum, binden Sie eine Variable an die darin enthaltenen Daten und führen Sie dann basierend darauf Code aus. Es ist zunächst ein bisschen tricky, aber wenn Sie sich daran gewöhnen, wünschen Sie sich es in allen Sprachen. Es ist immer ein Lieblingsfeature der Benutzer.

Matches sind erschöpfend

Es gibt noch einen anderen Aspekt von match, über den wir sprechen müssen: Die Muster der Arme müssen alle Möglichkeiten abdecken. Betrachten Sie diese Version unserer plus_one-Funktion, die einen Fehler hat und nicht kompilieren wird:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

Wir haben den None-Fall nicht behandelt, sodass dieser Code einen Fehler verursachen wird. Zum Glück ist es ein Fehler, den Rust erkennen kann. Wenn wir diesen Code versuchen, zu kompilieren, erhalten wir diesen Fehler:

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
  note: `Option<i32>` defined here
      = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding
a match arm with a wildcard pattern or an explicit pattern as
shown
    |
4   ~             Some(i) => Some(i + 1),
5   ~             None => todo!(),
    |

Rust weiß, dass wir nicht jede mögliche Möglichkeit abgedeckt haben und sogar weiß, welches Muster wir vergessen haben! Matches in Rust sind erschöpfend: Wir müssen jede letzte Möglichkeit erschöpfen, damit der Code gültig ist. Vor allem im Fall von Option<T>, wenn Rust uns daran hindert, zu vergessen, den None-Fall explizit zu behandeln, schützt es uns davor, anzunehmen, dass wir einen Wert haben, wenn wir möglicherweise null haben, und macht somit den früher diskutierten Fehler in Milliardenhöhe unmöglich.

Allfälle-Muster und das _ Platzhalter

Mit Enums können wir auch spezielle Aktionen für einige bestimmte Werte durchführen, aber für alle anderen Werte eine Standardaktion ausführen. Stellen Sie sich vor, dass wir ein Spiel implementieren, bei dem, wenn Sie bei einem Würfelwurf eine 3 werfen, Ihr Spieler nicht bewegt wird, sondern stattdessen einen neuen fancy Hut bekommt. Wenn Sie eine 7 werfen, verliert Ihr Spieler einen fancy Hut. Für alle anderen Werte bewegt sich Ihr Spieler um die entsprechende Anzahl von Feldern auf der Spielfläche. Hier ist ein match, das diese Logik implementiert, wobei das Ergebnis des Würfelwurfs hartcodiert ist, anstatt ein zufälliger Wert, und alle anderen Logiken durch Funktionen ohne Körper dargestellt werden, da deren tatsächliche Implementierung für dieses Beispiel außerhalb des Rahmens liegt:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Für die ersten beiden Arme sind die Muster die Literalwerte 3 und 7. Für den letzten Arm, der alle anderen möglichen Werte abdeckt, ist das Muster die Variable, die wir other genannt haben [1]. Der Code, der für den other-Arm ausgeführt wird, verwendet die Variable, indem er sie der move_player-Funktion übergibt.

Dieser Code kompiliert, obwohl wir nicht alle möglichen Werte aufgelistet haben, die ein u8 haben kann, weil das letzte Muster alle Werte abdecken wird, die nicht speziell aufgelistet sind. Dieses Allfälle-Muster erfüllt die Anforderung, dass match erschöpfend sein muss. Beachten Sie, dass wir den Allfälle-Arm am Ende platzieren müssen, da die Muster in Reihenfolge ausgewertet werden. Wenn wir den Allfälle-Arm früher platzieren würden, würden die anderen Arme niemals ausgeführt werden, daher wird uns Rust warnen, wenn wir Arme nach einem Allfälle-Arm hinzufügen!

Rust hat auch ein Muster, das wir verwenden können, wenn wir einen Allfälle-Arm möchten, aber den Wert im Allfälle-Muster nicht verwenden möchten: _ ist ein spezielles Muster, das jedem Wert entspricht und nicht an diesen Wert gebunden wird. Dies sagt Rust aus, dass wir den Wert nicht verwenden werden, sodass Rust uns nicht wegen einer nicht verwendeten Variable warnen wird.

Ändern wir die Spielregeln: Wenn Sie jetzt etwas anderes als eine 3 oder eine 7 werfen, müssen Sie erneut werfen. Wir brauchen den Allfälle-Wert nicht mehr, daher können wir unseren Code ändern, um _ statt der Variablen other zu verwenden:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

Dieses Beispiel erfüllt auch die Erschöpfendkeitsanforderung, da wir in letzterem Arm explizit alle anderen Werte ignorieren; wir haben nichts vergessen.

Schließlich ändern wir die Spielregeln noch einmal, sodass nichts weiter passiert, wenn Sie etwas anderes als eine 3 oder eine 7 werfen. Wir können das ausdrücken, indem wir den Einheitswert (der leere Tupeltyp, den wir in "Der Tupeltyp" erwähnt haben) als Code verwenden, der mit dem _-Arm zusammenhängt:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

Hier sagen wir Rust explizit aus, dass wir keinen anderen Wert verwenden werden, der nicht mit einem Muster in einem früheren Arm übereinstimmt, und dass wir in diesem Fall keinen Code ausführen möchten.

Es gibt noch mehr zu Mustern und Matching, das wir in Kapitel 18 behandeln werden. Für jetzt werden wir zur if let-Syntax übergehen, die in Situationen nützlich sein kann, in denen der match-Ausdruck etwas umständlich ist.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab The Match Control Flow Construct abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.