Дискриминационный союз в C #

93

[Примечание: этот вопрос имел первоначальное название « Объединение в стиле C (ish) в C # », но, как мне сообщил комментарий Джеффа, очевидно, эта структура называется «размеченным объединением»]

Извините за многословность этого вопроса.

Есть несколько похожих вопросов, которые я могу задать в SO, но они, похоже, сосредоточены на преимуществах объединения памяти или использовании его для взаимодействия. Вот пример такого вопроса .

Мое желание иметь что-то типа профсоюзов несколько иное.

В данный момент я пишу код, который генерирует объекты, похожие на этот

public class ValueWrapper
{
    public DateTime ValueCreationDate;
    // ... other meta data about the value

    public object ValueA;
    public object ValueB;
}

Довольно сложный материал, я думаю, вы согласитесь. Дело в том, что ValueAможет быть только несколько определенных типов (скажем string, intи Foo(который является классом), а ValueBможет быть еще один небольшой набор типов. Мне не нравится рассматривать эти значения как объекты (я хочу теплого, уютного ощущения кодирование с некоторой степенью безопасности типов).

Итак, я подумал о написании тривиального небольшого класса-оболочки, чтобы выразить тот факт, что ValueA логически является ссылкой на определенный тип. Я позвонил в класс, Unionпотому что то, что я пытаюсь достичь, напомнило мне концепцию объединения в C.

public class Union<A, B, C>
{
    private readonly Type type; 
    public readonly A a;
    public readonly B b;
    public readonly C c;

    public A A{get {return a;}}
    public B B{get {return b;}}
    public C C{get {return c;}}

    public Union(A a)
    {
        type = typeof(A);
        this.a = a;
    }

    public Union(B b)
    {
        type = typeof(B);
        this.b = b;
    }

    public Union(C c)
    {
        type = typeof(C);
        this.c = c;
    }

    /// <summary>
    /// Returns true if the union contains a value of type T
    /// </summary>
    /// <remarks>The type of T must exactly match the type</remarks>
    public bool Is<T>()
    {
        return typeof(T) == type;
    }

    /// <summary>
    /// Returns the union value cast to the given type.
    /// </summary>
    /// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
    public T As<T>()
    {
        if(Is<A>())
        {
            return (T)(object)a;    // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types? 
            //return (T)x;          // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
        }

        if(Is<B>())
        {
            return (T)(object)b; 
        }

        if(Is<C>())
        {
            return (T)(object)c; 
        }

        return default(T);
    }
}

Использование этого класса ValueWrapper теперь выглядит так

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}

чего-то вроде того, чего я хотел достичь, но мне не хватает одного довольно важного элемента - это принудительная проверка типа компилятором при вызове функций Is и As, как показано в следующем коде

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }

IMO Недопустимо спрашивать ValueA, является ли это a, charпоскольку в его определении ясно сказано, что это не так - это ошибка программирования, и я хотел бы, чтобы компилятор уловил это. [Также, если бы я мог сделать это правильно, то (надеюсь) я бы тоже получил intellisense - что было бы благом.]

Чтобы добиться этого, я хотел бы сказать компилятору, что тип Tможет быть одним из A, B или C

    public bool Is<T>() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 

Кто-нибудь знает, возможно ли то, чего я хочу достичь? Или я просто глуп, что вообще написал этот класс?

Заранее спасибо.

Крис Фьютрелл
источник
3
Объединения в C могут быть реализованы в C # для типов значений с помощью StructLayout(LayoutKind.Explicit)и FieldOffset. Конечно, этого нельзя сделать со ссылочными типами. То, что вы делаете, совсем не похоже на C Union.
Брайан
5
Это часто называют размеченным союзом .
Джефф Харди,
Спасибо, Джефф - я не знал об этом термине, но это в значительной степени именно то, чего я хочу достичь
Крис Фьютрелл
7
Возможно, вы ищете не тот ответ, но задумывались ли вы о F #? В нем есть типобезопасные объединения и сопоставление с образцом, запеченные прямо в языке, что намного проще для представления объединений, чем с C #.
Juliet
1
Другое название размеченного объединения - тип суммы.
cdiggins 05

Ответы:

114

Мне не очень нравятся представленные выше решения для проверки типов и приведения типов, поэтому вот 100% типобезопасное объединение, которое вызовет ошибки компиляции, если вы попытаетесь использовать неправильный тип данных:

using System;

namespace Juliet
{
    class Program
    {
        static void Main(string[] args)
        {
            Union3<int, char, string>[] unions = new Union3<int,char,string>[]
                {
                    new Union3<int, char, string>.Case1(5),
                    new Union3<int, char, string>.Case2('x'),
                    new Union3<int, char, string>.Case3("Juliet")
                };

            foreach (Union3<int, char, string> union in unions)
            {
                string value = union.Match(
                    num => num.ToString(),
                    character => new string(new char[] { character }),
                    word => word);
                Console.WriteLine("Matched union with value '{0}'", value);
            }

            Console.ReadLine();
        }
    }

    public abstract class Union3<A, B, C>
    {
        public abstract T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h);
        // private ctor ensures no external classes can inherit
        private Union3() { } 

        public sealed class Case1 : Union3<A, B, C>
        {
            public readonly A Item;
            public Case1(A item) : base() { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return f(Item);
            }
        }

        public sealed class Case2 : Union3<A, B, C>
        {
            public readonly B Item;
            public Case2(B item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return g(Item);
            }
        }

        public sealed class Case3 : Union3<A, B, C>
        {
            public readonly C Item;
            public Case3(C item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return h(Item);
            }
        }
    }
}
Джульетта
источник
3
Ага, если вам нужны типизированные дискриминированные союзы, вам понадобятся match, и это такой же хороший способ получить его, как и любой другой.
Павел Минаев
21
И если весь этот шаблонный код вас не устраивает, вы можете попробовать эту реализацию, которая вместо этого явно помечает случаи: pastebin.com/EEdvVh2R . Между прочим, этот стиль очень похож на то, как F # и OCaml представляют союзы внутри.
Juliet
4
Мне нравится более короткий код Джульетты, но что, если типы <int, int, string>? Как бы вы назвали второй конструктор?
Роберт Джеппесен,
2
Я не знаю, как у этого нет 100 голосов. Это красота!
Паоло Фалабелла
6
@nexus рассмотрите этот тип в F #:type Result = Success of int | Error of int
AlexFoxGill
33

Мне нравится направление принятого решения, но оно плохо масштабируется для объединений более трех элементов (например, объединение 9 элементов потребует 9 определений классов).

Вот еще один подход, который также на 100% безопасен для типов во время компиляции, но его легко развить до больших объединений.

public class UnionBase<A>
{
    dynamic value;

    public UnionBase(A a) { value = a; } 
    protected UnionBase(object x) { value = x; }

    protected T InternalMatch<T>(params Delegate[] ds)
    {
        var vt = value.GetType();    
        foreach (var d in ds)
        {
            var mi = d.Method;

            // These are always true if InternalMatch is used correctly.
            Debug.Assert(mi.GetParameters().Length == 1);
            Debug.Assert(typeof(T).IsAssignableFrom(mi.ReturnType));

            var pt = mi.GetParameters()[0].ParameterType;
            if (pt.IsAssignableFrom(vt))
                return (T)mi.Invoke(null, new object[] { value });
        }
        throw new Exception("No appropriate matching function was provided");
    }

    public T Match<T>(Func<A, T> fa) { return InternalMatch<T>(fa); }
}

public class Union<A, B> : UnionBase<A>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb) { return InternalMatch<T>(fa, fb); }
}

public class Union<A, B, C> : Union<A, B>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc) { return InternalMatch<T>(fa, fb, fc); }
}

public class Union<A, B, C, D> : Union<A, B, C>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd) { return InternalMatch<T>(fa, fb, fc, fd); }
}

public class Union<A, B, C, D, E> : Union<A, B, C, D>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    public Union(E e) : base(e) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd, Func<E, T> fe) { return InternalMatch<T>(fa, fb, fc, fd, fe); }
}

public class DiscriminatedUnionTest : IExample
{
    public Union<int, bool, string, int[]> MakeUnion(int n)
    {
        return new Union<int, bool, string, int[]>(n);
    }

    public Union<int, bool, string, int[]> MakeUnion(bool b)
    {
        return new Union<int, bool, string, int[]>(b);
    }

    public Union<int, bool, string, int[]> MakeUnion(string s)
    {
        return new Union<int, bool, string, int[]>(s);
    }

