Руководство. Использование сопоставления шаблонов для создания управляемых типами и управляемых данными алгоритмов
Вы можете написать функции, которые работают так, будто вы расширили типы, которые могут быть в других библиотеках. Еще один вариант использования шаблонов — создать функции, которые требуются для приложения и не являются фундаментальными функциями для расширяемого типа.
Из этого руководства вы узнаете, как выполнять следующие задачи:
- распознавать случаи, в которых следует использовать сопоставление шаблонов;
- использовать выражения сопоставления шаблонов для реализации поведения с учетом типов и значений свойств;
- комбинировать сопоставление шаблонов и другие методы для создания полных алгоритмов.
Предварительные условия
- Последняя версия .NET SDK
- Visual Studio Code редактор
- C# DevKit
Инструкции по установке
В Windows этот файл конфигурации WinGet используется для установки всех необходимых компонентов. Если у вас уже установлено что-то, WinGet пропустит этот шаг.
- Скачайте файл и дважды щелкните его, чтобы запустить его.
- Прочитайте лицензионное соглашение, введите и, и выберите ввод при появлении запроса на принятие.
- Если на панели задач появится мигающий запрос контроля учетных записей пользователей (UAC), разрешите установку продолжить.
На других платформах необходимо установить каждый из этих компонентов отдельно.
- Скачайте рекомендуемый установщик на странице загрузки пакета SDK для .NET и дважды щелкните его, чтобы запустить его. Страница загрузки обнаруживает платформу и рекомендует последний установщик для вашей платформы.
- Скачайте последнюю версию установщика на домашней странице Visual Studio Code и дважды щелкните его, чтобы запустить его. Эта страница также обнаруживает платформу, а ссылка должна быть правильной для вашей системы.
- Нажмите кнопку "Установить" на странице расширения C# DevKit. Откроется код Visual Studio и запрашивается, нужно ли установить или включить расширение. Выберите "Установить".
В этом руководстве предполагается, что вы знакомы с 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 %. В выходные дни взимается стандартная плата, независимо от времени суток. Вы можете использовать ряд 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
. Вместо этого для данной функции вы будете использовать сопоставление шаблонов, но вместе с другими методами. Вы можете создать одно выражение для сопоставления шаблонов, учитывающее все комбинации направления, дня недели и времени. Результат будет представлен в виде сложного выражения. Было бы трудно читать и трудно понять. В таком случает будет сложнее обеспечить правильность результатов. Но вы можете объединить эти методы для создания набора значений, который кратко описывает все эти состояния. Затем, используйте сопоставление шаблонов, чтобы вычислить множитель для платы за проезд. Кортеж содержит три дискретные условия:
- День является либо будним, либо выходным.
- Диапазон времени, за который взимается плата.
- Направление в город или за город.
В таблице ниже показаны комбинации входных значений и множителя для цены в часы пик:
день | Время | Направление | Премиум |
---|---|---|---|
Будний день | Утренний час пик | входящий трафик | x 2,00 |
Будний день | Утренний час пик | Исходящий | x 1,00 |
будний день | Дневное время | входящий трафик | x 1,50 |
Будний день | Дневное время | Исходящий | x 1,50 |
Будний день | Вечерний час пик | входящий трафик | x 1,00 |
будний день | Вечерний час пик | Исходящий | x 2,00 |
Будний день | Ночное время | входящий трафик | x 0,75 |
Будний день | Ночное время | Исходящий | 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. Изучите шаблоны самостоятельно и используйте эту методику во время написания кода. Изучение этих методов дает вам возможность по-новому подойти к решению проблем и создавать новую функциональность.