Übung zur Rust-Mustersyntax

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

In diesem Lab besprechen wir die gültige Syntax in Mustern und geben Beispiele dafür, wann und warum du eine bestimmte Syntax verwenden möchtest.

Pattern Syntax

In diesem Abschnitt sammeln wir alle Syntax, die in Mustern gültig ist, und besprechen, warum und wann du eine bestimmte Syntax verwenden möchtest.

Literale abgleichen

Wie du im Kapitel 6 gesehen hast, kannst du Muster direkt gegen Literale abgleichen. Der folgende Code gibt einige Beispiele:

Dateiname: src/main.rs

let x = 1;

match x {
    1 => println!("eins"),
    2 => println!("zwei"),
    3 => println!("drei"),
    _ => println!("irgendwas"),
}

Dieser Code druckt eins, weil der Wert in x 1 ist. Diese Syntax ist nützlich, wenn du möchtest, dass dein Code eine Aktion ausführt, wenn er einen bestimmten konkreten Wert erhält.

Benannte Variablen abgleichen

Benannte Variablen sind unwiderlegbare Muster, die jedem Wert entsprechen, und wir haben sie in diesem Buch bereits vielfach verwendet. Es gibt jedoch eine Komplikation, wenn du benannte Variablen in match-Ausdrücken verwendest. Da match einen neuen Gültigkeitsbereich startet, werden Variablen, die als Teil eines Musters innerhalb des match-Ausdrucks deklariert werden, die Variablen mit demselben Namen außerhalb des match-Konstrukts verdrängen, wie dies bei allen Variablen der Fall ist. In Listing 18-11 deklarieren wir eine Variable namens x mit dem Wert Some(5) und eine Variable y mit dem Wert 10. Anschließend erstellen wir einen match-Ausdruck für den Wert x. Schau dir die Muster in den match-Armen und die println! am Ende an und versuche, herauszufinden, was der Code ausgeben wird, bevor du diesen Code ausführst oder weiter liest.

Dateiname: src/main.rs

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

    match x {
      3 Some(50) => println!("Krieg 50"),
      4 Some(y) => println!("Übereinstimmung, y = {y}"),
      5 _ => println!("Standardfall, x = {:?}", x),
    }

  6 println!("am Ende: x = {:?}, y = {y}", x);
}

Listing 18-11: Ein match-Ausdruck mit einem Arm, der eine verdrängte Variable y einführt

Schauen wir uns an, was passiert, wenn der match-Ausdruck ausgeführt wird. Das Muster im ersten match-Arm [3] stimmt nicht mit dem definierten Wert von x [1] überein, also wird der Code fortgesetzt.

Das Muster im zweiten match-Arm [4] führt eine neue Variable namens y ein, die jedem Wert innerhalb eines Some-Werts entsprechen wird. Da wir uns in einem neuen Gültigkeitsbereich innerhalb des match-Ausdrucks befinden, handelt es sich um eine neue y-Variable, nicht um die y, die wir am Anfang mit dem Wert 10 deklariert haben [2]. Diese neue y-Bindung wird jedem Wert innerhalb eines Some entsprechen, was wir in x haben. Daher bindet diese neue y an den inneren Wert des Some in x. Dieser Wert ist 5, sodass der Ausdruck für diesen Arm ausgeführt wird und Übereinstimmung, y = 5 ausgibt.

Wenn x ein None-Wert anstelle von Some(5) gewesen wäre, wären die Muster in den ersten beiden Armen nicht übereinstimmend, sodass der Wert dem Unterstrich [5] entsprechen würde. Wir haben die x-Variable im Muster des Unterstrich-Arms nicht eingeführt, sodass die x im Ausdruck immer noch die äußere x ist, die nicht verdrängt wurde. In diesem hypothetischen Fall würde der match Standardfall, x = None ausgeben.

Wenn der match-Ausdruck abgeschlossen ist, endet auch sein Gültigkeitsbereich, und damit auch der Gültigkeitsbereich der inneren y. Die letzte println! [6] liefert am Ende: x = Some(5), y = 10.

