Frage

[Anmerkung: Diese Frage hatte den ursprünglichen Titel " C (ish) Stil Vereinigung in C # " aber als Jeffs Kommentar mich informiert, offenbar diese Struktur wird als eine ‚diskriminierte Vereinigung‘]

Entschuldigen Sie die Ausführlichkeit dieser Frage.

Es gibt ein paar ähnlich klingende Fragen zu meinem bereits in SO, aber sie scheinen zu konzentrieren sich auf den Speicher Vorteile der Vereinigung speichern oder für Interop verwenden. Hier ist ein Beispiel für eine solche Frage .

Mein Wunsch, eine Union-Typ Sache zu haben, ist etwas anders.

Ich schreibe einige Code zur Zeit, welche Objekte erzeugt, die ein bisschen wie diese

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

    public object ValueA;
    public object ValueB;
}

ziemlich kompliziert Sachen, die ich denke, Sie werden mir zustimmen. Die Sache ist die, dass ValueA nur von wenigen bestimmten Typen sein kann (sagen wir mal string, int und Foo (die eine Klasse) und ValueB eine weitere kleine Gruppe von Typen sein kann. Ich weiß nicht, wie diese Werte als Objekte behandeln (ich will die warme eng Kodierung mit etwas Art Sicherheitsgefühl).

So dachte ich über eine triviale wenig Wrapper-Klasse Schreiben zum Ausdruck bringt, dass ValueA logisch ein Verweis auf einen bestimmten Typ ist. Ich rief die Klasse Union weil das, was ich versuche, mich an die Union Konzept in C erinnert zu erreichen.

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

Diese Klasse verwendet ValueWrapper sieht nun wie folgt

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

, die wie etwas ist, was ich wollte erreichen, aber ich ein ziemlich wichtiges Element fehle - die Compiler Typprüfung erzwungen, wenn die und als Funktionen wie der folgenden Code Aufruf zeigt

    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 ist es nicht gültig ValueA zu fragen, ob es sich um eine char ist da eindeutig seine Definition sagt, es ist nicht - das ist ein Programmierfehler, und ich würde der Compiler auf diese abholen möchten. [Auch wenn ich diese richtig dann bekommen kann (hoffentlich) würde ich Intellisense bekommen -. Was ein Segen sein würde]

Um dies zu erreichen, ich mochte den Compiler sagen, dass die Art T eine von A sein kann, B oder 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;
    } 

Hat jemand eine Idee, ob das, was ich will möglich ist, erreichen? Oder bin ich einfach nur dumm für das Schreiben dieser Klasse in erster Linie?

Vielen Dank im Voraus.

War es hilfreich?

Lösung

Ich weiß nicht wirklich wie die Typprüfung und Typ Gießlösungen oben vorgesehen, hier ist also 100% typsichere Vereinigung, die Kompilierungsfehlern werfen, wenn Sie den falschen Datentyp zu verwenden versuchen:

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

Andere Tipps

Ich mag die Richtung der akzeptierten Lösung, aber es skaliert nicht gut für die Gewerkschaften von mehr als drei Elementen (zum Beispiel einer Vereinigung von 9 Einzelteilen würde 9 Klassendefinitionen erfordern).

Hier ist ein weiterer Ansatz, der auch 100% typsicher zur Compile-Zeit ist, aber das ist einfach zu groß Gewerkschaften zu wachsen.

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

Ich schrieb ein paar Blog-Posts zu diesem Thema, die nützlich sein könnten:

Angenommen, Sie haben ein Warenkorb Szenario mit drei Zuständen. „Empty“, „Aktiv“ und „Paid“, jeweils mit andere Verhalten

  • Sie erstellen eine ICartState Schnittstelle haben, dass alle Staaten gemeinsam haben (und es könnte nur eine leere Marker-Schnittstelle sein)
  • Sie erstellen drei Klassen, die diese Schnittstelle implementieren. (Die Klassen müssen nicht in einer Vererbungsbeziehung sein)
  • Die Schnittstelle enthält eine „fold“ Verfahren, bei dem Sie eine Lambda-in für jeden Zustand oder Fall passieren, dass Sie behandeln müssen.

Sie können die F # Laufzeit von C # verwenden, aber als eine geringeren Gewicht Alternative habe ich eine wenig T4-Vorlage geschrieben für Code wie folgt zu erzeugen.

Hier ist die Schnittstelle:

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

Und hier ist die Umsetzung:

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

Lassen Sie uns jetzt sagen, Sie CartStateEmpty und CartStateActive mit einem AddItem Verfahren erstrecken, die ist nicht von CartStatePaid umgesetzt.

Und auch lasst uns sagen, dass CartStateActive eine Pay Methode hat, dass die anderen Staaten nicht haben.

