Självstudie: Använda mönstermatchning för att skapa typdrivna och datadrivna algoritmer

Du kan skriva funktioner som fungerar som om du har utökat typer som kan finnas i andra bibliotek. En annan användning för mönster är att skapa funktioner som programmet kräver som inte är en grundläggande funktion av den typ som utökas.

I den här självstudien får du lära dig att:

  • Identifiera situationer där mönstermatchning ska användas.
  • Använd mönstermatchningsuttryck för att implementera beteende baserat på typer och egenskapsvärden.
  • Kombinera mönstermatchning med andra tekniker för att skapa fullständiga algoritmer.

Förutsättningar

Den här självstudien förutsätter att du är bekant med C# och .NET, inklusive antingen Visual Studio eller .NET CLI.

Scenarier för mönstermatchning

Modern utveckling omfattar ofta integrering av data från flera källor och presentation av information och insikter från dessa data i ett enda sammanhängande program. Du och ditt team har inte kontroll eller åtkomst för alla typer som representerar inkommande data.

Den klassiska objektorienterade designen anropar för att skapa typer i ditt program som representerar varje datatyp från dessa flera datakällor. Sedan skulle ditt program fungera med dessa nya typer, skapa arvshierarkier, skapa virtuella metoder och implementera abstraktioner. Dessa tekniker fungerar, och ibland är de de bästa verktygen. Andra gånger kan du skriva mindre kod. Du kan skriva mer tydlig kod med hjälp av tekniker som skiljer data från de åtgärder som manipulerar dessa data.

I den här självstudien skapar och utforskar du ett program som tar inkommande data från flera externa källor för ett enda scenario. Du ser hur mönstermatchning ger ett effektivt sätt att använda och bearbeta dessa data på sätt som inte ingick i det ursprungliga systemet.

Överväg ett större storstadsområde som använder vägtullar och prissättning med hög belastning för att hantera trafiken. Du skriver ett program som beräknar vägtullar för ett fordon baserat på dess typ. Senare förbättringar omfattar prissättning baserat på antalet passagerare i fordonet. Ytterligare förbättringar lägger till priser baserat på tid och veckodag.

Från den korta beskrivningen kan du snabbt ha skissat upp en objekthierarki för att modellera systemet. Dina data kommer dock från flera källor som andra system för fordonsregistreringshantering. Dessa system tillhandahåller olika klasser för att modellera dessa data och du har inte en enda objektmodell som du kan använda. I den här självstudien använder du dessa förenklade klasser för att modellera för fordonsdata från dessa externa system, enligt följande kod:

namespace ConsumerVehicleRegistration
{
    public class Car
    {
        public int Passengers { get; set; }
    }
}

namespace CommercialRegistration
{
    public class DeliveryTruck
    {
        public int GrossWeightClass { get; set; }
    }
}

namespace LiveryRegistration
{
    public class Taxi
    {
        public int Fares { get; set; }
    }

    public class Bus
    {
        public int Capacity { get; set; }
        public int Riders { get; set; }
    }
}

Du kan ladda ned startkoden från GitHub-lagringsplatsen dotnet/samples . Du kan se att fordonsklasserna kommer från olika system och finns i olika namnområden. Ingen gemensam basklass, förutom System.Object kan användas.

Mönstermatchningsdesign

Det scenario som används i den här självstudien belyser de typer av problem som mönstermatchning passar bra för att lösa:

  • De objekt som du behöver arbeta med finns inte i en objekthierarki som matchar dina mål. Du kanske arbetar med klasser som ingår i orelaterade system.
  • Funktionerna som du lägger till är inte en del av kärnabstraktionen för dessa klasser. Den avgift som betalas av ett fordon ändras för olika typer av fordon, men avgiften är inte en kärnfunktion i fordonet.

När formen på data och åtgärderna på dessa data inte beskrivs tillsammans gör mönstermatchningsfunktionerna i C# det enklare att arbeta med.

Implementera de grundläggande avgiftsberäkningarna

Den mest grundläggande vägtullsberäkningen förlitar sig endast på fordonstypen:

  • A Car är 2,00 dollar.
  • A Taxi är 3,50 dollar.
  • A Bus är 5,00 dollar.
  • A DeliveryTruck är $10.00

Skapa en ny TollCalculator klass och implementera mönstermatchning på fordonstypen för att få det avgiftsbelagda beloppet. Följande kod visar den första implementeringen av TollCalculator.

using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

namespace Calculators;

public class TollCalculator
{
    public decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car c           => 2.00m,
        Taxi t          => 3.50m,
        Bus b           => 5.00m,
        DeliveryTruck t => 10.00m,
        { }             => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
        null            => throw new ArgumentNullException(nameof(vehicle))
    };
}