Um einen match-Ausdruck zu erstellen, der die Werte der äußeren x und y vergleicht, anstatt eine verdrängte Variable zu introduzieren, müssten wir stattdessen einen match-Guard-Zusatzbedingung verwenden. Wir werden in "Zusätzliche Bedingungen mit match-Guards" über match-Guards sprechen.

Mehrere Muster

In match-Ausdrücken kannst du mehrere Muster mit der |-Syntax abgleichen, die der Muster-oder-Operator ist. Beispielsweise passen wir im folgenden Code den Wert von x gegen die match-Arme an. Der erste Arm hat eine oder-Option, was bedeutet, dass wenn der Wert von x einem der Werte in diesem Arm entspricht, der Code dieses Arms ausgeführt wird:

Dateiname: src/main.rs

let x = 1;

match x {
    1 | 2 => println!("eins oder zwei"),
    3 => println!("drei"),
    _ => println!("irgendwas"),
}

Dieser Code druckt eins oder zwei.

Wertebereiche mit..= abgleichen

Die ..=-Syntax ermöglicht es uns, auf einen inklusiven Wertebereich abzustimmen. Im folgenden Code wird, wenn ein Muster einem der Werte innerhalb des angegebenen Bereichs entspricht, der Arm ausgeführt:

Dateiname: src/main.rs

let x = 5;

match x {
    1..=5 => println!("eins bis fünf"),
    _ => println!("etwas anderes"),
}

Wenn x 1, 2, 3, 4 oder 5 ist, wird der erste Arm übereinstimmen. Diese Syntax ist für mehrere Übereinstimmungswerte bequemer als die Verwendung des |-Operators, um die gleiche Idee auszudrücken; wenn wir | verwenden würden, müssten wir 1 | 2 | 3 | 4 | 5 angeben. Das Angeben eines Bereichs ist viel kürzer, insbesondere wenn wir beispielsweise alle Zahlen zwischen 1 und 1.000 abgleichen möchten!

Der Compiler überprüft bei der Kompilierung, dass der Bereich nicht leer ist, und da die einzigen Typen, für die Rust feststellen kann, ob ein Bereich leer ist oder nicht, char und numerische Werte sind, sind nur numerische oder char-Werte mit Bereichen erlaubt.

Hier ist ein Beispiel mit char-Wertebereichen:

Dateiname: src/main.rs

let x = 'c';

match x {
    'a'..='j' => println!("früher ASCII-Buchstabe"),
    'k'..='z' => println!("später ASCII-Buchstabe"),
    _ => println!("etwas anderes"),
}

Rust kann erkennen, dass 'c' innerhalb des ersten Musters liegt, und druckt früher ASCII-Buchstabe.

Strukturzerlegung, um Werte aufzuteilen

Wir können auch Muster verwenden, um Structs, Enums und Tupel aufzuteilen, um verschiedene Teile dieser Werte zu verwenden. Schauen wir uns jeden Wert an.

Strukturzerlegung von Structs

Listing 18-12 zeigt eine Point-Struktur mit zwei Feldern, x und y, die wir mit einem Muster in einer let-Anweisung aufteilen können.

Dateiname: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Listing 18-12: Strukturzerlegung der Felder einer Struktur in separate Variablen

Dieser Code erstellt die Variablen a und b, die den Werten der x- und y-Felder der p-Struktur entsprechen. Dieses Beispiel zeigt, dass die Namen der Variablen im Muster nicht mit den Feldnamen der Struktur übereinstimmen müssen. Es ist jedoch üblich, die Variablennamen den Feldnamen zu entsprechen, um es einfacher zu 记住,welche Variablen aus welchen Feldern stammen. Aufgrund dieser üblichen Verwendung und weil das Schreiben von let Point { x: x, y: y } = p; viel Duplizität enthält, hat Rust eine Abkürzung für Muster, die Strukturfelder abgleichen: Du musst nur den Namen des Strukturfelds auflisten, und die aus dem Muster erstellten Variablen werden die gleichen Namen haben. Listing 18-13 verhält sich genauso wie der Code in Listing 18-12, aber die im let-Muster erstellten Variablen sind x und y anstelle von a und b.

Dateiname: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Listing 18-13: Strukturzerlegung von Strukturfeldern mit der Strukturfeldabkürzung

