Урок: Создание алгоритмов с помощью сопоставления с образцом

Сопоставление шаблонов позволяет добавлять функции для типов, определенных в других библиотеках, без изменения этих типов. Еще один вариант использования шаблонов — создать функции, которые требуются для приложения и не являются фундаментальными функциями для расширяемого типа.

В этом руководстве вы узнаете, как:

  • распознавать случаи, в которых следует использовать сопоставление шаблонов;
  • использовать выражения сопоставления шаблонов для реализации поведения с учетом типов и значений свойств;
  • комбинировать сопоставление шаблонов и другие методы для создания полных алгоритмов.

Prerequisites

Инструкции по установке

В Windows данный файл конфигурации WinGet используется для установки всех необходимых компонентов. Если у вас уже установлено что-то, WinGet пропустит этот шаг.

  1. Скачайте файл и дважды щелкните его, чтобы запустить его.
  2. Прочитайте лицензионное соглашение, введите и, и выберите ввод при появлении запроса на принятие.
  3. Если на панели задач появится мигающий запрос контроля учетных записей пользователей (UAC), разрешите установку продолжить.

На других платформах необходимо установить каждый из этих компонентов отдельно.

  1. Скачайте рекомендуемый установщик на странице загрузки пакета SDK для .NET и дважды щелкните его, чтобы запустить его. Страница загрузки обнаруживает платформу и рекомендует последний установщик для вашей платформы.
  2. Скачайте последнюю версию установщика на домашней странице Visual Studio Code и дважды щелкните его, чтобы запустить его. Эта страница также обнаруживает платформу, а ссылка должна быть правильной для вашей системы.
  3. Нажмите кнопку "Установить" на странице расширения C# DevKit. Откроется код Visual Studio и запрашивается, нужно ли установить или включить расширение. Выберите "Установить".

Сценарии для сопоставления шаблонов

Современная разработка часто предусматривает использование данных из нескольких источников, а также представление информации и идей на основе этих данных в одном связном приложении. Вы и ваша команда не можете контролировать типы, представляющие входящие данные.

Классический подход к объектно-ориентированному проектированию предполагает создание типов в приложении, представляющих каждый тип данных из этих нескольких источников данных. Затем приложение работает с этими новыми типами, создает иерархии наследования, создает виртуальные методы и реализует абстракции. Эти методы работают, а иногда они лучшие инструменты. Но в некоторых случаях можно писать меньше кода. В некоторых случаях отделение данных от операций, которые их используют, может упростить чтение кода.

В этом руководстве вы создадите и изучите приложение, которое принимает входящие данные из нескольких внешних источников для одного сценария. Вы видите, как сопоставление шаблонов обеспечивает эффективный способ использования и обработки этих данных способами, которые не были частью исходной системы.

Рассмотрим крупный городской район, использующий дорожные сборы и тарификацию в часы пик для управления трафиком. Вы напишете приложение, которое рассчитывает плату за автомобиль в зависимости от его типа. Затем усовершенствования включают цены, основанные на количестве пассажиров в автомобиле. Дальнейшие улучшения добавляют ценообразование на основе времени и дня недели.

Из этого краткого описания можно быстро нарисовать иерархию объектов для моделирования этой системы. Однако данные приходят из нескольких источников, таких как другие системы управления регистрацией автомобилей. Эти системы предоставляют различные классы для моделирования этих данных, и эти системы не используют общую объектную модель. В этом руководстве вы используете эти упрощенные классы для моделирования данных автомобиля из этих внешних систем, как показано в следующем коде:

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

Скачать начальный код можно из репозитория GitHub dotnet/samples. Вы можете увидеть, что классы транспортных средств находятся в разных системах и находятся в разных пространствах имен. Вы не можете использовать общий базовый класс, отличный от System.Object.

Дизайн сопоставления шаблонов

Сценарий, используемый в этом руководстве, выделяет типы проблем, которые хорошо решаются с помощью сопоставления с образцом:

  • Объекты, с которыми вам нужно работать, не находятся в иерархии объектов, которая соответствует вашим целям. Возможно, вы работаете с классами, которые являются частью несвязанных систем.
  • Функции, которые вы добавляете, не является частью основной абстракции для этих классов. Плата за автомобиль изменяется в зависимости от его типа. При этом плата не является основной функцией этого автомобиля.

Если фигура данных и операции с данными не описаны вместе, функции сопоставления шаблонов в C# упрощают работу.

Внедрение основных расчетов дорожных сборов

Самый простой расчет базового сбора выполняется с учетом типа автомобиля:

  • Car стоит 2,00 доллара.
  • Taxi стоит $3,50 дол. США.
  • Bus — 5,00 долларов.
  • DeliveryTruck составляет 10,00 $.

Создайте класс TollCalculator и реализуйте сопоставление шаблонов по типу автомобиля, чтобы получить сумму сбора. В приведенном ниже примере кода показана начальная реализация класса 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))
    };
}

