Sdílet prostřednictvím


Kurz: Použití porovnávání vzorů k vytváření algoritmů řízených typem a datových řízených algoritmy

Můžete psát funkce, které se chovají jako rozšířené typy, které mohou být v jiných knihovnách. Další možností použití vzorů je vytvoření funkcí, které vaše aplikace vyžaduje, aby nebyla základní funkcí typu, který se rozšiřuje.

V tomto kurzu se naučíte:

  • Rozpoznávejte situace, kdy se má použít porovnávání vzorů.
  • Pomocí výrazů porovnávání vzorů můžete implementovat chování na základě typů a hodnot vlastností.
  • Zkombinujte porovnávání vzorů s jinými technikami a vytvořte kompletní algoritmy.

Požadavky

  • Doporučujeme Visual Studio pro Windows. Bezplatnou verzi si můžete stáhnout ze stránky pro stažení sady Visual Studio. Visual Studio obsahuje sadu .NET SDK.
  • Editor editoru Visual Studio Code můžete použít také s jazykem C# DevKit. Budete muset nainstalovat nejnovější sadu .NET SDK samostatně.
  • Pokud dáváte přednost jinému editoru, musíte nainstalovat nejnovější sadu .NET SDK.

V tomto kurzu se předpokládá, že znáte C# a .NET, včetně sady Visual Studio nebo rozhraní příkazového řádku .NET.

Scénáře pro porovnávání vzorů

Moderní vývoj často zahrnuje integraci dat z více zdrojů a prezentování informací a přehledů z dat v jedné soudržné aplikaci. Vy a váš tým nebudete mít kontrolu ani přístup pro všechny typy, které představují příchozí data.

Klasický objektově orientovaný návrh by volal pro vytváření typů v aplikaci, které představují každý datový typ z těchto více zdrojů dat. Aplikace pak bude pracovat s těmito novými typy, vytvářet hierarchie dědičnosti, vytvářet virtuální metody a implementovat abstrakce. Tyto techniky fungují a někdy jsou nejlepšími nástroji. Jindy můžete napsat méně kódu. Srozumitelnější kód můžete napsat pomocí technik, které oddělují data od operací, které s nimi manipulují.

V tomto kurzu vytvoříte a prozkoumáte aplikaci, která přijímá příchozí data z několika externích zdrojů pro jeden scénář. Uvidíte, jak porovnávání vzorů představuje efektivní způsob, jak tato data využívat a zpracovávat způsoby, které nebyly součástí původního systému.

Zvažte hlavní metropolitní oblast, která ke správě provozu využívá placené poplatky a ceny ve špičce. Napíšete aplikaci, která počítá placené poplatky pro vozidlo na základě jeho typu. Pozdější vylepšení zahrnují ceny založené na počtu osob ve vozidle. Další vylepšení přidávají ceny na základě času a dne v týdnu.

Z tohoto stručného popisu jste možná rychle načrtli hierarchii objektů pro modelování tohoto systému. Vaše data ale pocházejí z více zdrojů, jako jsou jiné systémy správy registrace vozidel. Tyto systémy poskytují různé třídy pro modelování těchto dat a nemáte jediný objektový model, který můžete použít. V tomto kurzu použijete tyto zjednodušené třídy k modelování dat vozidel z těchto externích systémů, jak je znázorněno v následujícím kódu:

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

Počáteční kód si můžete stáhnout z úložiště dotnet/samples na GitHubu. Vidíte, že třídy vozidel pocházejí z různých systémů a jsou v různých oborech názvů. Žádná společná základní třída, jiná než System.Object lze použít.

Návrhy porovnávání vzorů

Scénář použitý v tomto kurzu zvýrazňuje druhy problémů, které porovnávání vzorů je vhodné k vyřešení:

  • Objekty, se kterými potřebujete pracovat, nejsou v hierarchii objektů, které odpovídají vašim cílům. Možná pracujete s třídami, které jsou součástí nesouvisejících systémů.
  • Funkce, které přidáváte, nejsou součástí základní abstrakce těchto tříd. Placená linka placená vozidlem se mění pro různé typy vozidel, ale placená linka není základní funkcí vozidla.

Pokud tvar dat a operací s daty nejsou popsány společně, funkce porovnávání vzorů v jazyce C# usnadňují práci.

Implementace základních výpočtů placené linky

Nejzásadnější výpočet placené linky závisí pouze na typu vozidla:

  • A Car je $2,00.
  • A Taxi je $3,50.
  • A Bus je 5,00 USD.
  • A DeliveryTruck je $10,00

Vytvořte novou TollCalculator třídu a implementujte porovnávání vzorů u typu vozidla, abyste získali částku placené linky. Následující kód ukazuje počáteční implementaci 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))
    };
}

