Дискримінований союз в 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, якщо це так, 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. Звичайно, цього не можна зробити з еталонними типами. Те, що ви робите, зовсім не схоже на С Союз.
Брайан

4
Це часто називають дискримінованим союзом .
Джефф Харді,

Дякую, Джефф - я не знав цього терміна, але це якраз саме те, чого я хочу досягти
Кріс Фетрелл

7
Можливо, це не той тип відповіді, який ви шукаєте, але чи розглядали ви F #? У ньому є типові безпечні об'єднання та відповідність шаблонів, написані прямо на мові, набагато простіше представляти спілки, ніж із C #.
Джульєтта

1
Інша назва дискримінованого союзу - це тип сум.
cdiggins

Відповіді:


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 представляють профспілки всередині.
Джульєтта

4
Мені подобається коротший код Джульєтти, але що, якщо типи <int, int, string>? Як би ви назвали другий конструктор?
Robert Jeppesen,

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));
    }
}

+1 Це має отримати більше схвалень; Мені подобається, як ви зробили його досить гнучким, щоб дозволити профспілки всіляких артерій.
Paul d'Aoust

+1 для гнучкості та стислості вашого рішення. Однак деякі деталі мене турбують. Я
розміщу

1
1. Застосування рефлексії може спричинити занадто велике покарання за ефективність у деяких сценаріях, враховуючи те, що дискриміновані профспілки, в силу своєї фундаментальної природи, можуть використовуватись дуже часто.
stakx - більше не вносить вклад

4
2. Використання dynamic& дженериків у UnionBase<A>ланцюжку успадкування видається непотрібним. Зробіть UnionBase<A>не загальним, вбийте конструктор, взявши an A, і зробіть valuean object(що в будь-якому випадку це; жодна додаткова вигода при оголошенні dynamic). Потім виведіть кожен Union<…>клас безпосередньо з UnionBase. Це має ту перевагу, що Match<T>(…)буде розкрито лише належний метод. (Як зараз, наприклад, Union<A, B>виявляє перевантаження, Match<T>(Func<A, T> fa)яке гарантовано спричиняє виняток, якщо вкладене значення не є A. Це не повинно відбуватися.)
stakx - більше не сприяє

3
Можливо, моя бібліотека OneOf стане вам у нагоді, вона робить більш-менш це, але знаходиться на Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

Я написав кілька дописів у цьому блозі, які можуть бути корисними:

Скажімо, у вас є сценарій кошика для покупок із трьома станами: "Порожній", "Активний" та "Платний", кожен із різною поведінкою.

  • Ви створюєте є 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має aPay метод, якого немає в інших державах.

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

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
        );
}    

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


Цікавий випадок використання. Для мене впровадження дискримінованих об’єднань на самих об’єктах стає досить багатослівним. Ось альтернатива функціонального стилю, яка використовує вирази перемикачів на основі вашої моделі: 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
);

``


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, не гарантує ефективності пам'яті. Якщо ви переходите Union<A, B, C>з одного типу на інший, ви необов’язково повторно використовуєте пам’ять ... швидше за все, ви виділяєте новий тип у купі та скидаєте інший вказівник у objectполе підкладки . На відміну від реального об’єднання , ваш підхід насправді може спричинити більший обвал купи, ніж ви отримали б, якби не використовували тип Union.


Як я вже згадував у своєму питанні, моїм стимулом була не краща ефективність пам’яті. Я змінив заголовок запитання, щоб краще відобразити мою мету - оригінальна назва "С (іш) союзу" на задньому
плані

Дискримінований союз має набагато більше сенсу для того, що ви намагаєтесь зробити. Щодо того, щоб перевірити час компіляції ... Я б розглянув .NET 4 та кодові контракти. За допомогою кодових контрактів може бути можливим примусове виконання Контракту під час компіляції. Вимоги, що забезпечує виконання ваших вимог щодо оператора .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 на загальний, а це по черзі означає, що викличний код повинен знати, які типи очікуються, і я просто не знаю цього заздалегідь, перш ніж проаналізувати текст ...
Chris Fewtrell

... але навіть коли я писав останній коментар, здавалося, що я щось, можливо, пропустив (або щось переплутав), тому що те, що я намагаюся зробити, не здається таким настільки складним, як я роблю це. Я думаю, що я повернусь назад і проведу кілька хвилин, працюючи над узагальненою обгорткою, і подивлюсь, чи можу я адаптувати код синтаксичного аналізу навколо неї.
Chris Fewtrell

Код, який я надав, повинен бути лише для логіки бізнесу. Проблема вашого підходу полягає в тому, що ви ніколи не знаєте, яке значення зберігається в Union під час компіляції. Це означає, що вам доведеться використовувати оператори if або перемикання кожного разу, коли ви отримуєте доступ до об'єкта Union, оскільки ці об'єкти не мають спільної функціональності! Як ви збираєтеся використовувати обгорткові об'єкти далі у своєму коді? Також ви можете створювати загальні об'єкти під час виконання (повільно, але можливо). Ще один простий варіант - у моєму відредагованому дописі.
Ярослав Яндекс

Зараз у вашому коді в принципі немає значущих перевірок типу компіляції - ви також можете спробувати динамічні об'єкти (динамічна перевірка типу під час виконання).
Ярослав Яндекс

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, або Default".

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

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


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.

Розглянемо приклад, щоб було зрозуміліше.

Уявіть, у нас є клас контактів:

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. Контакт має як електронну адресу, так і поштову адресу

Давайте випишемо доменні моделі. Для початку ми створимо клас "Контактна інформація", стан якого буде відповідати наведеним вище випадкам.

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 # для розваги та отримання прибутку

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.