Dieser Code erstellt die Variablen x und y, die den x- und y-Feldern der p-Variablen entsprechen. Das Ergebnis ist, dass die Variablen x und y die Werte aus der p-Struktur enthalten.

Wir können auch mit Literalwerten strukturieren, als Teil des Strukturmusters, anstatt Variablen für alle Felder zu erstellen. Dadurch können wir einige der Felder auf bestimmte Werte testen, während wir Variablen erstellen, um die anderen Felder aufzuteilen.

In Listing 18-14 haben wir einen match-Ausdruck, der Point-Werte in drei Fälle aufteilt: Punkte, die direkt auf der x-Achse liegen (was dann zutrifft, wenn y = 0), auf der y-Achse (x = 0) oder auf keiner Achse.

Dateiname: src/main.rs

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("Auf der x-Achse bei {x}"),
        Point { x: 0, y } => println!("Auf der y-Achse bei {y}"),
        Point { x, y } => {
            println!("Auf keiner Achse: ({x}, {y})");
        }
    }
}

Listing 18-14: Strukturzerlegung und Abgleich von Literalwerten in einem Muster

Der erste Arm wird jeden Punkt auf der x-Achse abgleichen, indem er angibt, dass das y-Feld übereinstimmt, wenn sein Wert mit dem Literal 0 übereinstimmt. Das Muster erstellt immer noch eine x-Variable, die wir im Code für diesen Arm verwenden können.

Ähnlich übereinstimmt der zweite Arm jeden Punkt auf der y-Achse, indem er angibt, dass das x-Feld übereinstimmt, wenn sein Wert 0 ist, und erstellt eine Variable y für den Wert des y-Felds. Der dritte Arm gibt keine Literale an, sodass er jeden anderen Point übereinstimmt und Variablen für beide x- und y-Felder erstellt.

In diesem Beispiel stimmt der Wert p mit dem zweiten Arm überein, weil x einen 0 enthält, sodass dieser Code Auf der y-Achse bei 7 ausgeben wird.

Denke daran, dass ein match-Ausdruck aufhört, die Arme zu überprüfen, sobald er das erste übereinstimmende Muster gefunden hat. Daher würde dieser Code auch dann nur Auf der x-Achse bei 0 ausgeben, wenn Point { x: 0, y: 0} auf der x-Achse und der y-Achse liegt.

Strukturzerlegung von Enums

Wir haben in diesem Buch Enums strukturiert (z.B. Listing 6-5), aber wir haben bisher noch nicht explizit diskutiert, dass das Muster zur Strukturzerlegung eines Enums der Art entspricht, wie die darin gespeicherten Daten definiert sind. Als Beispiel verwenden wir in Listing 18-15 das Message-Enum aus Listing 6-2 und schreiben einen match mit Mustern, die jedes innere Element strukturieren werden.

Dateiname: src/main.rs

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
  1 let msg = Message::ChangeColor(0, 160, 255);

    match msg {
      2 Message::Quit => {
            println!(
                "Der Quit-Variante gibt es keine Daten, die strukturiert werden könnten."
            );
        }
      3 Message::Move { x, y } => {
            println!(
                "Bewegung in x-Richtung {x}, in y-Richtung {y}"
            );
        }
      4 Message::Write(text) => {
            println!("Textnachricht: {text}");
        }
      5 Message::ChangeColor(r, g, b) => println!(
            "Farbe ändern zu rot {r}, grün {g} und blau {b}"
        ),
    }
}

Listing 18-15: Strukturzerlegung von Enum-Varianten, die verschiedene Arten von Werten enthalten

Dieser Code wird Farbe ändern zu rot 0, grün 160 und blau 255 ausgeben. Versuche, den Wert von msg [1] zu ändern, um den Code aus den anderen Armen auszuführen.

Für Enum-Varianten ohne Daten, wie Message::Quit [2], können wir den Wert nicht weiter strukturieren. Wir können nur auf den Literalwert Message::Quit abgleichen, und in diesem Muster sind keine Variablen enthalten.

Für struct-ähnliche Enum-Varianten, wie Message::Move [3], können wir ein Muster verwenden, das ähnlich dem Muster ist, das wir verwenden, um Structs abzugleichen. Nach dem Variantenamen setzen wir geschweifte Klammern und listen dann die Felder mit Variablen auf, sodass wir die Teile auseinandernehmen, um sie im Code für diesen Arm zu verwenden. Hier verwenden wir die Abkürzung wie in Listing 18-13.