Föregående kod använder ett uttryck (inte samma som en switch -instruktion) som testar deklarationsmönstret.switch Ett switch-uttryck börjar med variabeln, vehicle i föregående kod, följt av nyckelordet switch . Därefter kommer alla switcharmar inuti klammerparenteser. Uttrycket switch gör andra förbättringar av syntaxen som omger -instruktionen switch . Nyckelordet case utelämnas och resultatet av varje arm är ett uttryck. De två sista armarna visar en ny språkfunktion. Ärendet { } matchar alla icke-null-objekt som inte matchade en tidigare arm. Den här armen fångar upp eventuella felaktiga typer som skickas till den här metoden. Ärendet { } måste följa fallen för varje fordonstyp. Om ordningen ändrades skulle ärendet { } ha företräde. Slutligen identifierar det null konstanta mönstret när null det skickas till den här metoden. Mönstret null kan vara sist eftersom de andra mönstren endast matchar ett icke-null-objekt av rätt typ.

Du kan testa den här koden med hjälp av följande kod i Program.cs:

using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

using toll_calculator;

var tollCalc = new TollCalculator();

var car = new Car();
var taxi = new Taxi();
var bus = new Bus();
var truck = new DeliveryTruck();

Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}");
Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}");
Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}");
Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}");

try
{
    tollCalc.CalculateToll("this will fail");
}
catch (ArgumentException e)
{
    Console.WriteLine("Caught an argument exception when using the wrong type");
}
try
{
    tollCalc.CalculateToll(null!);
}
catch (ArgumentNullException e)
{
    Console.WriteLine("Caught an argument exception when using null");
}

Koden ingår i startprojektet, men den kommenteras ut. Ta bort kommentarerna så kan du testa det du har skrivit.

Du börjar se hur mönster kan hjälpa dig att skapa algoritmer där koden och data är separata. Uttrycket switch testar typen och genererar olika värden baserat på resultaten. Det är bara början.

Lägga till prissättning för beläggning

Vägtullsmyndigheten vill uppmuntra fordon att resa med maximal kapacitet. De har beslutat att ta ut mer när fordon har färre passagerare och uppmuntra fulla fordon genom att erbjuda lägre priser:

  • Bilar och taxibilar utan passagerare betalar en extra $ 0.50.
  • Bilar och taxibilar med två passagerare får rabatt på $ 0.50.
  • Bilar och taxibilar med tre eller fler passagerare får rabatt på $ 1.00.
  • Bussar som är mindre än 50% fulla betalar en extra $ 2.00.
  • Bussar som är mer än 90% fulla får en rabatt på $ 1.00.

Dessa regler kan implementeras med hjälp av ett egenskapsmönster i samma växeluttryck. Ett egenskapsmönster jämför ett egenskapsvärde med ett konstant värde. Egenskapsmönstret undersöker objektets egenskaper när typen har fastställts. Det enda fallet för ett Car expanderar till fyra olika fall:

vehicle switch
{
    Car {Passengers: 0} => 2.00m + 0.50m,
    Car {Passengers: 1} => 2.0m,
    Car {Passengers: 2} => 2.0m - 0.50m,
    Car                 => 2.00m - 1.0m,

    // ...
};

De första tre fallen testar typen som en Caroch kontrollerar sedan värdet för Passengers egenskapen. Om båda matchar utvärderas och returneras uttrycket.

Du skulle också utöka ärendena för taxibilar på ett liknande sätt:

vehicle switch
{
    // ...

    Taxi {Fares: 0}  => 3.50m + 1.00m,
    Taxi {Fares: 1}  => 3.50m,
    Taxi {Fares: 2}  => 3.50m - 0.50m,
    Taxi             => 3.50m - 1.00m,

    // ...
};

Implementera sedan reglerna för inflyttning genom att utöka ärendena för bussar, som du ser i följande exempel:

vehicle switch
{
    // ...

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    // ...
};

Den avgiftsbelagda myndigheten bryr sig inte om antalet passagerare i leveransbilarna. I stället justerar de avgiftsbeloppet baserat på lastbilarnas viktklass enligt följande:

  • Lastbilar över 5000 lbs debiteras en extra $ 5,00.
  • Lätta lastbilar under 3000 lbs får en rabatt på $ 2.00.

Regeln implementeras med följande kod:

vehicle switch
{
    // ...

    DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
    DeliveryTruck => 10.00m,
};

Föregående kod visar satsen för when en växelarm. Du använder when -satsen för att testa andra villkor än likhet på en egenskap. När du är klar har du en metod som liknar följande kod:

vehicle switch
{
    Car {Passengers: 0}        => 2.00m + 0.50m,
    Car {Passengers: 1}        => 2.0m,
    Car {Passengers: 2}        => 2.0m - 0.50m,
    Car                        => 2.00m - 1.0m,

    Taxi {Fares: 0}  => 3.50m + 1.00m,
    Taxi {Fares: 1}  => 3.50m,
    Taxi {Fares: 2}  => 3.50m - 0.50m,
    Taxi             => 3.50m - 1.00m,

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
    DeliveryTruck => 10.00m,

    { }     => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
    null    => throw new ArgumentNullException(nameof(vehicle))
};

