Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Med mönstermatchning kan du lägga till funktioner för typer som definierats i andra bibliotek utan att ändra dessa typer. 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 tutorialen lär du dig följande:
- 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.
Prerequisites
- Den senaste versionen av .NET SDK
- Visual Studio Code redigeringsverktyg
- C# DevKit
Installationsanvisningar
På Windows används den här WinGet-konfigurationsfilen för att installera alla krav. Om du redan har något installerat hoppar WinGet över det steget.
- Ladda ned filen och dubbelklicka för att köra den.
- Läs licensavtalet, skriv yoch välj Ange när du uppmanas att acceptera.
- Om du får en flashande UAC-fråga (User Account Control) i aktivitetsfältet tillåter du att installationen fortsätter.
På andra plattformar måste du installera var och en av dessa komponenter separat.
- Ladda ned det rekommenderade installationsprogrammet från nedladdningssidan .NET SDK och dubbelklicka för att köra det. Nedladdningssidan identifierar din plattform och rekommenderar den senaste installationsprogrammet för din plattform.
- Ladda ned det senaste installationsprogrammet från startsidan Visual Studio Code och dubbelklicka för att köra det. Den sidan identifierar även din plattform och länken bör vara korrekt för ditt system.
- Klicka på knappen "Installera" på sidan C# DevKit tillägg. Då öppnas Visual Studio-kod och du tillfrågas om du vill installera eller aktivera tillägget. Välj "installera".
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 kanske inte styr vilka typer som representerar inkommande data.
Den klassiska objektorienterade designmetoden kräver att du skapar typer i ditt program som representerar varje datatyp från dessa flera datakällor. Sedan fungerar ditt program med dessa nya typer, skapar arvshierarkier, skapar virtuella metoder och implementerar abstraktioner. Dessa tekniker fungerar, och ibland är de de bästa verktygen. Andra gånger kan du skriva mindre kod. I vissa fall kan det göra koden enklare att läsa genom att separera data från de åtgärder som använder dem.
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 ta emot 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 under rusningstid för att hantera trafik. 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 skissa upp en objekthierarki för att modellera det här systemet. Dina uppgifter kommer dock från flera källor som andra system för fordonsregistreringshantering. De här systemen tillhandahåller olika klasser för att modellera dessa data, och de här systemen delar inte en enda objektmodell. I den här självstudien använder du dessa förenklade klasser för att modellera 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. Du kan inte använda en annan gemensam basklass än System.Object.
Mönstermatchningsdesign
Scenariot som används i den här självstudien belyser de typer av problem som mönstermatchning löser väl:
- 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. - Ett
Busär $ 5.00. - En
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önstretswitch. 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 switch-instruktionen. Nyckelordet case utelämnas och resultatet av varje arm är ett uttryck. De två sista armarna visar en ny funktion för språket. Ä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 { } ska falla in under fallen för varje fordonstyp. Om ordningen ändrades skulle ärendet { } ha företräde. Slutligen identifierar nulldet konstanta mönstret när null skickas till denna metod. 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 genom att använda 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 är utkommenterad. Ta bort kommentarerna så kan du testa det du skrev.
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ägg till prissättning för boendebeläggning
Vägtullsmyndigheten vill uppmuntra fordon att resa med maximal kapacitet. De tar ut högre avgifter när fordon har färre passagerare och uppmuntrar 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.
Du kan implementera dessa regler med hjälp av ett egenskapsmönster i samma switch-uttryck. Ett egenskapsmönster jämför ett egenskapsvärde med ett konstant värde. Egenskapsmönstret undersöker egenskaperna för objektet när typen har fastställts.
Car {Passengers: 0} är i sig ett rekursivt mönster: det yttre egenskapsmönstret på Car innehåller ett inre konstant mönster som testar Passengers värdet. 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 uttrycket och returneras.
Du kan också utöka fallen 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 beläggningsreglerna genom att utöka fallen för bussar, vilket visas 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,
// ...
};
Tullmyndigheten bryr sig inte om antalet passagerare i lastbilarna. 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.
Du kan implementera den regeln 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. Använd when-satsen för att testa andra villkor än likhet för 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))
};
Som tidigare nämnts är dessa switch-armar rekursiva mönster, som kapslar 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.
Car och Taxi har båda fyra olika armar i de föregående exemplen. 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 kapsling av ett växeluttryck i en annan växelarm att du inte upprepar de Car- och Taxi-armar som innehåller underordnade armar som testar egenskapsvärdet. Den här tekniken används inte för Bus- och DeliveryTruck-armarna eftersom dessa armar testar intervall för egenskapen, inte diskreta värden.
Lägga till högsta priser
För den sista funktionen vill vägavgiftsmyndigheten lägga till tidskänslig högtrafikprissättning. 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- och else-satser för att uttrycka den här regeln 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 är svårläst. Du måste gå igenom alla indatafall och kapslade if instruktioner för att kunna förstå 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 omfattar 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 tullavgiften. 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 | Direction | Premie |
|---|---|---|---|
| Veckodag | morgonrusning | inbound | x 2,00 |
| Veckodag | morgonrusning | outbound | x 1,00 |
| Veckodag | dagtid | inbound | x 1,50 |
| Veckodag | dagtid | outbound | x 1,50 |
| Veckodag | kvällsrusning | inbound | x 1,00 |
| Veckodag | kvällsrusning | outbound | x 2,00 |
| Veckodag | övernattning | inbound | x 0,75 |
| Veckodag | övernattning | outbound | x 0,75 |
| Helg | morgonrusning | inbound | x 1,00 |
| Helg | morgonrusning | outbound | x 1,00 |
| Helg | dagtid | inbound | x 1,00 |
| Helg | dagtid | outbound | x 1,00 |
| Helg | kvällsrusning | inbound | x 1,00 |
| Helg | kvällsrusning | outbound | x 1,00 |
| Helg | övernattning | inbound | 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 switch-uttrycket.
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 repetitiv. 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 logiska mönster. Med ett relationsmönster kan du testa ett numeriskt värde med hjälp av <, >, <=eller >=. Logiska mönster kombinerar andra mönster: ett or mönster testar om ett uttryck matchar ett eller flera mönster, ett and mönster testar att ett uttryck matchar två distinkta mönster och ett not mönster testar att ett uttryck inte matchar ett mönster.
När du har skapat dessa metoder kan du använda ett annat switch uttryck med ett tupppelmönster – ett positionsmönster som matchar varje element i en tuppl – 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,
};
Föregående kod fungerar, men den kan förenklas. Alla åtta kombinationer under 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. Du kan ersätta de fyra switch-armarna med följande två rader:
(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 tar du bort de två rusningstidpunkterna som kostar ordinarie pris. När du har avlägsnat dessa armar, ersätter du false med ett discard (_) i den sista switch-grenen. Du får 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. Dataformen och operationerna 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 dess ursprungliga syfte. Mönstermatchning gav dig möjlighet att skriva funktionalitet som åsidosatte 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.