Předchozí kód používá switch výraz (ne stejný jako switch příkaz), který testuje vzor deklarace. Výraz switch začíná proměnnou vehicle v předchozím kódu následovaný klíčovým slovemswitch. Dále přichází všechny přepínací ramena uvnitř složených závorek. Výraz switch zpřesňuje syntaxi, která příkaz obklopuje switch . Klíčové case slovo je vynecháno a výsledek každé arm je výraz. Poslední dvě ramena ukazují novou funkci jazyka. Případ { } odpovídá jakémukoli objektu, který neodpovídá dřívějšímu armu. Tato arm zachytí všechny nesprávné typy předané této metodě. Případ { } musí odpovídat případům pro každý typ vozidla. Pokud by bylo pořadí obrácené, { } případ by měl přednost. Nakonec konstantní null vzor zjistí, kdy null je předána této metodě. Vzor null může být poslední, protože ostatní vzory odpovídají pouze objektu, který není null správného typu.

Tento kód můžete otestovat pomocí následujícího kódu v 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");
}

Tento kód je součástí počátečního projektu, ale je zakomentován. Odeberte komentáře a můžete otestovat, co jste napsali.

Začínáte vidět, jak vám vzory můžou pomoct vytvářet algoritmy, ve kterých je kód a data oddělená. Výraz switch testuje typ a vytváří různé hodnoty na základě výsledků. To je jen začátek.

Přidání cen obsazenosti

Úřad placené linky chce povzbuzovat vozidla k jízdě s maximální kapacitou. Rozhodli se účtovat více, když vozidla mají méně cestujících, a podpořit plné vozidla tím, že nabízejí nižší ceny:

  • Auta a taxíky bez cestujících platí navíc 0,50 USD.
  • Auta a taxi se dvěma cestujícími získají slevu 0,50 USD.
  • Auta a taxi se třemi nebo více cestujícími získají slevu 1,00 USD.
  • Autobusy, které jsou nižší než 50% plné platby navíc 2,00 USD.
  • Autobusy, které jsou více než 90% plné, získají slevu 1,00 USD.

Tato pravidla je možné implementovat pomocí vzoru vlastnosti ve stejném výrazu switch. Vzor vlastnosti porovnává hodnotu vlastnosti s konstantní hodnotou. Vzor vlastnosti zkoumá vlastnosti objektu po určení typu. Jeden případ rozbalí Car na čtyři různé případy:

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,

    // ...
};

První tři případy testují typ jako a Carpak zkontrolujte hodnotu Passengers vlastnosti. Pokud se oba shodují, tento výraz se vyhodnotí a vrátí.

Případy taxislužby byste také rozšířili podobným způsobem:

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,

    // ...
};

Dále implementujte pravidla obsazenosti rozšířením případů pro autobusy, jak je znázorněno v následujícím příkladu:

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,

    // ...
};

Placená autorita se nezajímá o počet cestujících v nákladních vozech. Místo toho upraví částku placené linky na základě třídy hmotnosti nákladních vozů následujícím způsobem:

  • Nákladní vozy nad 5000 lb se účtují navíc 5,00 USD.
  • Lehké nákladní vozy pod 3000 lbs jsou dány slevou 2,00 USD.

Toto pravidlo se implementuje s následujícím kódem:

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

Předchozí kód ukazuje klauzuli when přepínače arm. Klauzuli when použijete k otestování podmínek jiných než rovnosti u vlastnosti. Po dokončení budete mít metodu, která vypadá podobně jako následující kód:

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

Mnohé z těchto přepínačů představují příklady rekurzivních vzorů. Například Car { Passengers: 1} ukazuje konstantní vzor uvnitř vzoru vlastnosti.

Tento kód můžete zmenšit opakováním pomocí vnořených přepínačů. Taxi Oba mají Car v předchozích příkladech čtyři různé zbraně. V oboupřípadechch Tato technika se zobrazuje v následujícím kódu:

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

V předchozím příkladu použití rekurzivního výrazu znamená, že neopakujete Car a Taxi ramena obsahující podřízené zbraně, které testují hodnotu vlastnosti. Tato technika se nepoužívá pro Bus zbraně a DeliveryTruck zbraně, protože tyto zbraně testují rozsahy pro vlastnost, nikoli diskrétní hodnoty.

Přidání cen ve špičce

U konečné funkce chce úřad placené linky přidat ceny s časovými citlivými špičkami. Během ranního a večerního spěchu se poplatky zdvojnásobí. Toto pravidlo má vliv jenom na provoz v jednom směru: příchozí do města ráno a odchozí v hodině večerního spěchu. V jiných časech během pracovního dne se placené poplatky zvyšují o 50 %. Pozdní noc a brzké ráno se placené poplatky snižují o 25 %. Během víkendu je to normální sazba bez ohledu na čas. K vyjádření této řady if příkazů else můžete použít následující kód:

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