Många av dessa switcharmar är exempel på rekursiva mönster. Visar till exempel Car { Passengers: 1} ett konstant mönster i ett egenskapsmönster.

Du kan göra den här koden mindre repetitiv genom att använda kapslade växlar. Och Car Taxi båda har fyra olika armar i föregående exempel. I båda fallen kan du skapa ett deklarationsmönster som matar in i ett konstant mönster. Den här tekniken visas i följande kod:

public decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        Car c => c.Passengers switch
        {
            0 => 2.00m + 0.5m,
            1 => 2.0m,
            2 => 2.0m - 0.5m,
            _ => 2.00m - 1.0m
        },

        Taxi t => t.Fares switch
        {
            0 => 3.50m + 1.00m,
            1 => 3.50m,
            2 => 3.50m - 0.50m,
            _ => 3.50m - 1.00m
        },

        Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
        Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
        Bus b => 5.00m,

        DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
        DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
        DeliveryTruck t => 10.00m,

        { }  => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
        null => throw new ArgumentNullException(nameof(vehicle))
    };

I föregående exempel innebär rekursivt uttryck att du inte upprepar de Car armar som Taxi innehåller underordnade armar som testar egenskapsvärdet. Den här tekniken används inte för Bus armarna och DeliveryTruck eftersom dessa armar testar intervall för egenskapen, inte diskreta värden.

Lägga till högsta priser

För den sista funktionen vill avgiftsutfärdarna lägga till tidskänsliga högsta priser. Under morgon- och kvällsrusningen fördubblas vägtullarna. Den regeln påverkar endast trafik i en riktning: inkommande till staden på morgonen och utgående i kvällsrusningen. Under andra tider under arbetsdagen ökar vägtullarna med 50 %. Sent på kvällen och tidigt på morgonen sänks vägtullarna med 25 procent. Under helgen är det den normala hastigheten, oavsett tid. Du kan använda en serie if med - och else -instruktioner för att uttrycka detta med hjälp av följande kod:

public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
    if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
        (timeOfToll.DayOfWeek == DayOfWeek.Sunday))
    {
        return 1.0m;
    }
    else
    {
        int hour = timeOfToll.Hour;
        if (hour < 6)
        {
            return 0.75m;
        }
        else if (hour < 10)
        {
            if (inbound)
            {
                return 2.0m;
            }
            else
            {
                return 1.0m;
            }
        }
        else if (hour < 16)
        {
            return 1.5m;
        }
        else if (hour < 20)
        {
            if (inbound)
            {
                return 1.0m;
            }
            else
            {
                return 2.0m;
            }
        }
        else // Overnight
        {
            return 0.75m;
        }
    }
}

Föregående kod fungerar korrekt, men kan inte läsas. Du måste länka igenom alla indatafall och kapslade if instruktioner för att resonera om koden. I stället använder du mönstermatchning för den här funktionen, men du integrerar den med andra tekniker. Du kan skapa ett enda mönstermatchningsuttryck som skulle ta hänsyn till alla kombinationer av riktning, veckodag och tid. Resultatet skulle vara ett komplicerat uttryck. Det skulle vara svårt att läsa och svårt att förstå. Det gör det svårt att säkerställa korrekthet. Kombinera i stället dessa metoder för att skapa en tuppeln med värden som kortfattat beskriver alla dessa tillstånd. Använd sedan mönstermatchning för att beräkna en multiplikator för vägtullen. Tuppeln innehåller tre diskreta villkor:

  • Dagen är antingen en veckodag eller en helg.
  • Tidsintervallet när avgiften samlas in.
  • Riktningen är in i staden eller ut ur staden

I följande tabell visas kombinationerna av indatavärden och den högsta prismultiplikatorn:

Dag Tid Riktning Premium
Weekday morgonrusning inkommande x 2,00
Weekday morgonrusning outbound x 1,00
Weekday dagtid inkommande x 1,50
Weekday dagtid outbound x 1,50
Weekday kvällsrusning inkommande x 1,00
Weekday kvällsrusning outbound x 2,00
Weekday övernattning inkommande x 0,75
Weekday övernattning outbound x 0,75
Helg morgonrusning inkommande x 1,00
Helg morgonrusning outbound x 1,00
Helg dagtid inkommande x 1,00
Helg dagtid outbound x 1,00
Helg kvällsrusning inkommande x 1,00
Helg kvällsrusning outbound x 1,00
Helg övernattning inkommande x 1,00
Helg övernattning outbound x 1,00

Det finns 16 olika kombinationer av de tre variablerna. Genom att kombinera några av villkoren förenklar du det slutliga växeluttrycket.

