Pull to refresh

Discriminated Unions в C#

Reading time3 min
Views10K

Всем привет. Среди многих интересных концепций, имеющихся в 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 Worker
{   
    private Worker(){ }
    public record Developer(IEnumerable<string> KnownLanguages): Worker { }

    public record Manager(int MaintainedProjectsCount) : Worker;

    public record Tester(double UnitTestsPerHour) : Worker;
}

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

  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 worker)
{
    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 устроены более подробно, то существует пост на эту тему.

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 14: ↑14 and ↓0+14
Comments13

Articles