Domanda

[Nota: Questa domanda ha avuto il titolo originale " C (ish) unione di stile in C # " ma come commento di Jeff mi ha informato, a quanto pare questa struttura viene chiamato un 'unione discriminata']

Scusate la prolissità di questa domanda.

Ci sono un paio di domande simili cassa di risonanza per la mia già a SO ma sembrano concentrarsi sul salvataggio nella memoria benefici del sindacato o di utilizzarlo per l'interoperabilità. Ecco un esempio di una tale domanda .

Il mio desiderio di avere un tipo di unione cosa è un po 'diversa.

Sto scrivendo un certo codice in questo momento che genera gli oggetti che sembrano un po 'come questo

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

    public object ValueA;
    public object ValueB;
}

Roba abbastanza complicato penso che sarete d'accordo. Il fatto è che ValueA può essere solo di pochi alcuni tipi (diciamo string, int e Foo (che è una classe) e ValueB può essere un altro piccolo insieme di tipi. Non mi piace trattare questi valori come oggetti (voglio il caldo comodamente sensazione di codifica con un po 'di sicurezza di tipo).

Così ho pensato di scrivere un po 'di classe wrapper banale per esprimere il fatto che ValueA logicamente è un riferimento a un determinato tipo. Ho chiamato il Union classe perché quello che sto cercando di realizzare mi ha ricordato il concetto di unione in 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);
    }
}

Utilizzando questa classe ValueWrapper ora assomiglia a questo

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

, che è qualcosa di simile a quello che volevo ottenere ma mi manca un elemento abbastanza cruciale - che è compilatore applicata controllo di tipo quando si chiama la si trova e come funzioni come il seguente codice illustra

    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 Non è valida per chiedere ValueA se si tratta di un char fin dalla sua definizione dice chiaramente che non è - questo è un errore di programmazione e vorrei al compilatore di salire su questo. [Anche se ho potuto ottenere questo corretto, allora (si spera) vorrei avere IntelliSense troppo -. Che sarebbe una manna]

Al fine di raggiungere questo vorrei dire al compilatore che il tipo T può essere uno di A, B o 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;
    } 

Qualcuno ha qualche idea se quello che voglio ottenere è possibile? O sono semplicemente stupido per la scrittura di questa classe, in primo luogo?

Grazie in anticipo.

È stato utile?

Soluzione

Io non piace molto il tipo controllo e soluzioni di tipo-fusione di cui sopra, quindi ecco il 100% type-safe unione che getterà errori di compilazione, se si tenta di utilizzare il tipo di dati sbagliato:

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

Altri suggerimenti

mi piace la direzione della soluzione accettata ma non scala bene per unioni di più di tre elementi (ad esempio un'unione di 9 articoli richiederebbe 9 definizioni di classe).

Ecco un altro approccio che è anche il 100% type-safe a tempo di compilazione, ma che è facile da coltivare a grandi sindacati.

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

ho scritto alcuni post del blog su questo tema che potrebbero essere utili:

esempio di farvi avere uno scenario carrello della spesa con tre stati:. "Empty", "Active" e "Paid", ognuno con diverso comportamento

  • Si crea avere un'interfaccia ICartState che tutti gli Stati hanno in comune (e potrebbe essere solo un'interfaccia marcatore vuoto)
  • È possibile creare tre classi che implementano l'interfaccia. (Le classi non devono essere in una relazione di ereditarietà)
  • L'interfaccia contiene una "piega" il metodo, per cui si passa una lambda a per ogni stato o un caso che è necessario gestire.

È possibile utilizzare il # runtime F da C #, ma come un accendino alternativa di peso, ho scritto un po 'di template T4 per la generazione di codice come questo.

Ecco l'interfaccia:

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

Ed ecco l'attuazione:

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

Ora diciamo che si estende il CartStateEmpty e CartStateActive con un metodo AddItem che è non attuato da CartStatePaid.

E anche Diciamo che CartStateActive ha un metodo Pay che gli altri stati non dovete.