В предыдущем коде используется switch выражение (не то же самое, что switch инструкция), которое проверяет шаблон объявления. Выражение switch начинается с переменной vehicle в приведенном выше коде, за которой следует ключевое слово switch. Далее следуют все ветви switch внутри фигурных скобок. Выражение switchвносит другие уточнения в синтаксис, который окружает оператор switch. Ключевое слово case опущено, и результатом каждой ветви является выражение. Последние две ветви демонстрируют новую функцию языка. Случай { } соответствует любому ненулевому объекту, который не совпадает с предыдущей ветвью. Этот блок перехватывает неправильные типы, переданные этому методу. { } случай должен соответствовать случаям для каждого типа транспортного средства. Если порядок был бы обратным, случай { } имел бы преимущество. Наконец, null определяет, когда этому методу передается . Шаблон null может быть последним, так как другие шаблоны соответствуют только ненулевому объекту правильного типа.

Этот код можно протестировать с помощью следующего кода: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");
}

Этот код включен в начальный проект, но закомментирован. Удалите комментарии, и вы можете проверить то, что вы написали.

На этом примере можно понять, как шаблоны помогают создавать алгоритмы, в которых код и данные разделены. Выражение switch проверяет тип и создает различные значения в зависимости от результатов. Но это только начало.

Добавить ценообразование в зависимости от заполненности

Орган, взимающий сбор, поощряет передвижение автомобилей с максимальным количеством пассажиров. Они решают взимать больше, когда транспортные средства имеют меньше пассажиров, и поощрять полные транспортные средства, предлагая более низкие цены:

  • Автомобили и такси без пассажиров должны заплатить дополнительные 0,50 долл. США.
  • Автомобили и такси с двумя пассажирами получают скидку 0,50 долл. США.
  • Автомобили и такси с тремя пассажирами получают скидку 1,00 долл. США.
  • Если автобус заполнен менее чем на половину, взимается дополнительная плата — 2,00 дол. США.
  • Если автобус заполнен более чем на 90 %, действует скидка 1,00 дол. США.

Эти правила можно реализовать с помощью шаблона свойства в том же switch-выражении. Шаблон свойства позволяет сравнивать значение свойства с постоянным значением. шаблон свойства проверяет свойства объекта после определения типа. Car {Passengers: 0} сам по себе представляет собой рекурсивный шаблон: внешний шаблон свойства в Car содержит внутренний шаблон-константу, который проверяет значение Passengers. Один случай для Car включает четыре разных случая:

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,

    // ...
};

Первые три случая проверяют тип как Car, а затем проверяют значение свойства Passengers. Если оба условия выполняются, выражение вычисляется и возвращается.

Вы также можете расширить сценарии для такси аналогичным образом:

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,

    // ...
};

Затем реализуйте правила заполняемости, расширив случаи для автобусов, как показано в следующем примере:

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,

    // ...
};

Орган, взимающий сборы, не учитывает количество пассажиров в грузовых автомобилях, выполняющих доставку. Вместо этого сумма сбора корректируется с учетом веса грузовых автомобилей указанным ниже образом.

  • Для грузовых автомобилей весом более 2268 кг взимается дополнительная плата в размере 5,00 долл. США.
  • Для легких грузовиков весом до 1361 кг применяется скидка 2,00 долл. США.

Это правило можно реализовать с помощью следующего кода:

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

В предыдущем коде показано условие when ветки switch. Используйте условие when для тестирования условий, отличных от равенства, для свойства. По завершении у вас есть метод, который выглядит примерно так, как показано в следующем коде:

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

Как отмечалось ранее, эти ветви switch являются рекурсивными шаблонами, вкладывающими шаблон-константу в шаблон свойств.

Вы можете уменьшить количество повторяемых участков кода, использовав вложенные операторы switch. В предыдущих примерах объекты Car и Taxi имеют по четыре разные руки. В обоих случаях вы можете создать шаблон объявления, включаемый в шаблон константы. Пример использования этого способа показан в следующем коде:

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

В предыдущем примере вложение switch-выражения внутрь другой ветви switch означает, что вам не нужно повторять ветви Car и Taxi, содержащие вложенные ветви, которые проверяют значение свойства. Этот метод не используется для ветвей Bus и DeliveryTruck, так как эти ветви проверяют диапазоны значений свойства, а не отдельные значения.

Добавить тарифы на пиковые периоды

Для последней функции оператор платных дорог хочет добавить динамическое ценообразование в часы пик. Утром и вечером, когда дороги наиболее загружены, сумма сбора удваивается. Это правило влияет только на движение в одном направлении: при въезде в город утром и на выезде вечером в час пик. В другое время в течение рабочего дня плата увеличивается на 50 %. Поздно ночью и ранним утром плата уменьшается на 25 %. В выходные дни взимается стандартная плата, независимо от времени суток. Для выражения этого правила можно использовать ряд операторов if и else, используя следующий код:

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

