Всем привет. Среди многих интересных концепций, имеющихся в F#, меня привлекли Discriminated Unions. Я задался вопросом, как их реализовать в C#, ведь в нем отсутствует поддержка (синтаксическая) типов объединений, и я решил найти способ их имитации.
Discriminated Unions — тип данных, представляющий собой размеченные объединения, каждый из которых может состоять из собственных типов данных (также именованных).
Идея в том, что мы имеем ограниченное количество вариантов выбора, и каждый вариант может состоять из своего набора данных, никак не связанных с другими, но все варинанты объединены общим подтипом.
Для создания своих Discriminated Unions будем использовать эту мысль
Реализация
«Эталоном» будет реализация на F#
type Worker = | Developer of KnownLanguages: string seq | Manager of MaintainedProjectsCount: int | Tester of UnitTestsPerHour: double
Теперь реализация на C#
public abstract record RecordWorker { private RecordWorker(){ } public record Developer(IEnumerable<string> KnownLanguages): RecordWorker { } public record Manager(int MaintainedProjectsCount) : RecordWorker; public record Tester(double UnitTestsPerHour) : RecordWorker; }
Данная реализация подходит, под описанные выше критерии:
-
Ограниченный набор вариантов — все варанты выбора — внутри другого класса с приватным конструктором.
-
Каждый вариант состоит из своего набора данных — каждый вариант это отдельный класс.
-
Объединенные общим названием/подтипом — все наследуют базовый абстрактный класс.
В данной реализации я использовал record, т.к. они позволяют написать меньше кода и по поведению очень похожи на Discriminated Unions.
Использование
Функция на F#, использующая наш тип:
let getWorkerInfo (worker: Worker) = match worker with | Developer knownLanguages -> $"Known languages: %s{String.Join(',', knownLanguages)}" | Manager maintainedProjectsCount -> $"Currently maintained projects count %i{maintainedProjectsCount}" | Tester unitTestsPerHour -> $"My testing speed is %f{unitTestsPerHour} unit tests per hour"
На C# можно переписать таким образом:
string GetWorkerInfo(Worker w) { return worker switch { Worker.Developer(var knownLanguages) => $"Known languages {string.Join(',', knownLanguages)}", Worker.Manager(var maintainedProjectsCount) => $"Currently maintained projects count {maintainedProjectsCount}", Worker.Tester(var unitTestsPerHour) => $"My testing speed is {unitTestsPerHour} unit tests per hour", _ => throw new ArgumentOutOfRangeException(nameof(worker), worker, null) }; }
Нам становятся доступны подсказки IDE (Rider все равно ругается из-за отсутствия условия по-умолчанию):

Сравнение реализаций
|
|
C# |
F# |
|
Нахождение доступных вариантов |
IDE (Варианты — классы-поля базового класса) |
Теги (Enum) |
|
Реализуемые интерфейсы |
IEquatable<Worker> |
IEquatable<Worker> IStructuralEquatable |
|
Создание новых объектов |
Конструктор |
Статический метод (New*) |
|
Определение типа в райнтайме |
Только рефлексия |
Свойства для каждого варианта (Is*) |
|
Создаваемые свойства |
Get/Set |
Get-only |
|
Генерируемые методы сравнения |
==, !=, Equals |
Equals |
|
Рекурсивное определение Discriminated Unions |
Да, вариант выбора сделать абстрактным |
Нет, определить другой DU выше и сделать вариантом выбора в текущем |
|
Представление в IL |
Базовый абстрактный класс с наследующими его варантами-реализациями |
|
|
Хранение данных для каждого варианта |
Свойства с backing field |
|
|
Деконструкция полей |
Есть |
|
Примечания:
-
Методы Is* для Discriminated Unions под капотом используют рефлексию.
Выводы
Мой вариант основанный на record`ах сильно похож на тот, что генерируется компилятором F# (В чем-то даже превосходит).
Вариантов реализации много: на обычных классах, на структурах, partial классы.
Также преимуществом классовой реализации является возможность определения общих полей — в Discriminated Unions общие только свойства Tag и Is* для определения подтипа.
Если кому интересно как Discriminated Unions устроены более подробно, то существует пост на эту тему.
На этом у меня все. Если пропустил важные моменты, прошу поправить.
ссылка на оригинал статьи https://habr.com/ru/post/684104/
Добавить комментарий