    public Union<int, bool, string, int[]> MakeUnion(params int[] xs)
    {
        return new Union<int, bool, string, int[]>(xs);
    }

    public void Print(Union<int, bool, string, int[]> union)
    {
        var text = union.Match(
            n => "This is an int " + n.ToString(),
            b => "This is a boolean " + b.ToString(),
            s => "This is a string" + s,
            xs => "This is an array of ints " + String.Join(", ", xs));
        Console.WriteLine(text);
    }

    public void Run()
    {
        Print(MakeUnion(1));
        Print(MakeUnion(true));
        Print(MakeUnion("forty-two"));
        Print(MakeUnion(0, 1, 1, 2, 3, 5, 8));
    }
}
cdiggins
источник
+1 Это должно получить больше одобрений; Мне нравится, как вы сделали его достаточно гибким, чтобы допускать союзы всех видов.
Поль д'Ост
+1 за гибкость и краткость вашего решения. Однако есть некоторые детали, которые меня беспокоят. Я
опубликую
1
1. Использование отражения может привести к слишком большому снижению производительности в некоторых сценариях, учитывая, что дискриминируемые объединения из-за их фундаментальной природы могут использоваться очень часто.
stakx - больше не участвует
4
2. Использование dynamic& generics в UnionBase<A>цепочке наследования кажется ненужным. Сделать UnionBase<A>необщего, убить конструктор принимая Aи сделать (что это в любом случае, нет никаких дополнительное преимущество в объявлении его ). Затем создайте каждый класс непосредственно из . Это имеет то преимущество, что будет показан только правильный метод. (Как и сейчас, например, выявляет перегрузку, которая гарантированно вызовет исключение, если заключенное значение не является . Этого не должно происходить.)valueobjectdynamicUnion<…>UnionBaseMatch<T>(…)Union<A, B>Match<T>(Func<A, T> fa)A
stakx - больше не вносит свой вклад
3
Вы можете найти мою библиотеку OneOf полезной, она более или менее делает это, но есть на Nuget :) github.com/mcintyre321/OneOf
mcintyre321
20

Я написал несколько сообщений в блоге на эту тему, которые могут быть полезны:

Скажем , у вас есть корзина сценарий с тремя состояниями: «Пустой», «Активный» и «Paid», каждый с разным поведением.

  • Вы создаете ICartStateинтерфейс, который является общим для всех состояний (и это может быть просто пустой интерфейс маркера)
  • Вы создаете три класса, реализующие этот интерфейс. (Классы не обязательно должны быть в отношениях наследования)
  • Интерфейс содержит метод «сворачивания», с помощью которого вы передаете лямбда-выражение для каждого состояния или случая, которые вам необходимо обработать.

Вы можете использовать среду выполнения F # из C #, но в качестве более легкой альтернативы я написал небольшой шаблон T4 для генерации подобного кода.

Вот интерфейс:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}

А вот и реализация:

class CartStateEmpty : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the empty state, so invoke cartStateEmpty 
      return cartStateEmpty(this);
  }
}

class CartStateActive : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the active state, so invoke cartStateActive
      return cartStateActive(this);
  }
}

class CartStatePaid : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the paid state, so invoke cartStatePaid
      return cartStatePaid(this);
  }
}

Теперь предположим, что вы расширили CartStateEmptyи CartStateActiveс помощью AddItemметода, который не реализуется CartStatePaid.

А также предположим, что у CartStateActiveнего есть Payметод, которого нет в других штатах.

Тогда вот код, который показывает его использование - добавление двух предметов и оплата корзины:

public ICartState AddProduct(ICartState currentState, Product product)
{
    return currentState.Transition(
        cartStateEmpty => cartStateEmpty.AddItem(product),
        cartStateActive => cartStateActive.AddItem(product),
        cartStatePaid => cartStatePaid // not allowed in this case
        );

}

public void Example()
{
    var currentState = new CartStateEmpty() as ICartState;

    //add some products 
    currentState = AddProduct(currentState, Product.ProductX);
    currentState = AddProduct(currentState, Product.ProductY);

    //pay 
    const decimal paidAmount = 12.34m;
    currentState = currentState.Transition(
        cartStateEmpty => cartStateEmpty,  // not allowed in this case
        cartStateActive => cartStateActive.Pay(paidAmount),
        cartStatePaid => cartStatePaid     // not allowed in this case
        );
}    

