Kurz: Vytváření algoritmů pomocí porovnávání podle vzoru

Porovnávání vzorů umožňuje přidávat funkce pro typy definované v jiných knihovnách beze změny těchto typů. 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 návodu se naučíte, jak:

  • 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.

Prerequisites

Pokyny k instalaci

Ve Windows použijte tento konfigurační soubor WinGet k instalaci všech předpokladů. Pokud už máte něco nainstalovaného, WinGet tento krok přeskočí.

  1. Stáhněte soubor a poklikáním ho spusťte.
  2. Přečtěte si licenční smlouvu, zadejte ya po zobrazení výzvy k přijetí vyberte Enter.
  3. Pokud se na hlavním panelu zobrazí výzva řízení uživatelských účtů (UAC), povolte instalaci pokračovat.

Na jiných platformách je potřeba nainstalovat každou z těchto komponent samostatně.

  1. Stáhněte si doporučený instalační program ze stránky pro stažení sady .NET SDK a poklikáním ho spusťte. Stránka pro stažení zjistí vaši platformu a doporučí nejnovější instalační program pro vaši platformu.
  2. Stáhněte si nejnovější instalační program z domovské stránky Visual Studio Code a poklikáním ho spusťte. Tato stránka také zjistí vaši platformu a odkaz by měl být pro váš systém správný.
  3. Na stránce rozšíření C# DevKit klikněte na tlačítko Nainstalovat. Tím se otevře Visual Studio kód a zobrazí se dotaz, jestli chcete rozšíření nainstalovat nebo povolit. Vyberte "nainstalovat".

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 možná nebudete mít pod kontrolou typy, které představují příchozí data.

Klasický přístup k objektově orientovanému návrhu vyžaduje vytváření typů ve vaší aplikaci, které představují každý datový typ z těchto několika zdrojů dat. Pak vaše aplikace funguje s těmito novými typy, vytváří hierarchie dědičnosti, vytváří virtuální metody a implementuje abstrakce. Tyto techniky fungují a někdy jsou nejlepšími nástroji. Jindy můžete napsat méně kódu. V některých případech může oddělení dat od operací, které je používají, usnadnit čtení kódu.

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ář. Zjistíte, jak porovnávání vzorů/pattern matching představuje efektivní způsob, jak tato data využívat a zpracovávat způsoby, které nebyly součástí původního systému.

Představte si hlavní metropolitní oblast, která ke správě provozu využívá mýtné a ceny ve špičce. Píšete aplikaci, která počítá mýtné 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 můžete rychle načrtnout hierarchii objektů pro modelování tohoto systému. Vaše data ale pocházejí z více zdrojů, jako jsou jiné systémy pro správu registrací vozidel. Tyto systémy poskytují různé třídy pro modelování těchto dat a tyto systémy nesdílejí jediný objektový model. 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 jmenných prostorech. Nemůžete použít jinou společnou základní třídu než System.Object.

Návrhy pro shodu vzorů

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

  • 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. Mýtné placené vozidlem se liší podle různých typů vozidel, ale mýtné není hlavní 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ů mýtného

Nejzákladnější výpočet mýtného je založen 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 USD.

Vytvořte novou TollCalculator třídu a implementujte vzorové porovnávání u typu vozidla, abyste získali výši mýtného. 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. Příkaz switch začíná proměnnou vehicle v předchozím kódu, kterou následuje klíčové slovo switch. Dále se objevují všechny přepínací ramena uvnitř složených závorek. Výraz switch provádí další úpravy syntaxe obklopující příkaz switch. Klíčové case slovo je vynecháno a výsledek každé větve je výraz. Poslední dvě ramena ukazují novou funkci jazyka. Případ { } odpovídá jakémukoli nenulovému objektu, který neodpovídal žádnému dřívějšímu “armu”. Tato součást 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í nullvzor 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 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řidat ceny za obsazenost

Mýtná správa chce povzbuzovat vozidla k jízdě v maximálním obsazení. Rozhodnou se naúčtovat více, když mají vozidla méně cestujících, a podporují 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 méně než z 50% obsazené, platí navíc 2,00 $.
  • Autobusy, které jsou více než 90% plné, získají slevu 1,00 USD.