Poi ecco qualche codice che lo mostra in uso - l'aggiunta di due prodotti e poi pagare per il carrello:

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

Si noti che questo codice è completamente typesafe -. Senza fusione o condizionali ovunque, e gli errori di compilazione, se si tenta di pagare per un carro vuoto, dire

Ho scritto una libreria per fare questo a https://github.com/mcintyre321/OneOf

  

Installa-Package oneof

Ha i tipi generici in esso per fare DUs esempio OneOf<T0, T1> tutto il modo di OneOf<T0, ..., T9>. Ognuno di questi ha una .Match, e una dichiarazione .Switch che può essere utilizzato per il compilatore comportamento digitato sicura, per esempio:.

`` `

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

`` `

io non sono sicuro di aver capito pienamente il vostro obiettivo. In C, un'unione è una struttura che utilizza gli stessi locazioni di memoria per più di un campo. Ad esempio:

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

L'unione floatOrScalar potrebbe essere usato come un galleggiante, o un int, ma entrambi consumano stesso spazio di memoria. La modifica si cambia l'altra. È possibile ottenere la stessa cosa con una struct in C #:

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

La struttura usi sopra 32bits totale, piuttosto che 64bit. Questo è possibile solo con una struct. Il tuo esempio di cui sopra è una classe, e data la natura del CLR, non offre alcuna garanzia circa l'efficienza della memoria. Se si modifica un Union<A, B, C> da un tipo ad un altro, non sono necessariamente riutilizzando memoria ... molto probabilmente, si assegnano un nuovo tipo sul mucchio e far cadere un puntatore diverso nel campo supporto object. Contrariamente a un vera unione , il tuo approccio può effettivamente causare botte più mucchio di quanto si potrebbe altrimenti ottenere se non è stato utilizzato il tipo di Unione.

char foo = 'B';

bool bar = foo is int;

Il risultato è un avvertimento, non un errore. Se siete alla ricerca per i vostri Is e As funzioni da analoghi per gli operatori C #, allora non dovrebbe essere li limitando in questo modo in ogni caso.

Se si consente più tipi, non è possibile ottenere la sicurezza di tipo (a meno che i tipi sono correlati).

non può e non ottenere alcun tipo di sicurezza di tipo, si potrebbe ottenere solo byte-valore-sicurezza utilizzando FieldOffset.

E 'avrebbe molto più senso avere un ValueWrapper<T1, T2> generico con T1 ValueA e T2 ValueB, ...

P.S .: quando si parla di sicurezza di tipo I media di compilazione tipo di sicurezza.

Se avete bisogno di un wrapper di codice (l'esecuzione di logica bussiness sulle modifiche è possibile utilizzare qualcosa sulla falsariga di:

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

Per una facile via d'uscita si potrebbe usare (ha problemi di prestazioni, ma è molto semplice):

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

Ecco il mio tentativo. Fa momento della compilazione il controllo di tipi, utilizzando i vincoli di tipo generico.

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

Si potrebbe utilizzare alcuni prettying-up. Soprattutto, non riuscivo a capire come sbarazzarsi dei parametri di tipo per As / Is / Set (non c'è un modo per specificare un parametro di tipo C e lasciate # figura l'altro?)

Così ho colpito questo stesso problema molte volte, e ho appena si avvicinò con una soluzione che ottiene la sintassi che voglio (a scapito di qualche bruttura nell'attuazione del tipo Union).

Per ricapitolare: vogliamo che questo tipo di utilizzo al luogo di chiamata .

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

Vogliamo che i seguenti esempi di fallire per la compilazione, comunque, in modo da ottenere un po 'di sicurezza tipo.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Per il credito supplementare, mettiamoci anche non occupa più spazio di quanto sia assolutamente necessario.

Con tutto ciò che ha detto, ecco la mia applicazione per due parametri di tipo generico. L'implementazione per tre, quattro, e così via parametri di tipo è lineare.

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

E il mio tentativo sulla soluzione minimalista ma estendibile utilizzando nidificazione di Union / O di tipo . Anche l'utilizzo di parametri di default in metodo Match consente naturalmente "X o di default" scenario.

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

Si potrebbe generare eccezioni una volta c'è un tentativo di variabili di accesso che non sono state inizializzate, vale a dire se è stato creato con un parametro A e poi c'è un tentativo di accesso B o C, che potrebbe gettare, per esempio, UnsupportedOperationException. Avresti bisogno di un getter per farlo funzionare però.

È possibile esportare una funzione di corrispondenza pseudo-modello, come io uso per il tipo O nel mio Sasa biblioteca . C'è attualmente runtime in testa, ma alla fine ho intenzione di aggiungere un'analisi CIL inline tutti i delegati in una vera dichiarazione caso.

Non è possibile fare con esattamente la sintassi che hai usato, ma con un po 'più di dettaglio e copia / incolla è facile fare la risoluzione di sovraccarico fare il lavoro per voi:


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

A questo punto dovrebbe essere abbastanza evidente come implementarlo:


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

Non ci sono i controlli per l'estrazione del valore del tipo sbagliato, per esempio:.


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

Così si potrebbe prendere in considerazione l'aggiunta di controlli necessari e generare eccezioni in casi del genere.

Io uso proprio di Union Tipo.

Si consideri un esempio per renderlo più chiaro.

Immaginiamo di avere classe di contatto:

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

Questi sono tutti definiti come semplici stringhe, ma in realtà stanno solo stringhe? Ovviamente no. Il nome può essere costituito da Nome e Cognome. O è una e-mail solo un insieme di simboli? So che almeno dovrebbe contenere @ ed è necessariamente.

Diamo migliorare noi modello di dominio

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 questo classi sarà convalide durante la creazione e noi alla fine dovranno modelli validi. Consturctor in classe PersonaName richiedono FirstName e LastName allo stesso tempo. Ciò significa che, dopo la creazione, non può avere stato non valido.

rispettivamente

E Classe contatto

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

In questo caso abbiamo lo stesso problema, oggetto della classe di contatto può essere in stato non valido. Voglio dire che può avere EmailAddress ma non hanno nome

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

fix Ammettiamolo e creare classe contatto con il costruttore che richiede personalname, EmailAddress e PostalAddress:

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

Ma qui abbiamo un altro problema. Che cosa succede se persona solo emailadress e non hanno PostalAddress?

Se ci pensiamo bene lì ci rendiamo conto che ci sono tre possibilità di stato valido di contatto classe di oggetti:

  1. Un contatto ha solo un indirizzo email
  2. Un contatto ha solo un indirizzo postale
  3. Un contatto ha sia un indirizzo email e un indirizzo postale

scrittura di Let fuori modelli di dominio. Per l'inizio creeremo classe del contatto quale stato sarà corrispondente con i casi di cui sopra.

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

E Classe di contatto:

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

Prova Usa Ammettiamolo:

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

add metodo Match Let in classe 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)
        {
             ...
        } 
        ...
   }
}
  

Nel metodo partita, siamo in grado di scrivere questo codice, perché lo stato della classe contatto è controllata con i costruttori e può avere solo uno dei possibili stati.

Creiamo una classe ausiliaria, in modo che ogni volta non scrivere come molti codice.

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

Possiamo avere un tale classe in anticipo per diversi tipi, come si fa con i delegati Func, Azione. 4-6 parametri di tipo generico saranno integralmente per la classe dell'Unione.

riscrittura classe ContactInfo Let:

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

Qui il compilatore chiederà override per almeno un costruttore. Se ci dimentichiamo di ignorare il resto dei costruttori non possiamo creare oggetto della classe ContactInfo con un altro Stato. Questo ci proteggerà da eccezioni di runtime durante il 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()
            )
    );

Questo è tutto. Spero vi sia piaciuto.

Esempio tratto dal sito F # per divertimento e profitto

Il linguaggio C # Design Team discusso discriminato sindacati gennaio 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions- via-chiuso-tipi

E 'possibile votare per la richiesta di funzionalità a https://github.com/dotnet/csharplang / temi / 113

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top