Översikt över mönstermatchning

Mönstermatchning är en teknik där du testar ett uttryck för att avgöra om det har vissa egenskaper. C#-mönstermatchning ger mer koncis syntax för att testa uttryck och vidta åtgärder när ett uttryck matchar. Uttrycketis har stöd för mönstermatchning för att testa ett uttryck och villkorligt deklarera en ny variabel till resultatet av uttrycket. Med uttrycketswitch kan du utföra åtgärder baserat på det första matchande mönstret för ett uttryck. Dessa två uttryck stöder ett omfattande ordförråd med mönster.

Den här artikeln innehåller en översikt över scenarier där du kan använda mönstermatchning. Dessa tekniker kan förbättra kodens läsbarhet och korrekthet. En fullständig diskussion om alla mönster som du kan använda finns i artikeln om mönster i språkreferensen.

Null-kontroller

Ett av de vanligaste scenarierna för mönstermatchning är att se till att värdena inte nullär . Du kan testa och konvertera en nullbar värdetyp till dess underliggande typ när du testar för null att använda följande exempel:

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");
}

Föregående kod är ett deklarationsmönster för att testa typen av variabel och tilldela den till en ny variabel. Språkreglerna gör den här tekniken säkrare än många andra. Variabeln number är endast tillgänglig och tilldelad i den sanna delen av if satsen. Om du försöker komma åt den någon annanstans, antingen i else -satsen eller efter if blocket, utfärdar kompilatorn ett fel. För det andra, eftersom du inte använder operatorn == , fungerar det här mönstret när en typ överbelastar operatorn == . Det gör det till ett idealiskt sätt att kontrollera null-referensvärden och lägga till not mönstret:

string? message = ReadMessageOrDefault();

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

I föregående exempel användes ett konstant mönster för att jämföra variabeln med null. not Är ett logiskt mönster som matchar när det negerade mönstret inte matchar.

Typtester

En annan vanlig användning för mönstermatchning är att testa en variabel för att se om den matchar en viss typ. Följande kod testar till exempel om en variabel inte är null och implementerar System.Collections.Generic.IList<T> gränssnittet. Om den gör det använder den ICollection<T>.Count egenskapen i listan för att hitta mittindexet. Deklarationsmönstret matchar inte ett null värde, oavsett variabelns kompileringstidstyp. Koden nedan skyddar mot null, förutom att skydda mot en typ som inte implementerar IList.

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();
    }
}

Samma tester kan användas i ett switch uttryck för att testa en variabel mot flera olika typer. Du kan använda den informationen för att skapa bättre algoritmer baserat på den specifika körningstypen.

Jämför diskreta värden

Du kan också testa en variabel för att hitta en matchning för specifika värden. Följande kod visar ett exempel där du testar ett värde mot alla möjliga värden som deklareras i en uppräkning:

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)),
   };

I föregående exempel visas en metodsändning baserat på värdet för en uppräkning. Det sista _ fallet är ett ignorerande mönster som matchar alla värden. Den hanterar eventuella felvillkor där värdet inte matchar något av de definierade enum värdena. Om du utelämnar den växlingsarmen varnar kompilatorn för att ditt mönsteruttryck inte hanterar alla möjliga indatavärden. Vid körning utlöser switch uttrycket ett undantag om objektet som undersöks inte matchar någon av växlingsarmarna. Du kan använda numeriska konstanter i stället för en uppsättning uppräkningsvärden. Du kan också använda den här liknande tekniken för konstanta strängvärden som representerar kommandona:

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)),
   };

Föregående exempel visar samma algoritm, men använder strängvärden i stället för en uppräkning. Du skulle använda det här scenariot om ditt program svarar på textkommandon i stället för ett vanligt dataformat. Från och med C# 11 kan du också använda en Span<char> eller en ReadOnlySpan<char>för att testa konstanta strängvärden, som du ser i följande exempel:

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)),
   };

I alla dessa exempel säkerställer mönstret ignorera att du hanterar alla indata. Kompilatorn hjälper dig genom att se till att alla möjliga indatavärden hanteras.

Relationsmönster