Dann ist hier einige Code, zeigt es in Gebrauch - das Hinzufügen von zwei Elementen und dann für den Wagen zahlen:

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

Beachten Sie, dass dieser Code vollständig typsicher ist -. Kein Casting oder irgendwo conditionals und Compiler-Fehler, wenn Sie zu zahlen für einen leeren Wagen versuchen, sagt

Ich habe eine Bibliothek geschrieben, dies zu tun unter https://github.com/mcintyre321/OneOf

Install-Package OneOf

Es hat die generischen Typen in ihm zu tun DUs z.B. OneOf<T0, T1> den ganzen Weg zu OneOf<T0, ..., T9>. Jede dieser hat eine .Match und eine .Switch Aussage, die Sie für Compiler sicher getippt Verhalten verwenden können, z.

`` `

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

`` `

Ich bin nicht sicher, ob vollständig verstehe ich Ihr Ziel. In C wird eine Vereinigung eine Struktur, die die gleichen Speicherplatz für mehr als ein Feld verwendet wird. Zum Beispiel:

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

Die floatOrScalar Union könnte als Schwimmer verwendet werden, oder ein int, aber sie beide verbrauchen die gleiche Speicherplatz. Ändern eines ändert sich die anderen. Sie können die gleiche Sache mit einer Struktur in C # erreichen:

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

Die Gesamt 32bits obigen Struktur Anwendungen, anstatt 64bits. Dies ist nur möglich mit einer Struktur. Ihr Beispiel oben ist eine Klasse, und angesichts der Art der CLR, übernimmt keine Garantie über die Speichereffizienz. Wenn Sie einen Union<A, B, C> von einem Typ zum anderen ändern, werden Sie nicht unbedingt Speicher Wiederverwendung ... wahrscheinlich werden Sie eine neue Art auf dem Heap Aufteilung und einen anderen Zeiger in der Träger object Feld fallen. Im Gegensatz zu einer wirklicher Vereinigung , kann Ihr Ansatz tatsächlich dazu führen, mehr Heap Dreschen als Sie sonst, wenn Sie nicht Ihre Union Typen verwendet haben würde bekommen.

char foo = 'B';

bool bar = foo is int;

Dies führt zu einer Warnung, kein Fehler. Wenn Sie für Ihre Is und As Funktionen suchen Analoga für die C # Operatoren zu sein, dann sollten Sie sie nicht auf diese Weise irgendwie hinderlich sein.

Wenn Sie können mehrere Arten, Sie können nicht die Typsicherheit erreichen (es sei denn, die Typen verwandt sind).

Sie können nicht und werden nicht jede Art von Typsicherheit erzielen, könnten Sie nur Byte-Wert-Sicherheit mit FieldOffset erreichen.

Es würde viel mehr Sinn macht eine allgemeine ValueWrapper<T1, T2> mit T1 ValueA und T2 ValueB zu haben, ...

P. S .: wenn es um Typsicherheit I mittlere Kompilierung-Typ-Sicherheit.

