Algoritmy a programování IV

Týden 6

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

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

Klasický objektově orientovaný návrh by volal vytvoř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 to nejlepší nástroje. Jindy můžete psát méně kódu. Srozumitelnější kód můžete napsat pomocí technik, které oddělují data od operací, které s nimi pracují.

V tomto kurzu vytvoříte a prozkoumáte aplikaci, která přebírá příchozí data z několika externích zdrojů pro jeden scénář. Uvidíte, jak porovnávání vzorů poskytuje 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á placené poplatky a ceny ve špičce. Napíšete aplikaci, která vypočítá mýtné pro vozidlo na základě jeho typu. Pozdější vylepšení zahrnují ceny na základě počtu cestujících 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 však pocházejí z různých zdrojů, jako jsou jiné systémy správy registrací vozidel. Tyto systémy poskytují různé třídy pro modelování těchto dat a vy nemáte jediný objektový model, který můžete použít. V tomto kurzu použijete tyto zjednodušené třídy k modelování dat vozidla 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ů. Nelze použít žádnou jinou System.Object společnou základní třídu.

Návrhy porovnávání vzorů

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

  • Objekty, se kterými potřebujete pracovat, nejsou v hierarchii objektů, která odpovídá 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 pro tyto třídy. Mýto placené vozidlem se mění pro různé typy vozidel, ale není základní funkcí vozidla.

Pokud tvar dat a operace 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ýta

Nejzákladnější výpočet mýta závisí pouze na typu vozidla:

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

Vytvořte novou TollCalculator třídu a implementujte porovnávání vzorů pro typ vozidla, abyste získali částku placené. Následující kód ukazuje počáteční implementaci TollCalculator.



        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))
    };
}
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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 deklaraceVýraz switch začíná proměnnou v vehicle předchozím kódu následovanou klíčovým slovemswitch. Dále přichází všechna spínací ramena uvnitř složených závorek. Výraz switch provede další vylepšení syntaxe, která příkaz obklopuje switch . Klíčové case slovo je vynecháno a výsledkem každého armu je výraz. Poslední dvě ramena ukazují novou funkci jazyka. Případ { } odpovídá jakémukoli objektu, který není null, který se neshodoval s dřívějším armem. Toto rameno 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 se pořadí obrátilo, { } případ by měl přednost. Nakonec konstantní vzor zjistí, null kdy null je předán této metodě. Vzor null může být poslední, protože ostatní vzory odpovídají pouze objektu správného typu, který není null.

Tento kód můžete otestovat pomocí následujícího kódu v nástroji 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í úvodní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 vytvořit algoritmy, ve kterých jsou kód a data oddělená. Výraz switch testuje typ a na základě výsledků vytvoří různé hodnoty. To je jen začátek.

Přidání cen obsazenosti

Mýtný úřad chce nabádnout vozidla k maximální kapacitě. Rozhodli se účtovat vyšší poplatky, pokud mají vozidla méně cestujících, a podporovat kompletní vozidla tím, že nabízí nižší ceny:

  • Auta a taxi 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 taxíky se třemi nebo více cestujícími získají slevu 1,00 USD.
  • Autobusy, které jsou méně než 50% plné, platí navíc 2,00 USD.
  • Autobusy, které jsou více než 90% plné, dostanou slevu 1,00 USD.

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


 2.00m + 0.50m,
    Car {Passengers: 1} => 2.0m,
    Car {Passengers: 2} => 2.0m - 0.50m,
    Car                 => 2.00m - 1.0m,    // ...
};
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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 Cara pak zkontrolujte hodnotu Passengers vlastnosti. Pokud se oba shodují, tento výraz se vyhodnotí a vrátí.

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


 3.50m + 1.00m,
    Taxi {Fares: 1}  => 3.50m,
    Taxi {Fares: 2}  => 3.50m - 0.50m,
    Taxi             => 3.50m - 1.00m,    // ...
};
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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:


 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,    // ...
};
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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ý úřad se nezabývá počtem cestujících v nákladních vozidlech. Místo toho upraví výši mýtného na základě hmotnostní třídy nákladních vozů následujícím způsobem:

  • Nákladní vozy nad 5000 liber se účtují za příplatek 5,00 USD.
  • Lehké nákladní vozy do 3000 liber mají slevu 2,00 USD.

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


 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
    DeliveryTruck => 10.00m,
};
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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 zobrazuje klauzuli when spínacího ramene. Klauzuli when použijete k testování jiných podmínek než rovnosti u vlastnosti. Po dokončení budete mít metodu, která vypadá podobně jako následující kód:


 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))
};
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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))
};

Mnoho z těchto spínacích ramen je příkladem rekurzivních vzorůCar { Passengers: 1} Například zobrazuje konstantní vzor uvnitř vzoru vlastnosti.

Pomocí vnořených přepínačů můžete tento kód zmenšit. Taxi Oba Car mají v předchozích příkladech čtyři různá ramena. V obou případech můžete vytvořit vzor deklarace, který je součástí konstantního vzoru. Tato technika je znázorněna v následujícím kódu:



    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))
    };
" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border: 0px; padding: 0px; line-height: 1.3571; display: block; position: relative;">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 použití rekurzivního výrazu znamená, že neopakujete Car ramena a Taxi obsahující podřízené paže, které testují hodnotu vlastnosti. Tato technika se nepoužívá pro ramena Bus a DeliveryTruck , protože tyto ramena testují rozsahy pro vlastnost, nikoli diskrétní hodnoty.