Předchozí kód funguje správně, ale není čitelný. Ke zřetězování všech vstupních případů a vnořených if příkazů je potřeba kód zřetězí. Místo toho použijete porovnávání vzorů pro tuto funkci, ale integrujete ho s jinými technikami. Můžete vytvořit výraz shody s jedním vzorem, který by odpovídal všem kombinacím směru, dne v týdnu a času. Výsledkem by byl složitý výraz. Bylo by těžké číst a obtížně pochopit. To znesnadňuje zajištění správnosti. Místo toho tyto metody zkombinujte, abyste vytvořili řazenou kolekci hodnot, které stručně popisují všechny tyto stavy. Pak pomocí porovnávání vzorů vypočítejte násobitel pro placenou linku. Řazená kolekce členů obsahuje tři diskrétní podmínky:

  • Den je buď pracovní den, nebo víkend.
  • Pásma času, kdy se vybírá placené číslo.
  • Směr je do města nebo mimo město.

Následující tabulka ukazuje kombinace vstupních hodnot a násobitele cen ve špičce:

Den Čas Směr Premium
Weekday ranní spěch směřující sem x 2,00
Weekday ranní spěch odchozí x 1,00
Weekday den směřující sem x 1,50
Weekday den odchozí x 1,50
Weekday večerní spěch směřující sem x 1,00
Weekday večerní spěch odchozí x 2,00
Weekday přes noc směřující sem x 0,75
Weekday přes noc odchozí x 0,75
Weekend ranní spěch směřující sem x 1,00
Weekend ranní spěch odchozí x 1,00
Weekend den směřující sem x 1,00
Weekend den odchozí x 1,00
Weekend večerní spěch směřující sem x 1,00
Weekend večerní spěch odchozí x 1,00
Weekend přes noc směřující sem x 1,00
Weekend přes noc odchozí x 1,00

Existují 16 různých kombinací těchto tří proměnných. Kombinací některých podmínek zjednodušíte konečný výraz switch.

Systém, který shromažďuje placené poplatky, používá DateTime strukturu pro čas, kdy byla vybrána bezplatná linka. Metody sestavení členů, které vytvářejí proměnné z předchozí tabulky. Následující funkce používá výraz přepínače porovnávání vzorů k vyjádření, zda DateTime představuje víkend nebo pracovní den:

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

Tato metoda je správná, ale je repetitious. Můžete ho zjednodušit, jak je znázorněno v následujícím kódu:

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

Dále přidejte podobnou funkci, která do bloků zařadí čas do kategorií:

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

Přidáte soukromou enum hodnotu, která převede každý časový rozsah na diskrétní hodnotu. GetTimeBand Metoda pak používá relační vzory a konjunkční or vzory. Relační vzor umožňuje testovat číselnou hodnotu pomocí <, >, <=nebo >=. Vzor or testuje, jestli výraz odpovídá jednomu nebo více vzorům. Pomocí vzoru můžete také and zajistit, aby výraz odpovídal dvěma odlišným vzorům a vzor not k otestování, že výraz neodpovídá vzoru.

Po vytvoření těchto metod můžete k výpočtu ceny Premium použít jiný switch výraz se vzorem řazené kolekce členů. Můžete vytvořit switch výraz se všemi 16 rameny:

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

Výše uvedený kód funguje, ale dá se zjednodušit. Všechny osm kombinací na víkend mají stejnou placenou linku. Všechny osm můžete nahradit následujícím řádkem:

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

Příchozí i odchozí provoz mají stejný násobitel během dne v týdnu a přes noc. Tyto čtyři přepínací ramena mohou být nahrazeny následujícími dvěma řádky:

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

Kód by měl po těchto dvou změnách vypadat jako následující kód:

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

Nakonec můžete odebrat dvě hodiny spěchu, které platí pravidelnou cenu. Jakmile tyto zbraně odeberete, můžete je nahradit false zahozením (_) v poslední rameni přepínače. Budete mít následující dokončenou metodu:

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

Tento příklad zvýrazní jednu z výhod porovnávání vzorů: větve vzorů se vyhodnocují v pořadí. Pokud je přeuspořádáte tak, aby starší větev zpracovávala jeden z vašich pozdějších případů, kompilátor vás upozorní na nedostupný kód. Tato jazyková pravidla usnadňují předchozí zjednodušení s jistotou, že se kód nezměnil.

Porovnávání vzorů zpřístupňuje některé typy kódu čitelnějším a nabízí alternativu k metodám orientovaným na objekty, když do tříd nemůžete přidat kód. Cloud způsobuje, že data a funkce jsou roztaženy. Tvar dat a operací na něm nejsou nutně popsány společně. V tomto kurzu jste spotřebovali existující data úplně jinak než původní funkce. Porovnávání vzorů vám umožnilo psát funkce, které tyto typy přebíjejí, i když jste je nemohli rozšířit.

Další kroky

Hotový kód si můžete stáhnout z úložiště dotnet/samples na GitHubu. Prozkoumejte vzory sami a přidejte tuto techniku do běžných programovacích aktivit. Učení těchto technik vám dává další způsob, jak přistupovat k problémům a vytvářet nové funkce.

Viz také