Руководство. Использование сопоставления шаблонов для создания управляемых типами и управляемых данными алгоритмов

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

Из этого руководства вы узнаете, как выполнять следующие задачи:

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

Необходимые компоненты

  • Мы рекомендуем Visual Studio для Windows или Mac. Вы можете скачать бесплатную версию на странице скачивания Visual Studio. Visual Studio включает пакет SDK для .NET.
  • Вы также можете использовать редактор Visual Studio Code . Вам потребуется установить последний пакет SDK для .NET отдельно.
  • Если вы предпочитаете другой редактор, необходимо установить последний пакет SDK для .NET.

В этом руководстве предполагается, что вы знакомы с C# и .NET, включая Visual Studio или .NET CLI.

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

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

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

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

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

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

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 передается этому методу. Шаблон 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 включает четыре разных случая:

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

Многие из этих вариантов являются примерами рекурсивных шаблонов. Например, в Car { Passengers: 1} показан шаблон константы внутри шаблона свойств.

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

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

Добавление цен с учетом пиковой нагрузки

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

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. Вместо этого для данной функции вы будете использовать сопоставление шаблонов, но вместе с другими методами. Вы можете создать одно выражение для сопоставления шаблонов, учитывающее все комбинации направления, дня недели и времени. Результат будет представлен в виде сложного выражения. Такое выражение затрудняет чтение и понимание кода. В таком случает будет сложнее обеспечить правильность результатов. Но вы можете объединить эти методы для создания набора значений, который кратко описывает все эти состояния. Затем, используйте сопоставление шаблонов, чтобы вычислить множитель для платы за проезд. Кортеж содержит три дискретные условия:

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

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

день Время Направление Premium
Weekday Утренний час пик входящий трафик x 2,00
Weekday Утренний час пик Выезд x 1,00
Weekday Дневное время входящий трафик x 1,50
Weekday Дневное время Выезд x 1,50
Weekday Вечерний час пик входящий трафик x 1,00
Weekday Вечерний час пик Выезд x 2,00
Weekday Ночное время входящий трафик x 0,75
Weekday Ночное время Выезд x 0,75
Выходные Утренний час пик входящий трафик x 1,00
Выходные Утренний час пик Выезд x 1,00
Выходные Дневное время входящий трафик x 1,00
Выходные Дневное время Выезд x 1,00
Выходные Вечерний час пик входящий трафик x 1,00
Выходные Вечерний час пик Выезд x 1,00
Выходные Ночное время входящий трафик x 1,00
Выходные Ночное время Выезд 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шаблоны. Реляционный шаблон позволяет проверить числовое значение с помощью операторов <, >, <= или >=. Шаблон 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. Изучите шаблоны самостоятельно и используйте эту методику во время написания кода. Это позволит вам подойти к решению проблем с другой стороны, чтобы создавать новые функции.

См. также