Du kan använda relationsmönster för att testa hur ett värde jämförs med konstanter. Följande kod returnerar till exempel vattentillståndet baserat på temperaturen i Fahrenheit:

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

Föregående kod visar också det konjunktiva andlogiska mönstret för att kontrollera att båda relationsmönstren matchar. Du kan också använda ett disjunctive-mönster or för att kontrollera att något av mönstret matchar. De två relationsmönstren omges av parenteser, som du kan använda runt valfritt mönster för tydlighetens skull. De sista två reglagearmarna hanterar fallen för smältpunkten och kokpunkten. Utan dessa två armar varnar kompilatorn dig att din logik inte täcker alla möjliga indata.

Föregående kod visar också en annan viktig funktion som kompilatorn tillhandahåller för mönstermatchningsuttryck: Kompilatorn varnar dig om du inte hanterar alla indatavärden. Kompilatorn utfärdar också en varning om mönstret för en switcharm omfattas av ett tidigare mönster. Det ger dig frihet att omstrukturera och ändra ordning på växlingsuttryck. Ett annat sätt att skriva samma uttryck kan vara:

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

Den viktigaste lektionen i föregående exempel och eventuell annan refaktorisering eller omordning är att kompilatorn verifierar att koden hanterar alla möjliga indata.

Flera indata

Alla mönster som omfattas hittills har kontrollerat en indata. Du kan skriva mönster som undersöker flera egenskaper för ett objekt. Överväg följande Order post:

public record Order(int Items, decimal Cost);

Den föregående positionsposttypen deklarerar två medlemmar på explicita positioner. Först visas Items, och sedan orderns Cost. Mer information finns i Poster.

Följande kod undersöker antalet objekt och värdet för en order för att beräkna ett rabatterat pris:

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,
    };

De första två armarna undersöker två egenskaper hos Order. Den tredje undersöker endast kostnaden. Nästa checkar mot null, och finalen matchar alla andra värden. Om typen Order definierar en lämplig Deconstruct metod kan du utelämna egenskapsnamnen från mönstret och använda dekonstruktion för att undersöka egenskaper:

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,
    };

Föregående kod visar positionsmönstret där egenskaperna dekonstrueras för uttrycket.

Listmönster

Du kan kontrollera element i en lista eller en matris med hjälp av ett listmönster. Ett listmönster ger ett sätt att tillämpa ett mönster på alla element i en sekvens. Dessutom kan du använda mönstret ignorera (_) för att matcha alla element eller använda ett segmentmönster för att matcha noll eller flera element.

Listmönster är ett värdefullt verktyg när data inte följer en vanlig struktur. Du kan använda mönstermatchning för att testa datans form och värden i stället för att omvandla dem till en uppsättning objekt.

Tänk dig följande utdrag från en textfil som innehåller banktransaktioner:

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

Det är ett CSV-format, men vissa av raderna har fler kolumner än andra. Ännu värre för bearbetningen är att en kolumn i WITHDRAWAL typen innehåller användargenererad text och kan innehålla ett kommatecken i texten. Ett listmönster som innehåller mönstret ignorera , konstant mönster och var-mönster för att samla in värdeprocessdata i det här formatet:

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}");
}

Föregående exempel tar en strängmatris, där varje element är ett fält på raden. Uttrycksnycklarna switch i det andra fältet, som avgör typen av transaktion och antalet återstående kolumner. Varje rad ser till att data har rätt format. Mönstret ignorera (_) hoppar över det första fältet med datumet för transaktionen. Det andra fältet matchar typen av transaktion. Återstående element matchar hoppa till fältet med mängden. Den sista matchningen använder var-mönstret för att avbilda strängrepresentationen av mängden. Uttrycket beräknar mängden som ska läggas till eller subtraheras från saldot.

Med listmönster kan du matcha formen på en sekvens med dataelement. Du använder mönstret ignorera och segmentera för att matcha elementens plats. Du använder andra mönster för att matcha egenskaper om enskilda element.

Den här artikeln gav en genomgång av de typer av kod som du kan skriva med mönstermatchning i C#. Följande artiklar visar fler exempel på hur du använder mönster i scenarier och den fullständiga vokabulären för mönster som är tillgängliga att använda.

Se även