Übersicht über Musterabgleiche

Ein Musterabgleich ist ein Verfahren, bei dem Sie einen Ausdruck testen, um zu ermitteln, ob er bestimmte Merkmale aufweist. Der C#-Musterabgleich bietet eine präzisere Syntax zum Testen von Ausdrücken und zum Durchführen von Aktionen, wenn für einen Ausdruck eine Übereinstimmung gefunden wird. Der is-Ausdruck unterstützt Musterabgleiche, um einen Ausdruck zu testen und abhängig vom Ergebnis dieses Ausdrucks eine neue Variable zu deklarieren. Mit dem switch-Ausdruck können Sie Aktionen basierend auf dem ersten übereinstimmenden Muster für einen Ausdruck ausführen. Diese beiden Ausdrücke unterstützen ein umfangreiches Vokabular von Mustern.

Dieser Artikel bietet eine Übersicht über Szenarien, in denen Sie Musterabgleiche verwenden können. Diese Techniken können die Lesbarkeit und Korrektheit Ihres Codes verbessern. Eine vollständige Erörterung aller anwendbaren Muster finden Sie im Artikel zu Mustern in der Sprachreferenz.

NULL-Überprüfungen

Eines der gängigsten Szenarien für den Musterabgleich besteht darin, sicherzustellen, dass die Werte nicht null sind. Sie können einen Werttyp, der Nullwerte zulässt, testen und in den zugrunde liegenden Typ konvertieren und dabei mithilfe des folgenden Beispiels auf null-Werte testen:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

Der vorangehende Code ist ein Deklarationsmuster zum Testen des Variablentyps und zum Zuweisen dieses Typs zu einer neuen Variable. Aufgrund der Sprachregeln ist dieses Verfahren sicherer als viele andere. Der Zugriff auf die Variable number und ihre Zuweisung sind nur im TRUE-Teil der if-Klausel möglich. Wenn Sie versuchen, an anderer Stelle darauf zuzugreifen (in der else-Klausel oder nach dem if-Block), gibt der Compiler einen Fehler aus. Da Sie außerdem den ==-Operator nicht verwenden, funktioniert dieses Muster auch, wenn der ==-Operator durch einen Typ überladen wird. Daher eignet sich dieses Verfahren ideal für die Überprüfung auf Werte mit Nullverweis, indem das not-Muster hinzugefügt wird:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

Im vorherigen Beispiel wurde ein konstantes Muster verwendet, um die Variable mit null zu vergleichen. not ist ein logisches Muster, das übereinstimmt, wenn das negierte Muster nicht übereinstimmt.

Typtests

Eine weitere gängige Verwendung für den Musterabgleich sind Variablentests, mit denen festgestellt werden soll, ob die Variable mit einem bestimmten Typ übereinstimmt. Der folgende Code testet beispielsweise, ob eine Variable nicht NULL ist. Er implementiert die System.Collections.Generic.IList<T>-Schnittstelle. Wenn dies der Fall ist, wird die ICollection<T>.Count-Eigenschaft der betreffenden Liste verwendet, um den mittleren Index zu ermitteln. Das Deklarationsmuster führt nicht zu einer Übereinstimmung mit einem null-Wert, unabhängig vom Kompilierzeittyp der Variable. Der folgende Code schützt vor null und vor Typen, die IList nicht implementieren.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Dieselben Tests können in einem switch-Ausdruck angewandt werden, um eine Variable auf mehrere verschiedene Typen zu testen. Mithilfe dieser Informationen können Sie bessere Algorithmen basierend auf dem spezifischen Laufzeittyp erstellen.

Vergleichen diskreter Werte

Sie können eine Variable auch testen, um Übereinstimmungen mit bestimmten Werten zu finden. Der folgende Code zeigt ein Beispiel, in dem Sie einen Wert auf alle Werte testen, die in einer Enumeration deklariert sind:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

Im vorherigen Beispiel wird eine Methodenverteilung basierend auf dem Wert einer Enumeration veranschaulicht. Der abschließende _-Fall ist ein Ausschussmuster, das mit allen Werten übereinstimmt. Damit werden alle Fehlerbedingungen behandelt, bei denen der Wert mit keinem der definierten enum-Werte übereinstimmt. Wenn Sie diesen switch-Zweig weglassen, gibt der Compiler eine Warnung aus, dass der Musterausdruck nicht alle möglichen Eingabewerte verarbeitet. Zur Laufzeit löst der switch-Ausdruck eine Ausnahme aus, wenn das untersuchte Objekt mit keinem der switch-Arme übereinstimmt. Sie können anstelle von Enumerationswerten auch numerische Konstanten verwenden. Sie können dieses Verfahren auch auf ähnliche Weise für konstante Zeichenfolgenwerte verwenden, die die Befehle darstellen:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

Das vorherige Beispiel nutzt den gleichen Algorithmus, allerdings mit Zeichenfolgenwerten anstelle einer Enumeration. Dieses Szenario können Sie anwenden, wenn Ihre Anwendung auf Textbefehle und nicht auf ein reguläres Datenformat reagiert. Ab C# 11 können Sie auch Span<char> oder ReadOnlySpan<char> verwenden, um konstante Zeichenfolgenwerte zu prüfen, wie im folgenden Beispiel gezeigt:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

In allen diesen Beispielen stellt das Ausschussmuster sicher, dass sämtliche Eingaben verarbeitet werden. Der Compiler hilft Ihnen dabei, sicherzustellen, dass jeder mögliche Eingabewert verarbeitet wird.

Relationale Muster

