Discriminated Unions в C#

от автора

Всем привет. Среди многих интересных концепций, имеющихся в 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; }

Данная реализация подходит, под описанные выше критерии:

  1. Ограниченный набор вариантов — все варанты выбора — внутри другого класса с приватным конструктором.

  2. Каждый вариант состоит из своего набора данных — каждый вариант это отдельный класс.

  3. Объединенные общим названием/подтипом — все наследуют базовый абстрактный класс.

В данной реализации я использовал 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *