Diskriminierte Gewerkschaft in C #


91

[Anmerkung: Diese Frage hatte den Originaltitel " C (ish) style union in C # ", aber wie Jeffs Kommentar mir mitteilte, wird diese Struktur anscheinend als "diskriminierte Union" bezeichnet.]

Entschuldigen Sie die Ausführlichkeit dieser Frage.

Es gibt einige ähnlich klingende Fragen zu meinen bereits in SO, aber sie scheinen sich auf die speichersparenden Vorteile der Gewerkschaft zu konzentrieren oder sie für Interop zu verwenden. Hier ist ein Beispiel für eine solche Frage .

Mein Wunsch nach einer gewerkschaftlichen Sache ist etwas anders.

Ich schreibe gerade Code, der Objekte generiert, die ein bisschen so aussehen

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

    public object ValueA;
    public object ValueB;
}

Ziemlich kompliziertes Zeug, ich denke du wirst mir zustimmen. Die Sache ist, dass ValueAes nur wenige bestimmte Typen geben kann (sagen wir string, intund Foo(was eine Klasse ist) und ValueBeine andere kleine Gruppe von Typen sein kann. Ich mag es nicht, diese Werte als Objekte zu behandeln (ich möchte das warme, gemütliche Gefühl von Codierung mit ein wenig Typensicherheit).

Also habe ich darüber nachgedacht, eine triviale kleine Wrapper-Klasse zu schreiben, um die Tatsache auszudrücken, dass ValueA logisch eine Referenz auf einen bestimmten Typ ist. Ich rief die Klasse an, Unionweil mich das, was ich erreichen wollte, an das Gewerkschaftskonzept in C erinnerte.

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

Die Verwendung dieser Klasse ValueWrapper sieht jetzt so aus

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

Das ist ungefähr das, was ich erreichen wollte, aber mir fehlt ein ziemlich wichtiges Element - die vom Compiler erzwungene Typprüfung beim Aufrufen der Is- und As-Funktionen, wie der folgende Code 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 Es ist nicht gültig, ValueA zu fragen, ob es sich um eine handelt, charda die Definition eindeutig besagt, dass dies nicht der Fall ist. Dies ist ein Programmierfehler, und ich möchte, dass der Compiler dies aufgreift . [Auch wenn ich das richtig machen könnte, würde ich (hoffentlich) auch Intellisense bekommen - was ein Segen wäre.]

Um dies zu erreichen, möchte ich dem Compiler mitteilen, dass der Typ TA, B oder C sein kann

    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 erreichen möchte, möglich ist? Oder bin ich einfach nur dumm, diese Klasse überhaupt zu schreiben?

Danke im Voraus.


3
Gewerkschaften in C können in C # für Werttypen mit StructLayout(LayoutKind.Explicit)und implementiert werden FieldOffset. Dies ist natürlich nicht mit Referenztypen möglich. Was Sie tun, ist überhaupt nicht wie eine C-Union.
Brian

4
Dies wird oft als diskriminierte Gewerkschaft bezeichnet .
Jeff Hardy

Danke Jeff - mir war dieser Begriff nicht bekannt, aber genau das möchte ich erreichen
Chris Fewtrell

6
Wahrscheinlich nicht die Art von Antwort, nach der Sie suchen, aber haben Sie F # in Betracht gezogen? Es verfügt über typsichere Gewerkschaften und Mustervergleiche, die direkt in die Sprache integriert sind und die Gewerkschaften viel einfacher darstellen als mit C #.
Julia

1
Ein anderer Name für die diskriminierte Vereinigung ist ein Summentyp.
Cdiggins

Antworten:


112

Ich mag die oben bereitgestellten Lösungen zur Typprüfung und Typumwandlung nicht wirklich. Hier ist also eine 100% typsichere Vereinigung, die Kompilierungsfehler auslöst, wenn Sie versuchen, den falschen Datentyp zu verwenden:

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
Ja, wenn Sie typsichere diskriminierte Gewerkschaften wollen, brauchen Sie das match, und das ist so gut wie jeder andere .
Pavel

19
Und wenn Sie mit all dem Code auf dem Boilerplate nicht weiterkommen, können Sie diese Implementierung ausprobieren, bei der stattdessen Fälle explizit markiert werden: pastebin.com/EEdvVh2R . Übrigens ist dieser Stil der Art und Weise sehr ähnlich, wie F # und OCaml Gewerkschaften intern repräsentieren.
Julia

4
Ich mag Juliets kürzeren Code, aber was ist, wenn die Typen <int, int, string> sind? Wie würden Sie den zweiten Konstruktor aufrufen?
Robert Jeppesen

2
Ich weiß nicht, wie das nicht 100 Upvotes hat. Es ist eine Sache von Schönheit!
Paolo Falabella

5
@ Annex betrachten diesen Typ in F #:type Result = Success of int | Error of int
AlexFoxGill

33

Ich mag die Richtung der akzeptierten Lösung, aber sie lässt sich nicht gut für Gewerkschaften mit mehr als drei Elementen skalieren (z. B. würde eine Vereinigung von 9 Elementen 9 Klassendefinitionen erfordern).

Hier ist ein weiterer Ansatz, der zur Kompilierungszeit ebenfalls zu 100% typsicher ist, der sich jedoch leicht auf große Gewerkschaften ausweiten lässt.

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 Dies sollte mehr Genehmigungen erhalten; Ich mag die Art und Weise, wie Sie es flexibel genug gemacht haben, um Gewerkschaften aller Art zuzulassen.
Paul d'Aoust

+1 für die Flexibilität und Kürze Ihrer Lösung. Es gibt jedoch einige Details, die mich stören. Ich werde jeden als separaten Kommentar
posten

1
1. Die Verwendung von Reflexion kann in einigen Szenarien zu einem zu hohen Leistungsverlust führen, da diskriminierte Gewerkschaften aufgrund ihrer fundamentalen Natur sehr häufig eingesetzt werden.
stakx - nicht mehr am

4
2. Die Verwendung von dynamic& generics in UnionBase<A>und der Vererbungskette erscheint unnötig. Machen Sie UnionBase<A>nicht generisch, töten Sie den Konstruktor, der ein nimmt A, und machen Sie valueein object(was es sowieso ist; es gibt keinen zusätzlichen Vorteil, wenn Sie es deklarieren dynamic). Leiten Sie dann jede Union<…>Klasse direkt von ab UnionBase. Dies hat den Vorteil, dass nur die richtige Match<T>(…)Methode verfügbar gemacht wird. (So ​​wie es jetzt ist, z. B. Union<A, B>stellt es eine Überlastung dar, Match<T>(Func<A, T> fa)die garantiert eine Ausnahme A
auslöst,

2
Sie könnten meine Bibliothek OneOf nützlich finden, sie macht mehr oder weniger dies, ist aber auf Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

Ich habe einige Blog-Beiträge zu diesem Thema geschrieben, die nützlich sein könnten:

Angenommen, Sie haben ein Warenkorbszenario mit drei Status: "Leer", "Aktiv" und "Bezahlt" mit jeweils unterschiedlichem Verhalten.

  • Sie erstellen haben eine ICartState Schnittstelle, die alle Zustände gemeinsam haben (und es könnte sich nur um eine leere Markierungsschnittstelle handeln).
  • Sie erstellen drei Klassen, die diese Schnittstelle implementieren. (Die Klassen müssen nicht in einer Vererbungsbeziehung stehen.)
  • Die Schnittstelle enthält eine "Fold" -Methode, bei der Sie für jeden Zustand oder Fall, den Sie behandeln müssen, ein Lambda übergeben.

Sie könnten die F # -Runtime von C # verwenden, aber als leichtere Alternative habe ich eine kleine T4-Vorlage zum Generieren von Code wie diesem geschrieben.

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 Implementierung:

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

Angenommen, Sie erweitern das CartStateEmptyund CartStateActivemit einer AddItemMethode, die nicht von implementiert istCartStatePaid .

Und sagen wir auch, das CartStateActivehat einePay Methode gibt, die die anderen Staaten nicht haben.

Dann ist hier ein Code, der zeigt, wie er verwendet wird - zwei Artikel hinzufügen und dann für den Warenkorb bezahlen:

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 - keine Casting- oder Bedingungsbedingungen und Compilerfehler, wenn Sie beispielsweise versuchen, für einen leeren Einkaufswagen zu bezahlen.


11

Ich habe dafür eine Bibliothek unter https://github.com/mcintyre321/OneOf geschrieben

Install-Package OneOf

Es enthält die generischen Typen für DUs, z. B. OneOf<T0, T1>bis zu OneOf<T0, ..., T9>. Jeder von diesen hat ein .Matchund ein.Switch Anweisung, die Sie für ein compilersicheres typisiertes 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
);

`` `


6

Ich bin nicht sicher, ob ich Ihr Ziel vollständig verstehe. In C ist eine Vereinigung eine Struktur, die dieselben Speicherorte für mehr als ein Feld verwendet. Beispielsweise:

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

Die floatOrScalarUnion kann als Float oder Int verwendet werden, aber beide belegen denselben Speicherplatz. Das Ändern eines ändert das andere. Sie können dasselbe mit einer Struktur in C # erreichen:

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

Die obige Struktur verwendet insgesamt 32 Bit anstelle von 64 Bit. Dies ist nur mit einer Struktur möglich. Ihr Beispiel oben ist eine Klasse und gibt angesichts der Art der CLR keine Garantie für die Speichereffizienz. Wenn Sie einen Union<A, B, C>von einem Typ in einen anderen ändern, wird der Speicher nicht unbedingt wiederverwendet. Höchstwahrscheinlich weisen Sie dem Heap einen neuen Typ zu und lassen einen anderen Zeiger im Hintergrundfeld fallen object. Im Gegensatz zu einer echten Union kann Ihr Ansatz tatsächlich mehr Heap-Thrashing verursachen, als Sie sonst erhalten würden, wenn Sie Ihren Union-Typ nicht verwenden würden.


Wie ich in meiner Frage erwähnte, war meine Motivation nicht eine bessere Speichereffizienz. Ich habe den
Fragentitel

Eine diskriminierte Gewerkschaft macht für das, was Sie versuchen, viel mehr Sinn. Was die Überprüfung der Kompilierungszeit angeht ... Ich würde mich mit .NET 4 und Code-Verträgen befassen. Mit Code-Verträgen ist es möglicherweise möglich, einen Vertrag zur Kompilierungszeit durchzusetzen. Anforderungen, die Ihre Anforderungen an den Operator .Is <T> erzwingen.
Jrista

Ich denke, ich muss die Verwendung einer Union in der allgemeinen Praxis noch in Frage stellen. Auch in C / C ++ sind Gewerkschaften ein riskantes Unterfangen und müssen mit äußerster Sorgfalt eingesetzt werden. Ich bin gespannt, warum Sie ein solches Konstrukt in C # einbringen müssen ... welchen Wert sehen Sie darin, daraus herauszukommen?
Jrista

2
char foo = 'B';

bool bar = foo is int;

Dies führt zu einer Warnung, nicht zu einem Fehler. Wenn Sie suchen, dass Ihre Isund AsFunktionen für die C # -Operatoren analog sind, sollten Sie sie sowieso nicht auf diese Weise einschränken.


2

Wenn Sie mehrere Typen zulassen, können Sie keine Typensicherheit erreichen (es sei denn, die Typen sind verwandt).

Sie können und werden keine Typensicherheit erreichen, Sie können nur mit FieldOffset Byte-Wert-Sicherheit erreichen.

Es wäre viel sinnvoller, ein Generikum ValueWrapper<T1, T2>mit T1 ValueAund T2 ValueB, ...

PS: Wenn ich über Typensicherheit spreche, meine ich Typensicherheit zur Kompilierungszeit.

Wenn Sie einen Code-Wrapper benötigen (Durchführen einer Geschäftslogik für Änderungen), können Sie Folgendes 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 einen einfachen Ausweg könnten Sie verwenden (es hat Leistungsprobleme, 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

Ihr Vorschlag, ValueWrapper generisch zu machen, scheint die offensichtliche Antwort zu sein, verursacht mir jedoch Probleme bei meiner Arbeit. Im Wesentlichen erstellt mein Code diese Wrapper-Objekte durch Parsen einer Textzeile. Ich habe also eine Methode wie ValueWrapper MakeValueWrapper (String-Text). Wenn ich den Wrapper generisch mache, muss ich die Signatur von MakeValueWrapper so ändern, dass sie generisch ist. Dies bedeutet wiederum, dass der aufrufende Code wissen muss, welche Typen erwartet werden, und ich weiß dies nur nicht im Voraus, bevor ich den Text analysiere ...
Chris Fewtrell

... aber selbst als ich den letzten Kommentar schrieb, fühlte es sich an, als hätte ich vielleicht etwas verpasst (oder etwas durcheinander gebracht), weil das, was ich versuche, nicht so schwierig ist, wie ich es mache. Ich denke, ich werde zurückgehen und ein paar Minuten an einem generierten Wrapper arbeiten und sehen, ob ich den Parsing-Code um ihn herum anpassen kann.
Chris Fewtrell

Der Code, den ich bereitgestellt habe, soll nur für die Geschäftslogik sein. Das Problem bei Ihrem Ansatz ist, dass Sie nie wissen, welcher Wert zur Kompilierungszeit in der Union gespeichert ist. Dies bedeutet, dass Sie beim Zugriff auf das Union-Objekt if- oder switch-Anweisungen verwenden müssen, da diese Objekte keine gemeinsame Funktionalität haben! Wie werden Sie die Wrapper-Objekte in Ihrem Code weiter verwenden? Sie können auch generische Objekte zur Laufzeit erstellen (langsam, aber möglich). Eine weitere einfache Option ist in meinem bearbeiteten Beitrag.
Jaroslav Jandek

Grundsätzlich gibt es derzeit keine aussagekräftigen Typprüfungen zur Kompilierungszeit in Ihrem Code. Sie können auch dynamische Objekte ausprobieren (dynamische Typprüfung zur Laufzeit).
Jaroslav Jandek

2

Hier ist mein Versuch. Es überprüft die Kompilierungszeit von Typen unter Verwendung generischer Typeinschränkungen.

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 etwas hübsches gebrauchen. Insbesondere konnte ich nicht herausfinden, wie die Typparameter in As / Is / Set entfernt werden können (gibt es keine Möglichkeit, einen Typparameter anzugeben und C # den anderen berechnen zu lassen?)


2

Ich bin also schon oft auf dasselbe Problem gestoßen und habe gerade eine Lösung gefunden, die die gewünschte Syntax erhält (auf Kosten einer gewissen Hässlichkeit bei der Implementierung des Union-Typs).

Um es noch einmal zusammenzufassen: Wir möchten diese Art der Nutzung an der Anrufstelle.

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 möchten jedoch, dass die folgenden Beispiele nicht kompiliert werden können, damit wir ein Mindestmaß an Typensicherheit erhalten.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Nehmen wir für zusätzliche Gutschriften auch nicht mehr Platz ein als unbedingt erforderlich.

Nach alledem ist hier meine Implementierung für zwei generische Typparameter. Die Implementierung für Typparameter für drei, vier usw. ist unkompliziert.

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

Und mein Versuch einer minimalen, aber erweiterbaren Lösung mit Verschachtelung vom Typ Union / Beide . Auch die Verwendung von Standardparametern in der Match-Methode aktiviert natürlich das Szenario "Entweder X oder Standard".

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

Sie können Ausnahmen auslösen, wenn versucht wird, auf Variablen zuzugreifen, die nicht initialisiert wurden. Wenn sie also mit einem A-Parameter erstellt wurden und später versucht wird, auf B oder C zuzugreifen, kann beispielsweise UnsupportedOperationException ausgelöst werden. Sie würden einen Getter brauchen, damit es funktioniert.


Ja - die erste Version, die ich geschrieben habe, hat eine Ausnahme in der As-Methode ausgelöst - aber während dies sicherlich das Problem im Code hervorhebt, möchte ich dies lieber zur Kompilierungszeit als zur Laufzeit erfahren.
Chris Fewtrell

0

Sie können eine Pseudomuster-Übereinstimmungsfunktion exportieren, wie ich sie für den Typ "Beide" in meiner Sasa-Bibliothek verwende . Derzeit gibt es Laufzeit-Overhead, aber ich plane schließlich, eine CIL-Analyse hinzuzufügen, um alle Delegierten in eine echte case-Anweisung zu integrieren.


0

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


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

Inzwischen sollte es ziemlich offensichtlich sein, wie es implementiert werden soll:


    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 Überprüfungen zum Extrahieren des Werts des falschen Typs, z.


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

In solchen Fällen können Sie die erforderlichen Überprüfungen hinzufügen und Ausnahmen auslösen.


0

Ich benutze eigene Union Type.

Betrachten Sie ein Beispiel, um es klarer zu machen.

Stellen Sie sich vor, wir haben eine Kontaktklasse:

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

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

Lassen Sie uns das Domain-Modell verbessern

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 werden Validierungen während der Erstellung und wir werden schließlich gültige Modelle haben. Für Consturctor in der PersonaName-Klasse sind Vorname und Nachname gleichzeitig erforderlich. Dies bedeutet, dass es nach der Erstellung keinen ungültigen Status haben kann.

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 das gleiche Problem. Das Objekt der Kontaktklasse befindet sich möglicherweise in einem ungültigen Zustand. Ich meine, es hat vielleicht EmailAddress, aber keinen Namen

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

Lassen Sie es uns beheben und eine Kontaktklasse mit einem Konstruktor erstellen, für den PersonalName, EmailAddress und PostalAddress erforderlich sind:

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

Aber hier haben wir ein anderes Problem. Was ist, wenn die Person nur eine E-Mail-Adresse und keine Postanschrift hat?

Wenn wir dort darüber nachdenken, erkennen wir, dass es drei Möglichkeiten für einen gültigen Status des Kontaktklassenobjekts gibt:

  1. Ein Kontakt hat nur eine E-Mail-Adresse
  2. Ein Kontakt hat nur eine Postanschrift
  3. Ein Kontakt hat sowohl eine E-Mail-Adresse als auch eine Postanschrift

Schreiben wir Domain-Modelle auf. Zu Beginn erstellen wir eine Kontaktinfo-Klasse, deren Status den oben genannten Fällen entspricht.

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

Versuchen wir es mal:

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

Fügen wir die Match-Methode in die ContactInfo-Klasse ein

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 Übereinstimmungsmethode können wir diesen Code schreiben, da der Status der Kontaktklasse mit Konstruktoren gesteuert wird und möglicherweise nur einer der möglichen Status vorliegt.

Erstellen wir eine Hilfsklasse, damit nicht jedes Mal so viel Code geschrieben wird.

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 für verschiedene Typen im Voraus haben, wie dies bei den Delegierten Func, Action der Fall ist. 4-6 generische Typparameter sind für die Union-Klasse vollständig.

Lassen Sie uns die ContactInfoKlasse umschreiben :

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 fordert der Compiler die Überschreibung für mindestens einen Konstruktor an. Wenn wir vergessen, den Rest der Konstruktoren zu überschreiben, können wir kein Objekt der ContactInfo-Klasse mit einem anderen Status erstellen. Dies schützt uns vor Laufzeitausnahmen während des Matchings.

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, dass Sie Spaß hatten.

Beispiel aus der Site F # für Spaß und Gewinn


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.