Tato pravidla můžete implementovat pomocí vzoru vlastností ve stejném výrazu switch. Vzor vlastnosti porovnává hodnotu vlastnosti s konstantní hodnotou. Property pattern zkoumá vlastnosti objektu po určení typu. Car {Passengers: 0} je sám o sobě rekurzivní vzor: vnější vzor vlastnosti obsahuje vnitřní konstantní vzor Car, který testuje hodnotu Passengers. Jeden případ se rozšiřuje 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 Car, pak zkontrolují hodnotu vlastnosti Passengers. Pokud se oba shodují, výraz se vyhodnotí a vrátí výsledek.

Případy pro taxi můžete rozšířit 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,

    // ...
};

Mýtná správa se nezajímá o počet osob v dodávkových vozech. Místo toho upraví částku mýtného na základě hmotnostní třídy nákladních vozidel následujícím způsobem:

  • Nákladní vozy nad 5000 lb se účtují navíc 5,00 USD.
  • Lehká nákladní vozidla pod 1360 kg mají slevu 2,00 USD.

Toto pravidlo můžete implementovat pomocí následujícího kódu:

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 větve přepínače. Pomocí klauzule when otestujte jiné podmínky než rovnost u vlastnosti. Po dokončení máte 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))
};

Jak už jsme uvedli dříve, tato ramena switche jsou rekurzivní vzory, které vnořují konstantní vzor do vzoru vlastnosti.

Tento kód můžete udělat méně opakujícím se použitím vnořených přepínačů. Car a Taxi mají oba čtyři různá ramena v předchozích příkladech. V obou případech můžete vytvořit vzor deklarace, který se propojí se vzorem konstanty. 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í ukázce vnoření výrazu switch do jiné větve switch znamená, že se neopakují větve Car a Taxi obsahující vnořené větve, které testují hodnotu vlastnosti. Tato technika se nepoužívá pro ramena Bus, DeliveryTruck, protože tato ramena testují rozsahy vlastnosti, nikoli diskrétní hodnoty.

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

Pro poslední funkci chce správce mýtného zavést časově citlivé ceny ve špičce. 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 %. V noci a časně ráno se poplatky snižují o 25 %. Během víkendu je to normální sazba bez ohledu na čas. Toto pravidlo můžete vyjádřit pomocí příkazů if a else pomocí následujícího kódu:

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ý. Je potřeba procházet všechny vstupní případy a vnořené příkazy if, abyste mohli kód analyzovat. 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ý odpovídá 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. Poté pomocí vyhledávání vzorů vypočítejte násobitel pro mýtné. N-tice obsahuje tři diskrétní podmínky.

  • Den je buď pracovní den, nebo víkend.
  • Časové období, kdy se vybírá mýto.
  • 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 Time Direction Premium
pracovní den ranní spěch inbound x 2,00
pracovní den ranní spěch outbound x 1,00
pracovní den denní doba inbound x 1,50
pracovní den denní doba outbound x 1,50
pracovní den večerní spěch inbound x 1,00
pracovní den večerní spěch outbound x 2,00
pracovní den přes noc inbound x 0,75
pracovní den přes noc outbound x 0,75
Víkend ranní spěch inbound x 1,00
Víkend ranní spěch outbound x 1,00
Víkend denní doba inbound x 1,00
Víkend denní doba outbound x 1,00
Víkend večerní spěch inbound x 1,00
Víkend večerní spěch outbound x 1,00
Víkend přes noc inbound x 1,00
Víkend přes noc outbound 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 přepínače.

Systém, který vybírá mýtné, používá DateTime strukturu pro zaznamenání času, kdy bylo mýtné vybráno. Vytvořte metody č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 opakující se. 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. Pak metoda GetTimeBand používá relační vzory a logické vzory. Relační vzor umožňuje testovat číselnou hodnotu pomocí <, >, <=nebo >=. Logické vzory kombinují jiné vzory: vzor or testuje, zda výraz odpovídá jednomu nebo více vzorům, vzor and testuje, zda výraz odpovídá dvěma odlišným vzorům, a vzor not testuje, zda výraz neodpovídá vzoru.

Po vytvoření těchto metod můžete k výpočtu cenové přirážky použít jiný switch výraz se vzorem pro n-tici – pozičním vzorem, který odpovídá jednotlivým prvkům n-tice. Můžete vytvořit switch výraz se všemi 16 větvemi:

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

Předchozí kód funguje, ale dá se zjednodušit. Všech osm kombinací na víkend má stejný poplatek. Všech 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 můžete nahradit 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 odeberte dvě časová pásma ve špičce, za která se účtuje běžná cena. Po odebrání těchto větví nahraďte false zahazovací větví (_) v poslední větvi přepínače. Získáte 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 žijí odděleně. 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é