Обратите внимание, что этот код полностью типизирован - нигде нет приведений или условных выражений, а также ошибок компилятора, если вы попытаетесь заплатить, скажем, за пустую корзину.

Grundoon
источник
Интересный вариант использования. Для меня реализация размеченных объединений на самих объектах становится довольно многословной. Вот альтернатива в функциональном стиле, в которой используются выражения переключения, основанные на вашей модели: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Вы можете видеть, что DU на самом деле не нужны, если существует только один «счастливый» путь, но они становятся очень полезными, когда метод может возвращать тот или иной тип, в зависимости от правил бизнес-логики.
Дэвид Кучча,
13

Я написал библиотеку для этого на https://github.com/mcintyre321/OneOf

Установить пакет OneOf

В нем есть общие типы для выполнения DU, например, OneOf<T0, T1>полностью OneOf<T0, ..., T9>. У каждого из них есть оператор .Matchи .Switchоператор, который можно использовать для безопасного типизированного поведения компилятора, например:

``

OneOf<string, ColorName, Color> backgroundColor = getBackground(); 
Color c = backgroundColor.Match(
    str => CssHelper.GetColorFromString(str),
    name => new Color(name),
    col => col
);

``

mcintyre321
источник
7

Я не уверен, что полностью понимаю вашу цель. В C объединение - это структура, которая использует одни и те же ячейки памяти для более чем одного поля. Например:

typedef union
{
    float real;
    int scalar;
} floatOrScalar;

floatOrScalarСоюз может быть использован как поплавок, или Int, но оба они потребляют один и тот же объем памяти. Изменение одного меняет другое. Вы можете добиться того же с помощью структуры на C #:

[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
    [FieldOffset(0)]
    public float Real;
    [FieldOffset(0)]
    public int Scalar;
}

В приведенной выше структуре используется всего 32 бита, а не 64 бита. Это возможно только со структурой. Приведенный выше пример является классом и, учитывая характер среды CLR, не дает никаких гарантий относительно эффективности памяти. Если вы меняете a Union<A, B, C>с одного типа на другой, вы не обязательно повторно используете память ... скорее всего, вы выделяете новый тип в куче и отбрасываете другой указатель в objectполе поддержки . В отличие от настоящего объединения , ваш подход может фактически вызвать большее количество разметки кучи, чем если бы вы не использовали свой тип Union.

Jrista
источник
Как я уже упоминал в своем вопросе, моей мотивацией была не лучшая эффективность памяти. Я изменил заголовок вопроса, чтобы лучше отразить мою цель - первоначальное название «C (ish) union», задним числом, вводит в заблуждение
Крис Фьютрелл
Дискриминационный союз имеет гораздо больше смысла для того, что вы пытаетесь сделать. Что касается проверки во время компиляции ... Я бы посмотрел на .NET 4 и Code Contracts. С помощью Code Contracts может быть возможно принудительное исполнение Contract.Requires во время компиляции, которое обеспечивает выполнение ваших требований к оператору .Is <T>.
jrista
Думаю, мне все еще нужно поставить под сомнение использование союза в общей практике. Даже в C / C ++ союзы - вещь рискованная, и их нужно использовать с особой осторожностью. Мне любопытно, почему вам нужно привнести такую ​​конструкцию в C # ... какую ценность вы ощущаете от этого?
jrista
2
char foo = 'B';

bool bar = foo is int;

Это приводит к предупреждению, а не к ошибке. Если вы ищете для ваших Isи Asфункций , которые будут аналогов для C # операторов, то вы не должны ограничивать их таким образом в любом случае.

Адам Робинсон
источник
2

Если вы разрешаете несколько типов, вы не можете обеспечить безопасность типов (если типы не связаны).

Вы не можете и не сможете добиться какой-либо безопасности типов, вы можете достичь безопасности байтовых значений только с помощью FieldOffset.

Было бы гораздо разумнее иметь общий ValueWrapper<T1, T2>с T1 ValueAи T2 ValueB, ...

PS: когда говорят о безопасности типов, я имею в виду безопасность типов во время компиляции.

