Ü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 Musterabgleich, um einen Ausdruck zu testen und eine neue Variable für das Ergebnis dieses Ausdrucks 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 = "This is not the null string";

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, verwendet sie die ICollection<T>.Count Eigenschaft in dieser Liste, um den mittleren Index zu finden. 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-Arm weglassen, gibt der Compiler eine Warnung aus, dass Sie nicht alle möglichen Eingabewerte verarbeitet haben. 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 einen oder einen Span<char>ReadOnlySpan<char>Test für Konstantenzeichenfolgenwerte verwenden, 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 Muster, um zu überprüfen, ob beide relationalen and Muster übereinstimmen. 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 vorherige Code veranschaulicht auch ein weiteres wichtiges Feature, das der Compiler für Musterabgleichsausdrücke bereitstellt: Der Compiler warnt Sie, wenn Sie nicht jeden Eingabewert behandeln. Der Compiler stellt auch eine Warnung vor, wenn ein Schalterarm bereits von einem vorherigen Schalterarm behandelt wird. Dadurch können Sie Ausdrücke neu erstellen und neu anordnen. Eine weitere Möglichkeit zum Schreiben desselben Ausdrucks könnte folgendes sein:

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

Die wichtige Lektion in diesem Bereich und alle anderen Umgestaltungen oder Neuanordnungen sind, dass der Compiler überprüft, dass Sie alle Eingaben behandelt haben.

Mehrfacheingaben

Bei allen Mustern, die Sie bisher gesehen haben, 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 ein Mittel zum Anwenden eines Musters auf ein beliebiges Element einer Sequenz. Darüber hinaus können Sie das Verwerfenmuster (_) anwenden, um einem beliebigen Element zu entsprechen oder ein Datenschnittmuster auf null oder mehr Elemente anzuwenden. Im folgenden Beispiel wird bestimmt, ob ein Array mit den binären Ziffern übereinstimmt, oder den Anfang einer Fibonacci-Sequenz:

public void MatchElements(int[] array)
{
    if (array is [0,1])
    {
        Console.WriteLine("Binary Digits");
    }
    else if (array is [1,1,2,3,5,8, ..])
    {
        Console.WriteLine("array looks like a Fibonacci sequence");
    }
    else
    {
        Console.WriteLine("Array shape not recognized");
    }
}

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

Siehe auch