Systemet som samlar in vägtullarna använder en DateTime struktur för den tid då avgiften samlades in. Skapa medlemsmetoder som skapar variablerna från föregående tabell. Följande funktion använder ett mönstermatchande växeluttryck för att uttrycka om en DateTime representerar en helg eller en veckodag:

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
    {
        DayOfWeek.Monday    => true,
        DayOfWeek.Tuesday   => true,
        DayOfWeek.Wednesday => true,
        DayOfWeek.Thursday  => true,
        DayOfWeek.Friday    => true,
        DayOfWeek.Saturday  => false,
        DayOfWeek.Sunday    => false
    };

Den metoden är korrekt, men den är repetitious. Du kan förenkla det, som du ser i följande kod:

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
    {
        DayOfWeek.Saturday => false,
        DayOfWeek.Sunday => false,
        _ => true
    };

Lägg sedan till en liknande funktion för att kategorisera tiden i blocken:

private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };

Du lägger till en privat enum för att konvertera varje tidsintervall till ett diskret värde. GetTimeBand Sedan använder metoden relationsmönster och konjunktiva or mönster. Med ett relationsmönster kan du testa ett numeriskt värde med hjälp av <, >, <=eller >=. Mönstret or testar om ett uttryck matchar ett eller flera mönster. Du kan också använda ett and mönster för att säkerställa att ett uttryck matchar två distinkta mönster och ett not mönster för att testa att ett uttryck inte matchar ett mönster.

När du har skapat dessa metoder kan du använda ett annat switch uttryck med tuppelns mönster för att beräkna prispremien. Du kan skapa ett switch uttryck med alla 16 armar:

public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime, true) => 1.50m,
        (true, TimeBand.Daytime, false) => 1.50m,
        (true, TimeBand.EveningRush, true) => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, true) => 0.75m,
        (true, TimeBand.Overnight, false) => 0.75m,
        (false, TimeBand.MorningRush, true) => 1.00m,
        (false, TimeBand.MorningRush, false) => 1.00m,
        (false, TimeBand.Daytime, true) => 1.00m,
        (false, TimeBand.Daytime, false) => 1.00m,
        (false, TimeBand.EveningRush, true) => 1.00m,
        (false, TimeBand.EveningRush, false) => 1.00m,
        (false, TimeBand.Overnight, true) => 1.00m,
        (false, TimeBand.Overnight, false) => 1.00m,
    };

Koden ovan fungerar, men den kan förenklas. Alla åtta kombinationer för helgen har samma avgift. Du kan ersätta alla åtta med följande rad:

(false, _, _) => 1.0m,

Både inkommande och utgående trafik har samma multiplikator under veckodagen dagtid och nattetid. Dessa fyra switcharmar kan ersättas med följande två linjer:

(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _)   => 1.5m,

Koden bör se ut som följande kod efter dessa två ändringar:

public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true)  => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime,     _)     => 1.50m,
        (true, TimeBand.EveningRush, true)  => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight,   _)     => 0.75m,
        (false, _,                   _)     => 1.00m,
    };

Slutligen kan du ta bort de två rusningstiderna som betalar det vanliga priset. När du tar bort dessa armar, kan du ersätta false med en kasta (_) i den sista växeln armen. Du har följande färdiga metod:

public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.Overnight, _) => 0.75m,
        (true, TimeBand.Daytime, _) => 1.5m,
        (true, TimeBand.MorningRush, true) => 2.0m,
        (true, TimeBand.EveningRush, false) => 2.0m,
        _ => 1.0m,
    };

Det här exemplet visar en av fördelarna med mönstermatchning: mönstergrenarna utvärderas i ordning. Om du ordnar om dem så att en tidigare gren hanterar ett av dina senare fall varnar kompilatorn dig om den oåtkomliga koden. Dessa språkregler gjorde det lättare att göra de föregående förenklingarna med förtroende för att koden inte ändrades.

Mönstermatchning gör vissa typer av kod mer läsbara och erbjuder ett alternativ till objektorienterade tekniker när du inte kan lägga till kod i dina klasser. Molnet gör att data och funktioner lever åtskilda. Formen på data och åtgärderna på dem beskrivs inte nödvändigtvis tillsammans. I den här självstudien använder du befintliga data på helt andra sätt än den ursprungliga funktionen. Mönstermatchning gav dig möjlighet att skriva funktioner som överskriv dessa typer, även om du inte kunde utöka dem.

Nästa steg

Du kan ladda ned den färdiga koden från GitHub-lagringsplatsen dotnet/samples . Utforska mönster på egen hand och lägg till den här tekniken i dina vanliga kodningsaktiviteter. Genom att lära dig de här teknikerna får du ett annat sätt att hantera problem och skapa nya funktioner.

Se även