Wenn Sie einen Code-Wrapper müssen (performing bussiness Logik auf Änderungen können Sie etwas entlang der Linien von verwenden:

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

Für eine einfache Lösung Sie nutzen könnten (es Performance-Probleme hat, aber es ist sehr einfach):

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

Hier ist mein Versuch. Es tut der Kompilierung von Typen überprüft, generische Typ Einschränkungen verwenden.

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

Es könnte einige prettying-up verwenden. Vor allem kann ich nicht herausfinden, wie As / Is / Set Beseitigung der Typ-Parameter zu erhalten (nicht es eine Möglichkeit, einen Typ-Parameter angeben und C # Figur der andere lassen?)

So habe ich traf das gleiche Problem viele Male, und ich kam gerade mit einer Lösung, die die Syntax bekommt Ich möchte (auf Kosten einiger Hässlichkeit in der Umsetzung der Union Typ.)

Zur Erinnerung: Wir wollen diese Art der Nutzung an der Aufrufstelle

.
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";

Wir wollen die folgenden Beispiele zum Scheitern verurteilt, jedoch zu kompilieren, so dass wir ein gewisses Maß an Typsicherheit bekommen.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Für zusätzliche Kredite, lassen sie auch nicht mehr Platz in Anspruch nehmen, als unbedingt erforderlich ist.

Mit all das gesagt, hier ist meine Implementierung für zwei generische Typparameter. Die Implementierung für drei, vier, usw. Typ Parameter ist geradlinig.

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

Und mein Versuch auf minimal noch erweiterbare Lösung mit Verschachtelung von Union / Jede Art . Auch Verwendung von Standardparametern in Match-Methode natürlich ermöglicht „entweder X oder Default“ Szenario.

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

Sie könnten Ausnahmen auslösen, wenn es ein Versuch, den Zugriff auf Variablen, die nicht initialisiert, das heißt, wenn es mit einem A-Parameter erstellt wird und später gibt es ein Versuch, den Zugriff auf B oder C, könnte es werfen, sagen wir, UnsupportedOperationException. Sie würden einen Getter müssen es aber funktionieren.

Sie kann eine pseudo-Pattern-Matching-Funktion exportieren, wie ich für die Beiden Typen in meinem Sasa Bibliothek . Es gibt zur Zeit Laufzeit-Overhead, aber ich plane schließlich eine CIL-Analyse hinzufügen alle die Delegierten zu einer wahren Fall Aussage Inline.

Es ist nicht möglich, mit genau der Syntax zu tun, die Sie verwendet haben, aber mit etwas mehr Ausführlichkeit und Kopieren / Einfügen ist es einfach, die Überladungsauflösung machen die Arbeit für Sie zu machen:


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

Nun sollte es ziemlich offensichtlich, wie es zu implementieren:


    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
    {
    }

Es gibt keine Kontrollen für den Wert des falschen Typs zu extrahieren, z.


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

So Sie sich anschauen sollten notwendigen Kontrollen Hinzufügen und Ausnahmen in solchen Fällen werfen.

Ich benutze eigen von Union Typ.

Beispiel betrachten, um es deutlicher zu machen.

Stellen Sie sich vor wir haben Kontakt Klasse:

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

Diese sind alle als einfache Strings definiert, aber in Wirklichkeit sind sie nur Strings? Natürlich nicht. Der Name kann aus Vornamen und Nachnamen. Oder ist eine E-Mail nur eine Reihe von Symbolen? Ich weiß, dass zumindest sollte es enthalten @ und es ist notwendig.

Lassen Sie uns verbessern uns Domain-Modell

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) { ... } 
}

In diesen Klassen Validierungen wird während der Erstellung und wir werden schließlich gültige Modelle haben. Consturctor in PersonaName Klasse erfordert Vor- und den Nachnamen zugleich. Dies bedeutet, dass nach der Gründung, kann es nicht ungültigen Zustand hat.

Und Kontaktklasse jeweils

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

In diesem Fall haben wir gleiches Problem haben, Gegenstand der Kontaktklasse kann in einem ungültigen Zustand sein. Ich meine, es kann aber haben nicht Emailaddress-Name

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

Lassen Sie uns fix it und erstellen Kontaktklasse mit Konstruktor, personal, Emailaddress und Postal erfordert:

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

Aber hier haben wir ein weiteres Problem. Was passiert, wenn Person nur E-Mail adresse und hat nicht Postal?

Wenn wir darüber nachdenken da wir erkennen, dass es drei Möglichkeiten des gültigen Zustandes des Kontaktklassenobjekts:

  1. Ein Kontakt hat nur eine E-Mail-Adresse
  2. Ein Kontakt hat nur eine Postadresse
  3. Ein Kontakt hat sowohl eine E-Mail-Adresse und eine Postadresse

Lassen Sie uns schreiben, Domain-Modelle. Für den Anfang sind wir Kontakt Info-Klasse schaffen, den Zustand mit oben genannten Fällen wird entsprechende wird.

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

Und Kontaktklasse:

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

Lassen Sie uns versuchen Verwendung es:

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

Lassen Sie uns Add Match-Methode in Contact Klasse

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)
        {
             ...
        } 
        ...
   }
}

In der Match-Methode können wir diesen Code schreiben, weil der Zustand der Kontaktklasse mit Konstrukteuren gesteuert wird, und es kann nur eine der möglicher Zustände hat.

Lassen Sie sich eine Hilfsklasse erstellen, so dass jedes Mal, schreiben Sie nicht so viele Codes.

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

Wir können eine solche Klasse haben im Voraus für verschiedene Arten, wie mit den Delegierten Func, Aktion durchgeführt wird. 4-6 generische Typparameter für Union Klasse ungekürzt wiedergegeben werden.

Lassen Sie uns Rewrite ContactInfo Klasse:

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

Hier wird der Compiler überschreiben fragen für mindestens einen Konstruktor. Wenn wir vergessen, den Rest der Konstrukteure außer Kraft setzen können wir nicht Gegenstand der Contact Klasse mit einem anderen Staat schaffen. Dies schützt uns vor Laufzeitausnahmen während Matching.

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

Das ist alles. Ich hoffe, Sie genießen.

Beispiel von der Website genommen F # für Spaß und Profit

Die Sprache C # Design Team diskutiert Gewerkschaften im Januar 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions- via-closed-Typen

Sie können für die Feature-Request stimmen unter https://github.com/dotnet/csharplang / issues / 113

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top