Für tuple-ähnliche Enum-Varianten, wie Message::Write, die ein Tuple mit einem Element enthält [4], und Message::ChangeColor, die ein Tuple mit drei Elementen enthält [5], ist das Muster ähnlich dem Muster, das wir verwenden, um Tuples abzugleichen. Die Anzahl der Variablen im Muster muss der Anzahl der Elemente in der Variante entsprechen, auf die wir abgleichen.

Strukturzerlegung geschachtelter Structs und Enums

Bisher haben unsere Beispiele alle nur auf Structs oder Enums eine Ebene tief abgestimmt, aber das Abgleichen funktioniert auch für geschachtelte Elemente! Beispielsweise können wir den Code in Listing 18-15 umgestalten, um RGB- und HSV-Farben in der ChangeColor-Nachricht zu unterstützen, wie in Listing 18-16 gezeigt.

Dateiname: src/main.rs

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Farbe ändern zu rot {r}, grün {g} und blau {b}"
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Farbe ändern zu Farbton {h}, Sättigung {s}, Helligkeit {v}"
        ),
        _ => (),
    }
}

Listing 18-16: Abgleichen von geschachtelten Enums

Das Muster des ersten Arms im match-Ausdruck passt auf eine Message::ChangeColor-Enum-Variante, die eine Color::Rgb-Variante enthält; dann bindet das Muster an die drei inneren i32-Werte. Das Muster des zweiten Arms passt ebenfalls auf eine Message::ChangeColor-Enum-Variante, aber die innere Enum passt auf Color::Hsv statt dessen. Wir können diese komplexen Bedingungen in einem einzigen match-Ausdruck angeben, auch wenn zwei Enums beteiligt sind.

Strukturzerlegung von Structs und Tupeln

Wir können Strukturzerlegungsmuster noch komplexer kombinieren, miteinander kombinieren und verschachteln. Das folgende Beispiel zeigt eine komplizierte Strukturzerlegung, bei der wir Structs und Tupel in einem Tupel verschachteln und alle primitiven Werte daraus extrahieren:

let ((feet, inches), Point { x, y }) =
    ((3, 10), Point { x: 3, y: -10 });

Dieser Code ermöglicht es uns, komplexe Typen in ihre Bestandteile aufzuteilen, sodass wir die Werte, an denen wir interessiert sind, separat verwenden können.

Die Verwendung von Mustern bei der Strukturzerlegung ist eine bequeme Methode, um Teile von Werten, wie z.B. den Wert aus jedem Feld in einem Struct, voneinander separat zu verwenden.

Überspringen von Werten in einem Muster

Du hast gesehen, dass es manchmal nützlich ist, Werte in einem Muster zu ignorieren, wie z.B. im letzten Arm eines match, um einen Fallback zu erhalten, der eigentlich nichts tut, aber alle verbleibenden möglichen Werte abdeckt. Es gibt einige Möglichkeiten, ganze Werte oder Teile von Werten in einem Muster zu ignorieren: Verwendung des _-Musters (das du schon kennst), Verwendung des _-Musters innerhalb eines anderen Musters, Verwendung eines Namens, der mit einem Unterstrich beginnt, oder Verwendung von .., um die verbleibenden Teile eines Werts zu ignorieren. Lass uns untersuchen, wie und warum man jedes dieser Muster verwendet.

Ein ganzes Element mit _

Wir haben das Unterstrich-Zeichen als Platzhalter-Muster verwendet, das jedem Wert entspricht, aber nicht an diesen gebunden wird. Dies ist besonders nützlich als letzter Arm in einem match-Ausdruck, aber wir können es auch in jedem Muster verwenden, einschließlich Funktionsparameter, wie in Listing 18-17 gezeigt.

Dateiname: src/main.rs

fn foo(_: i32, y: i32) {
    println!("Dieser Code verwendet nur den y-Parameter: {y}");
}

fn main() {
    foo(3, 4);
}

Listing 18-17: Verwendung von _ in einer Funktionssignatur