Предыдущий код работает правильно, но не является читаемым. Чтобы понять код, необходимо последовательно пройти через все входные примеры и вложенные операторы if. Вместо этого для данной функции вы будете использовать сопоставление шаблонов, но вместе с другими методами. Вы можете создать одно выражение сопоставления шаблонов, которое учитывает все сочетания направления, дня недели и времени. Результат будет представлен в виде сложного выражения. Было бы трудно читать и трудно понять. В таком случает будет сложнее обеспечить правильность результатов. Но вы можете объединить эти методы для создания набора значений, который кратко описывает все эти состояния. Затем, используйте сопоставление шаблонов, чтобы вычислить множитель для платы за проезд. Кортеж содержит три дискретные условия:

  • День является либо будним, либо выходным.
  • Диапазон времени, за который взимается плата.
  • Направление в город или из города.

В таблице ниже показаны комбинации входных значений и множителя для цены в часы пик:

День Время Direction Premium
Будний день Утренний час пик inbound x 2,00
Будний день Утренний час пик outbound x 1,00
Будний день дневное время inbound x 1,50
Будний день дневное время outbound x 1,50
Будний день Вечерний час пик inbound x 1,00
Будний день Вечерний час пик outbound x 2,00
Будний день Ночное время inbound x 0,75
Будний день Ночное время outbound x 0,75
Выходные Утренний час пик inbound x 1,00
Выходные Утренний час пик outbound x 1,00
Выходные дневное время inbound x 1,00
Выходные дневное время outbound x 1,00
Выходные Вечерний час пик inbound x 1,00
Выходные Вечерний час пик outbound x 1,00
Выходные Ночное время inbound x 1,00
Выходные Ночное время outbound x 1,00

Для этих трех переменных существует 16 разных комбинаций. Объединяя некоторые условия, вы упрощаете окончательное switch-выражение.

В системе для сбора платы структура DateTime используется для определения времени сбора. Создайте методы-члены, которые создают переменные на основе предыдущей таблицы. В следующей функции для сопоставления шаблонов используется выражение switch, позволяющее определить, соответствует ли DateTime выходным или рабочим дням недели:

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

Этот метод работает правильно, но фрагменты кода повторяются. Вы можете упростить его, как показано в следующем примере:

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

Затем добавьте аналогичную функцию, чтобы категоризировать время на блоки:

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

Чтобы преобразовать каждый диапазон времени в дискретное значение, добавьте приватное enum. GetTimeBand Затем метод использует реляционные шаблоны и логические шаблоны. Реляционный шаблон позволяет проверить числовое значение с помощью операторов <, >, <= или >=. Логические шаблоны объединяют другие шаблоны: or-шаблон проверяет, соответствует ли выражение одному или нескольким шаблонам, and-шаблон проверяет, что выражение соответствует двум разным шаблонам, а not-шаблон проверяет, что выражение не соответствует шаблону.

После создания этих методов можно использовать другое switch выражение с шаблоном кортежа — позиционный шаблон, соответствующий каждому элементу кортежа, для вычисления ценовой надбавки. Вы можете записать выражение switch со всеми 16 ветвями.

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

Предыдущий код работает, но его можно упростить. Плата за все восемь комбинаций на выходные дни одинаковая. Вы можете заменить все восемь вариантов одной строкой:

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

Для входящего и исходящего трафика используется одинаковый множитель в дневное и ночное время в рабочие дни. Эти четыре рычага переключателя можно заменить следующими двумя строками:

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

После этих двух изменений код должен выглядеть как показано ниже:

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

Наконец, удалите два периода часа пик, по которым действует обычная цена. После удаления этих ветвей замените false на отбрасывание (_) в последней ветви switch. Вы получите следующий готовый метод:

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

В этом примере продемонстрировано одно из преимуществ сопоставления шаблонов: ветви шаблона вычисляются по порядку. Если поменять их местами так, чтобы более ранняя ветвь обрабатывала один из более поздних случаев, компилятор предупредит вас о недостижимом коде. Эти правила языка облегчают реализацию упрощений, выполненных выше, и гарантируют, что код не изменится.

Сопоставление шаблонов делает код некоторых типов более удобочитаемым и предлагает альтернативу объектно-ориентированным методам, когда добавление кода в классы невозможно. Разделение облака приводит к тому, что данные и функциональность существуют отдельно. Форма данных и операции с ними не обязательно описываются вместе. В этом руководстве вы использовали существующие данные совершенно другим образом, чем они предназначены для использования изначально. Сопоставление шаблонов дало вам возможность создавать функциональность, которая переопределяла эти типы, даже если вы не могли их расширить.

Дальнейшие действия

Скачать готовый код можно из репозитория GitHub dotnet/samples. Изучите шаблоны самостоятельно и используйте эту методику во время написания кода. Изучение этих методов дает вам возможность по-новому подойти к решению проблем и создавать новую функциональность.

См. также