Если вам нужна оболочка кода (выполняющая бизнес-логику для модификаций, вы можете использовать что-то вроде:

public class Wrapper
{
    public ValueHolder<int> v1 = 5;
    public ValueHolder<byte> v2 = 8;
}

public struct ValueHolder<T>
    where T : struct
{
    private T value;

    public ValueHolder(T value) { this.value = value; }

    public static implicit operator T(ValueHolder<T> valueHolder) { return valueHolder.value; }
    public static implicit operator ValueHolder<T>(T value) { return new ValueHolder<T>(value); }
}

Для простого выхода вы можете использовать (у него проблемы с производительностью, но это очень просто):

public class Wrapper
{
    private object v1;
    private object v2;

    public T GetValue1<T>() { if (v1.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v1; }
    public void SetValue1<T>(T value) { v1 = value; }

    public T GetValue2<T>() { if (v2.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v2; }
    public void SetValue2<T>(T value) { v2 = value; }
}

//usage:
Wrapper wrapper = new Wrapper();
wrapper.SetValue1("aaaa");
wrapper.SetValue2(456);

string s = wrapper.GetValue1<string>();
DateTime dt = wrapper.GetValue1<DateTime>();//InvalidCastException
Ярослав Яндек
источник
Ваше предложение сделать ValueWrapper универсальным кажется очевидным ответом, но это вызывает у меня проблемы в том, что я делаю. По сути, мой код создает эти объекты-оболочки путем анализа некоторой текстовой строки. Итак, у меня есть такой метод, как ValueWrapper MakeValueWrapper (текстовая строка). Если я сделаю оболочку универсальной, мне нужно изменить подпись MakeValueWrapper на универсальную, а затем это, в свою очередь, означает, что вызывающий код должен знать, какие типы ожидаются, и я просто не знаю этого заранее, прежде чем я проанализирую текст. ...
Крис Фьютрелл
... но даже когда я писал последний комментарий, мне казалось, что я, возможно, что-то пропустил (или что-то испортил), потому что то, что я пытаюсь сделать, не кажется таким сложным, как я делаю. Думаю, я вернусь и потрачу несколько минут на работу над обобщенной оболочкой и посмотрю, смогу ли я адаптировать для нее код синтаксического анализа.
Крис Фьютрелл,
Код, который я предоставил, предназначен только для деловой логики. Проблема с вашим подходом заключается в том, что вы никогда не знаете, какое значение хранится в Union во время компиляции. Это означает, что вам придется использовать операторы if или switch всякий раз, когда вы обращаетесь к объекту Union, поскольку эти объекты не имеют общих функций! Как вы собираетесь использовать объекты-оболочки в своем коде? Также вы можете создавать общие объекты во время выполнения (медленно, но возможно). Другой простой вариант - в моем отредактированном сообщении.
Jaroslav Jandek
Прямо сейчас у вас в коде практически нет значимых проверок типов во время компиляции - вы также можете попробовать динамические объекты (проверка динамического типа во время выполнения).
Ярослав Яндек
2

Вот моя попытка. Он выполняет проверку типов во время компиляции с использованием общих ограничений типа.

class Union {
    public interface AllowedType<T> { };

    internal object val;

    internal System.Type type;
}

static class UnionEx {
    public static T As<U,T>(this U x) where U : Union, Union.AllowedType<T> {
        return x.type == typeof(T) ?(T)x.val : default(T);
    }

    public static void Set<U,T>(this U x, T newval) where U : Union, Union.AllowedType<T> {
        x.val = newval;
        x.type = typeof(T);
    }

    public static bool Is<U,T>(this U x) where U : Union, Union.AllowedType<T> {
        return x.type == typeof(T);
    }
}

class MyType : Union, Union.AllowedType<int>, Union.AllowedType<string> {}

class TestIt
{
    static void Main()
    {
        MyType bla = new MyType();
        bla.Set(234);
        System.Console.WriteLine(bla.As<MyType,int>());
        System.Console.WriteLine(bla.Is<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,int>());

        bla.Set("test");
        System.Console.WriteLine(bla.As<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,int>());

        // compile time errors!
        // bla.Set('a'); 
        // bla.Is<MyType,char>()
    }
}

Это может потребовать некоторого прихорашивания. В частности, я не мог понять, как избавиться от параметров типа As / Is / Set (разве нет способа указать один параметр типа и позволить C # определять другой?)

Амнон
источник
2

Итак, я сталкивался с этой же проблемой много раз, и я просто придумал решение, которое получает желаемый синтаксис (за счет некоторого уродства в реализации типа Union).

Подведем итоги: мы хотим, чтобы на месте вызова было такое использование.

Union<int, string> u;

u = 1492;
int yearColumbusDiscoveredAmerica = u;

u = "hello world";
string traditionalGreeting = u;

var answers = new SortedList<string, Union<int, string, DateTime>>();
answers["life, the universe, and everything"] = 42;
answers["D-Day"] = new DateTime(1944, 6, 6);
answers["C#"] = "is awesome";

Однако мы хотим, чтобы следующие примеры не компилировались, чтобы получить хоть какую-то безопасность типов.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

В качестве дополнительного вознаграждения давайте также не будем занимать больше места, чем абсолютно необходимо.

С учетом всего сказанного, вот моя реализация для двух параметров универсального типа. Реализация трех, четырех и т.д. параметров типа проста.

public abstract class Union<T1, T2>
{
    public abstract int TypeSlot
    {
        get;
    }

    public virtual T1 AsT1()
    {
        throw new TypeAccessException(string.Format(
            "Cannot treat this instance as a {0} instance.", typeof(T1).Name));
    }

    public virtual T2 AsT2()
    {
        throw new TypeAccessException(string.Format(
            "Cannot treat this instance as a {0} instance.", typeof(T2).Name));
    }

    public static implicit operator Union<T1, T2>(T1 data)
    {
        return new FromT1(data);
    }

    public static implicit operator Union<T1, T2>(T2 data)
    {
        return new FromT2(data);
    }

    public static implicit operator Union<T1, T2>(Tuple<T1, T2> data)
    {
        return new FromTuple(data);
    }

    public static implicit operator T1(Union<T1, T2> source)
    {
        return source.AsT1();
    }

    public static implicit operator T2(Union<T1, T2> source)
    {
        return source.AsT2();
    }

    private class FromT1 : Union<T1, T2>
    {
        private readonly T1 data;

        public FromT1(T1 data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 1; } 
        }

        public override T1 AsT1()
        { 
            return this.data;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }

    private class FromT2 : Union<T1, T2>
    {
        private readonly T2 data;

        public FromT2(T2 data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 2; } 
        }

        public override T2 AsT2()
        { 
            return this.data;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }

    private class FromTuple : Union<T1, T2>
    {
        private readonly Tuple<T1, T2> data;

        public FromTuple(Tuple<T1, T2> data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 0; } 
        }

        public override T1 AsT1()
        { 
            return this.data.Item1;
        }

        public override T2 AsT2()
        { 
            return this.data.Item2;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }
}
Филип Тарон
источник
2

И моя попытка минимального, но расширяемого решения с использованием вложенности типа Union / Either . Кроме того, использование параметров по умолчанию в методе Match, естественно, включает сценарий «Либо X, либо по умолчанию».

using System;
using System.Reflection;
using NUnit.Framework;

namespace Playground
{
    [TestFixture]
    public class EitherTests
    {
        [Test]
        public void Test_Either_of_Property_or_FieldInfo()
        {
            var some = new Some(false);
            var field = some.GetType().GetField("X");
            var property = some.GetType().GetProperty("Y");
            Assert.NotNull(field);
            Assert.NotNull(property);

            var info = Either<PropertyInfo, FieldInfo>.Of(field);
            var infoType = info.Match(p => p.PropertyType, f => f.FieldType);

            Assert.That(infoType, Is.EqualTo(typeof(bool)));
        }

        [Test]
        public void Either_of_three_cases_using_nesting()
        {
            var some = new Some(false);
            var field = some.GetType().GetField("X");
            var parameter = some.GetType().GetConstructors()[0].GetParameters()[0];
            Assert.NotNull(field);
            Assert.NotNull(parameter);

            var info = Either<ParameterInfo, Either<PropertyInfo, FieldInfo>>.Of(parameter);
            var name = info.Match(_ => _.Name, _ => _.Name, _ => _.Name);

            Assert.That(name, Is.EqualTo("a"));
        }

        public class Some
        {
            public bool X;
            public string Y { get; set; }

            public Some(bool a)
            {
                X = a;
            }
        }
    }

    public static class Either
    {
        public static T Match<A, B, C, T>(
            this Either<A, Either<B, C>> source,
            Func<A, T> a = null, Func<B, T> b = null, Func<C, T> c = null)
        {
            return source.Match(a, bc => bc.Match(b, c));
        }
    }

    public abstract class Either<A, B>
    {
        public static Either<A, B> Of(A a)
        {
            return new CaseA(a);
        }

        public static Either<A, B> Of(B b)
        {
            return new CaseB(b);
        }

        public abstract T Match<T>(Func<A, T> a = null, Func<B, T> b = null);

        private sealed class CaseA : Either<A, B>
        {
            private readonly A _item;
            public CaseA(A item) { _item = item; }

            public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
            {
                return a == null ? default(T) : a(_item);
            }
        }

        private sealed class CaseB : Either<A, B>
        {
            private readonly B _item;
            public CaseB(B item) { _item = item; }

            public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
            {
                return b == null ? default(T) : b(_item);
            }
        }
    }
}
дадхи
источник
1

Вы можете генерировать исключения при попытке доступа к переменным, которые не были инициализированы, т.е. если он создан с параметром A, а позже есть попытка доступа к B или C, это может вызвать, скажем, UnsupportedOperationException. Однако вам понадобится геттер, чтобы он работал.

мистер попо
источник
Да, первая версия, которую я написал, вызвала исключение в методе As, но хотя это определенно подчеркивает проблему в коде, я предпочитаю, чтобы мне говорили об этом во время компиляции, чем во время выполнения.
Chris Fewtrell
0

Вы можете экспортировать функцию сопоставления псевдошаблонов, как я использую для типа Either в моей библиотеке Sasa . В настоящее время существуют накладные расходы времени выполнения, но в конечном итоге я планирую добавить анализ CIL, чтобы встроить все делегаты в истинный оператор case.

не спрашивая
источник
0

Невозможно использовать именно тот синтаксис, который вы использовали, но с немного большей многословностью и копированием / вставкой легко заставить разрешение перегрузки сделать эту работу за вас:


// this code is ok
var u = new Union("");
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

// and this one will not compile
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

К настоящему времени должно быть довольно очевидно, как это реализовать:


    public class Union
    {
        private readonly Type type;
        public readonly A a;
        public readonly B b;
        public readonly C c;

        public Union(A a)
        {
            type = typeof(A);
            this.a = a;
        }

        public Union(B b)
        {
            type = typeof(B);
            this.b = b;
        }

        public Union(C c)
        {
            type = typeof(C);
            this.c = c;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(A) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(B) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(C) == type;
        }

        public A Value(GetValueTypeSelector _)
        {
            return a;
        }

        public B Value(GetValueTypeSelector _)
        {
            return b;
        }

        public C Value(GetValueTypeSelector _)
        {
            return c;
        }
    }

    public static class Is
    {
        public static TypeTestSelector OfType()
        {
            return null;
        }
    }

    public class TypeTestSelector
    {
    }

    public static class Get
    {
        public static GetValueTypeSelector ForType()
        {
            return null;
        }
    }

    public class GetValueTypeSelector
    {
    }

Нет никаких проверок на извлечение значения неправильного типа, например:


var u = Union(10);
string s = u.Value(Get.ForType());

Таким образом, вы можете подумать о добавлении необходимых проверок и исключении в таких случаях.

Константин Ознобихин
источник
0

Я использую свой Union Type.

Рассмотрим пример, чтобы было понятнее.

Представьте, что у нас есть класс Contact:

public class Contact 
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string PostalAdrress { get; set; }
}

Все они определены как простые строки, но действительно ли это просто строки? Конечно, нет. Имя может состоять из имени и фамилии. Или электронное письмо - это просто набор символов? Я знаю, что по крайней мере он должен содержать @ и это обязательно.

Давайте улучшим модель предметной области

public class PersonalName 
{
    public PersonalName(string firstName, string lastName) { ... }
    public string Name() { return _fistName + " " _lastName; }
}

public class EmailAddress 
{
    public EmailAddress(string email) { ... } 
}

public class PostalAdrress 
{
    public PostalAdrress(string address, string city, int zip) { ... } 
}

В этих классах будут проверки во время создания, и в конечном итоге у нас будут действующие модели. Конструктору в классе PersonaName требуется одновременно FirstName и LastName. Это означает, что после создания он не может иметь недопустимое состояние.

И контактный класс соответственно

public class Contact 
{
    public PersonalName Name { get; set; }
    public EmailAdress EmailAddress { get; set; }
    public PostalAddress PostalAddress { get; set; }
}

В этом случае у нас такая же проблема, объект класса Contact может находиться в недопустимом состоянии. Я имею в виду, что у него может быть адрес электронной почты, но не имя

var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };

Давайте исправим это и создадим класс Contact с конструктором, который требует PersonalName, EmailAddress и PostalAddress:

public class Contact 
{
    public Contact(
               PersonalName personalName, 
               EmailAddress emailAddress,
               PostalAddress postalAddress
           ) 
    { 
         ... 
    }
}

Но здесь возникает другая проблема. Что делать, если у человека есть только адрес электронной почты, а не почтовый адрес?

Если мы подумаем об этом, то поймем, что есть три возможности действительного состояния объекта класса Contact:

  1. У контакта есть только адрес электронной почты
  2. У контакта есть только почтовый адрес
  3. У контакта есть как адрес электронной почты, так и почтовый адрес.

Выпишем модели предметной области. Для начала создадим класс Contact Info, состояние которого будет соответствовать вышеуказанным случаям.

public class ContactInfo 
{
    public ContactInfo(EmailAddress emailAddress) { ... }
    public ContactInfo(PostalAddress postalAddress) { ... }
    public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}

И контактный класс:

public class Contact 
{
    public Contact(
              PersonalName personalName,
              ContactInfo contactInfo
           )
    {
        ...
    }
}

Попробуем использовать:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases

Добавим метод Match в класс ContactInfo

public class ContactInfo 
{
   // constructor 
   public TResult Match<TResult>(
                      Func<EmailAddress,TResult> f1,
                      Func<PostalAddress,TResult> f2,
                      Func<Tuple<EmailAddress,PostalAddress>> f3
                  )
   {
        if (_emailAddress != null) 
        {
             return f1(_emailAddress);
        } 
        else if(_postalAddress != null)
        {
             ...
        } 
        ...
   }
}

В методе сопоставления мы можем написать этот код, потому что состояние контактного класса контролируется конструкторами, и он может иметь только одно из возможных состояний.

Создадим вспомогательный класс, чтобы каждый раз не писать столько кода.

public abstract class Union<T1,T2,T3>
    where T1 : class
    where T2 : class
    where T3 : class
{
    private readonly T1 _t1;
    private readonly T2 _t2;
    private readonly T3 _t3;
    public Union(T1 t1) { _t1 = t1; }
    public Union(T2 t2) { _t2 = t2; }
    public Union(T3 t3) { _t3 = t3; }

    public TResult Match<TResult>(
            Func<T1, TResult> f1,
            Func<T2, TResult> f2,
            Func<T3, TResult> f3
        )
    {
        if (_t1 != null)
        {
            return f1(_t1);
        }
        else if (_t2 != null)
        {
            return f2(_t2);
        }
        else if (_t3 != null)
        {
            return f3(_t3);
        }
        throw new Exception("can't match");
    }
}

Мы можем заранее иметь такой класс для нескольких типов, как это делается с делегатами Func, Action. 4-6 параметров универсального типа будут полностью для класса Union.

Перепишем ContactInfoкласс:

public sealed class ContactInfo : Union<
                                     EmailAddress,
                                     PostalAddress,
                                     Tuple<EmaiAddress,PostalAddress>
                                  >
{
    public Contact(EmailAddress emailAddress) : base(emailAddress) { }
    public Contact(PostalAddress postalAddress) : base(postalAddress) { }
    public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}

Здесь компилятор запросит переопределение хотя бы для одного конструктора. Если мы забудем переопределить остальные конструкторы, мы не сможем создать объект класса ContactInfo с другим состоянием. Это защитит нас от исключений времени выполнения во время сопоставления.

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console
    .WriteLine(
        contact
            .ContactInfo()
            .Match(
                (emailAddress) => emailAddress.Address,
                (postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
                (emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
            )
    );

Вот и все. Надеюсь, тебе понравилось.

Пример взят с сайта F # для развлечения и наживы

Когоя
источник