Dieser Code ignoriert vollständig den Wert 3, der als erster Argument übergeben wird, und gibt Dieser Code verwendet nur den y-Parameter: 4 aus.

In den meisten Fällen, wenn Sie einen bestimmten Funktionsparameter nicht mehr benötigen, würden Sie die Signatur ändern, sodass der nicht verwendete Parameter nicht mehr enthalten ist. Das Ignorieren eines Funktionsparameters kann besonders nützlich sein, wenn Sie beispielsweise ein Merkmal implementieren und dabei eine bestimmte Typsignatur benötigen, aber der Funktionskörper in Ihrer Implementierung einen der Parameter nicht benötigt. Dadurch vermeiden Sie eine Compiler-Warnung über nicht verwendete Funktionsparameter, die Sie erhalten würden, wenn Sie stattdessen einen Namen verwenden würden.

Teile eines Werts mit einem geschachtelten _

Wir können auch _ innerhalb eines anderen Musters verwenden, um nur einen Teil eines Werts zu ignorieren. Beispielsweise, wenn wir nur einen Teil eines Werts testen möchten, aber die anderen Teile in dem entsprechenden Code, den wir ausführen möchten, nicht benötigen. Listing 18-18 zeigt den Code, der für das Verwalten eines Einstellungs-Werts verantwortlich ist. Die geschäftlichen Anforderungen sind, dass der Benutzer nicht in der Lage sein soll, eine vorhandene Anpassung einer Einstellung zu überschreiben, aber die Einstellung aufheben und ihr einen Wert zuweisen kann, wenn sie derzeit nicht gesetzt ist.

Dateiname: src/main.rs

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Kann einen vorhandenen benutzerdefinierten Wert nicht überschreiben");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("Einstellung ist {:?}", setting_value);

Listing 18-18: Verwendung eines Unterstrich-Zeichens innerhalb von Mustern, die Some-Varianten abgleichen, wenn wir den Wert innerhalb von Some nicht verwenden müssen

Dieser Code wird Kann einen vorhandenen benutzerdefinierten Wert nicht überschreiben ausgeben und dann Einstellung ist Some(5). Im ersten match-Arm müssen wir nicht auf die Werte innerhalb der Some-Variante abgleichen oder verwenden, aber wir müssen den Fall testen, wenn setting_value und new_setting_value die Some-Variante sind. In diesem Fall geben wir den Grund aus, warum setting_value nicht geändert wird, und es wird nicht geändert.

In allen anderen Fällen (wenn entweder setting_value oder new_setting_value None ist), die durch das _-Muster im zweiten Arm ausgedrückt werden, möchten wir, dass new_setting_value setting_value wird.

Wir können auch Unterstrich-Zeichen an mehreren Stellen innerhalb eines Musters verwenden, um bestimmte Werte zu ignorieren. Listing 18-19 zeigt ein Beispiel dafür, wie der zweite und vierte Wert in einem Tupel aus fünf Elementen ignoriert werden.

Dateiname: src/main.rs

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Einige Zahlen: {first}, {third}, {fifth}");
    }
}

Listing 18-19: Ignorieren mehrerer Teile eines Tupels

Dieser Code wird Einige Zahlen: 2, 8, 32 ausgeben, und die Werte 4 und 16 werden ignoriert.

Eine nicht genutzte Variable, indem man ihren Namen mit einem Unterstrich beginnt

Wenn Sie eine Variable erstellen, aber sie nirgends verwenden, wird Rust normalerweise eine Warnung ausgeben, da eine nicht genutzte Variable ein Fehler sein könnte. Manchmal ist es jedoch nützlich, eine Variable zu erstellen, die man noch nicht verwenden möchte, z.B. wenn man prototypiert oder gerade ein Projekt beginnt. In dieser Situation können Sie Rust mitteilen, dass Sie keine Warnung über die nicht genutzte Variable erhalten möchten, indem Sie den Variablennamen mit einem Unterstrich beginnen. In Listing 18-20 erstellen wir zwei nicht genutzte Variablen, aber wenn wir diesen Code kompilieren, sollten wir nur eine Warnung über eine von ihnen erhalten.

Dateiname: src/main.rs

fn main() {
    let _x = 5;
    let y = 10;
}