Mit relationalen Mustern können Sie testen, ob ein Wert mit Konstanten übereinstimmt. Der folgende Code gibt beispielsweise den Zustand des Wassers basierend auf der Temperatur in Fahrenheit zurück:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

Der vorherige Code veranschaulicht auch das konjunktive logische Musterand, mit dem beide relationalen Muster überprüft werden. Sie können auch ein disjunktives or-Muster verwenden, um zu überprüfen, ob eines von zwei Mustern übereinstimmt. Die beiden relationalen Muster sind in Klammern eingeschlossen, die Sie aus Gründen der Übersichtlichkeit bei allen Mustern verwenden können. Die letzten beiden switch-Arme behandeln die Fälle für den Schmelzpunkt und den Siedepunkt. Ohne diese beiden Arme warnt Sie der Compiler, dass Ihre Logik nicht alle möglichen Eingaben behandelt.

Der obige Code veranschaulicht auch ein weiteres wichtiges Feature, das der Compiler für Musterabgleichsausdrücke bereitstellt: Der Compiler gibt eine Warnung aus, wenn nicht jeder Eingabewert verarbeitet wird. Der Compiler gibt auch eine Warnung aus, wenn das Muster für einen switch-Zweig bereits in einem vorherigen Muster verarbeitet wird. Dadurch können Sie switch-Ausdrücke umgestalten und neu anordnen. Eine andere Möglichkeit, denselben Ausdruck zu schreiben, ist:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

Die wichtigste Lektion im vorstehenden Beispiel und jeder anderen Umgestaltung oder Neuanordnung besteht darin, dass der Compiler überprüft, ob Ihr Code alle möglichen Eingaben abdeckt.

Mehrfacheingaben

Bei allen Mustern, die bisher behandelt wurden, wurde genau eine Eingabe überprüft. Sie können Muster schreiben, die mehrere Eigenschaften eines Objekts untersuchen. Betrachten Sie den folgenden Order-Datensatz:

public record Order(int Items, decimal Cost);

Der positionelle Typ „record“ deklariert zwei Member an festgeschriebenen Positionen. Zuerst steht der Artikel (Items), dann der Preis der Bestellung (Cost). Weitere Informationen finden Sie unter Datensätze.

Der folgende Code untersucht die Anzahl der Artikel und den Wert einer Bestellung, um einen Rabattpreis zu berechnen:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Die ersten beiden Arme untersuchen zwei Eigenschaften der Order. Der dritte untersucht nur die Kosten. Der nächste Arm überprüft auf null-Werte, und der letzte dient dem Abgleich mit jedem anderen Wert. Wenn der Order-Typ eine geeignete Deconstruct-Methode definiert, können Sie die Eigenschaftennamen im Muster weglassen und die Dekonstruktion verwenden, um Eigenschaften zu untersuchen:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Der vorangehende Code veranschaulicht das Positionsmuster, bei dem die Eigenschaften für den Ausdruck dekonstruiert werden.

Listenmuster

Sie können Elemente in einer Liste oder einem Array mithilfe eines Listenmusters überprüfen. Ein Listenmuster bietet die Möglichkeit, ein Muster auf ein beliebiges Element einer Sequenz anzuwenden. Darüber hinaus können Sie das Verwerfenmuster (_) für den Abgleich mit einem beliebigen Element bzw. ein Slicemuster für den Abgleich mit keinem oder mehr Elementen anwenden.

Listenmuster sind ein nützliches Tool, wenn Daten keiner regulären Struktur folgen. Sie können den Musterabgleich verwenden, um Form und Werte der Daten zu prüfen, anstatt sie in eine Gruppe von Objekten zu transformieren.

Betrachten Sie den folgenden Auszug aus einer Textdatei, die Banktransaktionen enthält:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Es handelt sich um ein CSV-Format, aber einige Zeilen weisen mehr Spalten als andere auf. Noch schlimmer für die Verarbeitung ist, dass eine Spalte im Typ WITHDRAWAL vom Benutzer generierten Text enthält und ein Komma im Text enthalten kann. Ein Listenmuster, das das Verwerfsmuster, das Konstantenmuster und das Variablenmuster enthält, um den Wert zu erfassen, verarbeitet Daten in diesem Format:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

Das obige Beispiel übernimmt ein Zeichenfolgenarray, wobei jedes Element ein Feld in der Zeile ist. Der switch-Ausdruck bezieht sich auf das zweite Feld, das die Art der Transaktion und die Anzahl der verbleibenden Spalten bestimmt. Jede Zeile stellt sicher, dass die Daten das richtige Format aufweisen. Das Verwerfenmuster (_) überspringt das erste Feld mit dem Datum der Transaktion. Das zweite Feld entspricht dem Transaktionstyp. Verbleibende Elementabgleiche springen zum Feld mit dem Betrag. Der letzte Abgleich verwendet das Variablenmuster (var), um die Zeichenfolgendarstellung des Betrags zu erfassen. Der Ausdruck berechnet den Betrag, der zum Saldo addiert oder davon subtrahiert werden soll.

Listenmuster bieten die Möglichkeit, Abgleiche abhängig von der Form einer Sequenz von Datenelementen durchzuführen. Mit dem Verwerfenmuster und dem Slicemuster können Sie die Position von Elementen abgleichen. Sie verwenden andere Muster, um Merkmale einzelner Elemente abzugleichen.

In diesem Artikel haben Sie verschiedene Arten von Code kennengelernt, die Sie mit Musterabgleichen in C# schreiben können. Die folgenden Artikel enthalten weitere Beispiele für die Verwendung von Mustern in Szenarien und das vollständige Vokabular der verfügbaren Muster.

Siehe auch