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 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 все равно ругается из-за отсутствия условия по-умолчанию)

2b694b88589eb2f0cd2f7e8815fb4fae.png

Сравнение реализаций

C#

F#

Нахождение доступных вариантов

IDE (Варианты — классы-поля базового класса)

Теги (Enum)

Реализуемые интерфейсы

IEquatable

IEquatable

IStructuralEquatable

Создание новых объектов

Конструктор

Статический метод (New*)

Определение типа в райнтайме

Только рефлексия

Свойства для каждого варианта (Is*)

Создаваемые свойства

Get/Set

Get-only

Генерируемые методы сравнения

==, !=, Equals

Equals

Рекурсивное определение Discriminated Unions

Да, вариант выбора сделать абстрактным

Нет, определить другой DU выше и сделать вариантом выбора в текущем

Представление в IL

Базовый абстрактный класс с наследующими его варантами-реализациями

Хранение данных для каждого варианта

Свойства с backing field

Деконструкция полей

Есть

Примечания:

Выводы

Мой вариант основанный на record`ах сильно похож на тот, что генерируется компилятором F# (В чем-то даже превосходит).

Вариантов реализации много: на обычных классах, на структурах, partial классы.

Также преимуществом классовой реализации является возможность определения общих полей — в Discriminated Unions общие только свойства Tag и Is* для определения подтипа.

Если кому интересно как Discriminated Unions устроены более подробно, то существует пост на эту тему.

На этом у меня все. Если пропустил важные моменты, прошу поправить.

© Habrahabr.ru