Listing 18-20: Beginnen eines Variablennamens mit einem Unterstrich, um Warnungen über nicht genutzte Variablen zu vermeiden

Hier erhalten wir eine Warnung darüber, dass die Variable y nicht verwendet wird, aber keine Warnung darüber, dass _x nicht verwendet wird.

Beachten Sie, dass es einen subtilen Unterschied zwischen der Verwendung von nur _ und der Verwendung eines Namens, der mit einem Unterstrich beginnt, gibt. Die Syntax _x bindet immer noch den Wert an die Variable, während _ überhaupt nicht bindet. Um einen Fall zu zeigen, in dem diese Unterscheidung wichtig ist, wird Listing 18-21 uns einen Fehler liefern.

Dateiname: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("found a string");
}

println!("{:?}", s);

Listing 18-21: Eine nicht genutzte Variable, die mit einem Unterstrich beginnt, bindet immer noch den Wert, was möglicherweise die Eigentumsgewalt über den Wert erlangt.

Wir erhalten einen Fehler, da der Wert von s immer noch in _s bewegt wird, was es uns verhindert, s erneut zu verwenden. Verwenden Sie jedoch das Unterstrich-Zeichen allein, umbindet es niemals an den Wert. Listing 18-22 wird ohne Fehler kompiliert, da s nicht in _ bewegt wird.

Dateiname: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{:?}", s);

Listing 18-22: Verwenden eines Unterstrich-Zeichens bindet nicht den Wert.

Dieser Code funktioniert einwandfrei, da wir s niemals an etwas binden; es wird nicht bewegt.

Verbleibende Teile eines Werts mit ..

Bei Werten, die viele Teile haben, können wir die Syntax .. verwenden, um bestimmte Teile zu nutzen und den Rest zu ignorieren, wodurch es nicht mehr erforderlich ist, für jeden ignorierten Wert ein Unterstrich anzugeben. Das ..-Muster ignoriert alle Teile eines Werts, die wir in anderen Teilen des Musters nicht explizit abgleichen. In Listing 18-23 haben wir eine Point-Struktur, die eine Koordinate im dreidimensionalen Raum speichert. Im match-Ausdruck möchten wir nur auf die x-Koordinate operieren und die Werte in den y- und z-Feldern ignorieren.

Dateiname: src/main.rs

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

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x,.. } => println!("x ist {x}"),
}

Listing 18-23: Ignorieren aller Felder einer Point außer x durch Verwendung von ..

Wir listieren den x-Wert und verwenden dann einfach das ..-Muster. Dies ist schneller als es wäre, y: _ und z: _ aufzuschreiben, insbesondere wenn wir mit Strukturen arbeiten, die viele Felder haben und nur ein oder zwei Felder relevant sind.

Die Syntax .. wird so weit erweitert, wie es erforderlich ist. Listing 18-24 zeigt, wie man .. mit einem Tupel verwendet.

Dateiname: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first,.., last) => {
            println!("Einige Zahlen: {first}, {last}");
        }
    }
}

Listing 18-24: Nur die ersten und letzten Werte in einem Tupel abgleichen und alle anderen Werte ignorieren

In diesem Code werden die ersten und letzten Werte mit first und last abgleicht. Das .. wird alle Werte dazwischen abgleichen und ignorieren.

Wir müssen jedoch beachten, dass die Verwendung von .. eindeutig sein muss. Wenn unklar ist, welche Werte für die Zuordnung bestimmt sind und welche ignoriert werden sollen, wird Rust uns einen Fehler geben. Listing 18-25 zeigt ein Beispiel für eine zweideutige Verwendung von .., sodass der Code nicht kompilieren wird.

Dateiname: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second,..) => {
            println!("Einige Zahlen: {second}");
        },
    }
}

Listing 18-25: Ein Versuch, .. auf eine zweideutige Weise zu verwenden

Wenn wir diesen Code kompilieren, erhalten wir diesen Fehler:

error: `..` kann nur einmal pro Tupel-Muster verwendet werden
 --> src/main.rs:5:22
  |
5 |         (.., second,..) => {
  |          --          ^^ kann nur einmal pro Tupel-Muster verwendet werden
  |          |
  |          zuvor hier verwendet

Es ist für Rust nicht möglich, zu bestimmen, wie viele Werte im Tupel ignoriert werden sollen, bevor ein Wert mit second abgeglichen wird, und dann wie viele weitere Werte danach ignoriert werden sollen. Dieser Code könnte bedeuten, dass wir 2 ignorieren möchten, second an 4 binden und dann 8, 16 und 32 ignorieren; oder dass wir 2 und 4 ignorieren möchten, second an 8 binden und dann 16 und 32 ignorieren; und so weiter. Der Variablennamen second hat für Rust keine besondere Bedeutung, daher erhalten wir einen Compilerfehler, da das Verwenden von .. an zwei Stellen so zweideutig ist.

Zusätzliche bedingte Anweisungen mit Match Guards

Ein Match Guard ist eine zusätzliche if-Bedingung, die nach dem Muster in einem match-Arm angegeben wird und die ebenfalls zutreffen muss, damit dieser Arm ausgewählt wird. Match Guards sind hilfreich, um komplexere Konzepte auszudrücken, als dies allein ein Muster erlaubt.

Die Bedingung kann Variablen verwenden, die im Muster erstellt werden. Listing 18-26 zeigt ein match, bei dem der erste Arm das Muster Some(x) hat und auch einen Match Guard von if x % 2 == 0 (was true ist, wenn die Zahl gerade ist).

Dateiname: src/main.rs

let num = Some(4);

match num {
    Some(x) if x % 2 == 0 => println!("Die Zahl {x} ist gerade"),
    Some(x) => println!("Die Zahl {x} ist ungerade"),
    None => (),
}

Listing 18-26: Hinzufügen eines Match Guards zu einem Muster

Dieses Beispiel wird Die Zahl 4 ist gerade ausgeben. Wenn num mit dem Muster im ersten Arm verglichen wird, stimmt es überein, weil Some(4) mit Some(x) übereinstimmt. Anschließend überprüft der Match Guard, ob der Rest bei der Division von x durch 2 gleich 0 ist, und da dies der Fall ist, wird der erste Arm ausgewählt.

Wenn num stattdessen Some(5) gewesen wäre, wäre der Match Guard im ersten Arm false gewesen, weil der Rest bei der Division von 5 durch 2 1 ist, was nicht gleich 0 ist. Rust würde dann zum zweiten Arm gehen, der übereinstimmen würde, weil der zweite Arm keinen Match Guard hat und daher jeder Some-Variante entspricht.

Es gibt keine Möglichkeit, die Bedingung if x % 2 == 0 innerhalb eines Musters auszudrücken, daher ermöglicht uns der Match Guard, diese Logik auszudrücken. Der Nachteil dieser zusätzlichen Ausdrucksfähigkeit ist, dass der Compiler nicht versucht, die Vollständigkeit zu überprüfen, wenn Match Guard-Ausdrücke beteiligt sind.

In Listing 18-11 haben wir erwähnt, dass wir Match Guards verwenden können, um unser Pattern-Shadowing-Problem zu lösen. Erinnern Sie sich, dass wir eine neue Variable innerhalb des Musters im match-Ausdruck erstellt haben, anstatt die Variable außerhalb des match zu verwenden. Diese neue Variable bedeutete, dass wir nicht gegen den Wert der äußeren Variable testen konnten. Listing 18-27 zeigt, wie wir einen Match Guard verwenden können, um dieses Problem zu beheben.

Dateiname: src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

Listing 18-27: Verwenden eines Match Guards, um auf Gleichheit mit einer äußeren Variable zu testen

Dieser Code wird jetzt Default case, x = Some(5) ausgeben. Das Muster im zweiten match-Arm führt keine neue Variable y ein, die die äußere y überdecken würde, was bedeutet, dass wir die äußere y im Match Guard verwenden können. Anstatt das Muster als Some(y) anzugeben, was die äußere y überdecken würde, geben wir Some(n) an. Dies erstellt eine neue Variable n, die nichts überdeckt, da es keine n-Variable außerhalb des match gibt.

Der Match Guard if n == y ist kein Muster und führt daher keine neuen Variablen ein. Diese y ist die äußere y, statt eine neue überdeckte y, und wir können nach einem Wert suchen, der denselben Wert wie die äußere y hat, indem wir n mit y vergleichen.

Sie können auch den oder-Operator | in einem Match Guard verwenden, um mehrere Muster anzugeben; die Match Guard-Bedingung wird auf alle Muster angewandt. Listing 18-28 zeigt die Priorität, wenn ein Muster, das | verwendet, mit einem Match Guard kombiniert wird. Der wichtigste Teil dieses Beispiels ist, dass der if y-Match Guard auf 4, 5 und 6 anwendet, obwohl es so aussehen könnte, als würde if y nur auf 6 angewandt werden.

Dateiname: src/main.rs

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

Listing 18-28: Kombinieren mehrerer Muster mit einem Match Guard

Die Match-Bedingung besagt, dass der Arm nur dann übereinstimmt, wenn der Wert von x gleich 4, 5 oder 6 ist und wenn y true ist. Wenn dieser Code ausgeführt wird, stimmt das Muster des ersten Arms überein, weil x 4 ist, aber der Match Guard if y ist false, daher wird der erste Arm nicht ausgewählt. Der Code springt zum zweiten Arm, der übereinstimmt, und dieses Programm gibt no aus. Der Grund ist, dass die if-Bedingung auf das gesamte Muster 4 | 5 | 6 anwendet, nicht nur auf den letzten Wert 6. Mit anderen Worten, die Priorität eines Match Guards im Bezug auf ein Muster verhält sich so:

(4 | 5 | 6) if y =>...

statt so:

4 | 5 | (6 if y) =>...

Nachdem der Code ausgeführt wurde, ist das Prioritätsverhalten offensichtlich: Wenn der Match Guard nur auf den letzten Wert in der Liste von Werten, die mit dem |-Operator angegeben werden, angewendet würde, wäre der Arm übereingestimmt und das Programm hätte yes ausgegeben.

@-Bindungen

Der at-Operator @ ermöglicht es uns, eine Variable zu erstellen, die einen Wert speichert, während wir diesen Wert auf einen Muster-Vergleich testen. In Listing 18-29 möchten wir testen, ob das id-Feld eines Message::Hello im Bereich 3..=7 liegt. Wir möchten auch den Wert an die Variable id_variable binden, damit wir ihn im mit dem Arm assoziierten Code verwenden können. Wir könnten diese Variable id nennen, wie das Feld, aber für dieses Beispiel verwenden wir einen anderen Namen.

Dateiname: src/main.rs

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("Gefunden eine ID im Bereich: {id_variable}"),
    Message::Hello { id: 10..=12 } => {
        println!("Gefunden eine ID in einem anderen Bereich")
    }
    Message::Hello { id } => println!("Eine andere ID: {id}"),
}

Listing 18-29: Verwenden von @ zum Binden an einen Wert in einem Muster und zum Testen desselben

Dieses Beispiel wird Gefunden eine ID im Bereich: 5 ausgeben. Indem wir id_variable @ vor dem Bereich 3..=7 angeben, fangen wir den Wert ein, der dem Bereich entspricht, und testen gleichzeitig, ob der Wert dem Bereichsmuster entspricht.

Im zweiten Arm, wo wir nur einen Bereich im Muster angegeben haben, hat der mit dem Arm assoziierte Code keine Variable, die den tatsächlichen Wert des id-Felds enthält. Der Wert des id-Felds könnte 10, 11 oder 12 sein, aber der Code, der mit diesem Muster zusammenhängt, weiß nicht, welcher Wert es ist. Der Muster-Code kann den Wert aus dem id-Feld nicht verwenden, weil wir den id-Wert nicht in einer Variable gespeichert haben.

Im letzten Arm, wo wir eine Variable ohne Bereich angegeben haben, haben wir den Wert in der Variable id im Code des Arms zur Verfügung. Der Grund ist, dass wir die Kurzschreibweise für Strukturfelder verwendet haben. Aber wir haben in diesem Arm keinen Test auf den Wert im id-Feld durchgeführt, wie wir es in den ersten beiden Armen getan haben: jeder Wert würde diesem Muster entsprechen.

Das Verwenden von @ ermöglicht es uns, einen Wert zu testen und ihn in einer Variable innerhalb eines Musters zu speichern.

Zusammenfassung

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