Unione discriminata in C #


92

[Nota: questa domanda aveva il titolo originale " Unione in stile C (ish) in C # " ma, come mi ha informato il commento di Jeff, a quanto pare questa struttura è chiamata "unione discriminata"]

Scusa la verbosità di questa domanda.

Ci sono un paio di domande simili che suonano già in SO, ma sembrano concentrarsi sui vantaggi di risparmio di memoria dell'unione o sull'utilizzo per l'interoperabilità. Ecco un esempio di tale domanda .

Il mio desiderio di avere una cosa di tipo sindacale è in qualche modo diverso.

Al momento sto scrivendo del codice che genera oggetti che assomigliano un po 'a questo

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

    public object ValueA;
    public object ValueB;
}

Cose piuttosto complicate, penso che sarai d'accordo. Il fatto è che ValueApuò essere solo di un paio di certi tipi (diciamo string, inte Foo(che è una classe), e ValueBpuò 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).

Quindi ho pensato di scrivere una piccola classe wrapper banale per esprimere il fatto che ValueA logicamente è un riferimento a un tipo particolare. Ho chiamato la classe Unionperché quello che sto cercando di ottenere 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);
    }
}

L'utilizzo di questa classe ValueWrapper ora ha questo aspetto

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

che è qualcosa di simile a ciò che volevo ottenere ma mi manca un elemento abbastanza cruciale: il controllo del tipo applicato dal compilatore quando si chiamano le funzioni Is e As come dimostra il codice seguente

    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 è valido chiedere a ValueA se è a charpoiché la sua definizione dice chiaramente che non lo è - questo è un errore di programmazione e vorrei che il compilatore lo prendesse. [Inoltre, se potessi farlo correttamente, allora (si spera) diventerei anche intellisense, il che sarebbe un vantaggio.]

Per ottenere ciò, vorrei dire al compilatore che il tipo Tpuò essere 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 idea se quello che voglio ottenere è possibile? O sono semplicemente stupido per aver scritto questa lezione in primo luogo?

Grazie in anticipo.


3
Le unioni in C possono essere implementate in C # per i tipi di valore utilizzando StructLayout(LayoutKind.Explicit)e FieldOffset. Questo non può essere fatto con i tipi di riferimento, ovviamente. Quello che stai facendo non è affatto come un'unione C.
Brian il

4
Questo è spesso chiamato un sindacato discriminata .
Jeff Hardy il

Grazie Jeff - Non ero a conoscenza di questo termine, ma questo è più o meno esattamente ciò che voglio ottenere
Chris Fewtrell

7
Probabilmente non è il tipo di risposta che stai cercando, ma hai considerato F #? Dispone di unioni indipendenti dai tipi e di corrispondenza dei modelli cotti direttamente nella lingua, molto più facili da rappresentare rispetto a C #.
Juliet

1
Un altro nome per l'unione discriminata è un tipo di somma.
cdiggins

Risposte:


113

Non mi piacciono molto le soluzioni di controllo e casting del tipo fornite sopra, quindi ecco un'unione sicura al 100% che genererà 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);
            }
        }
    }
}

3
Sì, se vuoi unioni discriminate per tipicità, ne avrai bisogno match, e questo è un buon modo per ottenerlo.
Pavel Minaev

20
E se tutto quel codice boilerplate ti mette giù, puoi provare questa implementazione che contrassegna esplicitamente i casi: pastebin.com/EEdvVh2R . Per inciso, questo stile è molto simile al modo in cui F # e OCaml rappresentano internamente i sindacati.
Juliet

4
Mi piace il codice più breve di Juliet, ma cosa succede se i tipi sono <int, int, string>? Come chiameresti il ​​secondo costruttore?
Robert Jeppesen,

2
Non so come questo non abbia 100 voti positivi. È una cosa bellissima!
Paolo Falabella

5
@nexus considera questo tipo in F #:type Result = Success of int | Error of int
AlexFoxGill

33

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

Ecco un altro approccio che è anche sicuro al 100% in fase di compilazione, ma che è facile da estendere a grandi unioni.

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 Questo dovrebbe ottenere più approvazioni; Mi piace il modo in cui lo hai reso abbastanza flessibile da consentire unioni di ogni tipo.
Paul d'Aoust,

+1 per la flessibilità e la brevità della tua soluzione. Ci sono alcuni dettagli che mi preoccupano, però.
Pubblicherò

1
1. Il ricorso alla riflessione potrebbe incorrere in una penalizzazione eccessiva in alcuni scenari, dato che i sindacati discriminati, per la loro natura fondamentale, potrebbero essere utilizzati molto spesso.
stakx - non contribuisce più il

4
2. L'uso di dynamic& generics in UnionBase<A>e la catena di ereditarietà non sembra necessario. Rendi UnionBase<A>non generico, uccidi il costruttore che prende un Ae crea valueun object(che è comunque; non c'è alcun vantaggio aggiuntivo nel dichiararlo dynamic). Quindi derivare ogni Union<…>classe direttamente da UnionBase. Ciò ha il vantaggio che Match<T>(…)verrà esposto solo il metodo corretto . (Come è ora, ad esempio, Union<A, B>espone un sovraccarico Match<T>(Func<A, T> fa)che è garantito per lanciare un'eccezione se il valore racchiuso non è an A. Ciò non dovrebbe accadere.)
stakx - non contribuisce più il

3
Potresti trovare utile la mia libreria OneOf, fa più o meno questo, ma è su Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

Ho scritto alcuni post sul blog su questo argomento che potrebbero essere utili:

Supponiamo che tu abbia uno scenario di carrello degli acquisti con tre stati: "Vuoto", "Attivo" e "A pagamento", ciascuno con un comportamento diverso .

  • Hai creato ICartStateun'interfaccia che tutti gli stati hanno in comune (e potrebbe essere solo un'interfaccia marker vuota)
  • Crei tre classi che implementano quell'interfaccia. (Le classi non devono essere in una relazione di ereditarietà)
  • L'interfaccia contiene un metodo "fold", in base al quale si passa un lambda per ogni stato o caso che è necessario gestire.

È possibile utilizzare il runtime F # da C # ma come alternativa più leggera, ho scritto un piccolo modello T4 per generare 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'implementazione:

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

Supponiamo ora di estendere CartStateEmptye CartStateActivecon un AddItemmetodo non implementato da CartStatePaid.

E diciamo anche che CartStateActiveha un Paymetodo che gli altri stati non hanno.

Quindi ecco un codice che lo mostra in uso: aggiungere due articoli e quindi pagare 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
        );
}    

Nota che questo codice è completamente privo di tipizzazione: nessun casting o condizionali da nessuna parte e errori del compilatore se provi a pagare per un carrello vuoto, ad esempio.


Caso d'uso interessante. Per me, l'implementazione delle unioni discriminate sugli oggetti stessi diventa piuttosto prolissa. Ecco un'alternativa in stile funzionale che utilizza espressioni di commutazione, in base al modello: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Puoi vedere che le DU non sono realmente necessarie se c'è un solo percorso "felice", ma diventano molto utili quando un metodo può restituire un tipo o un altro, a seconda delle regole della logica di business.
David Cuccia

12

Ho scritto una libreria per farlo su https://github.com/mcintyre321/OneOf

Pacchetto di installazione OneOf

Ha i tipi generici per fare DU, ad esempio OneOf<T0, T1>fino a OneOf<T0, ..., T9>. Ognuno di questi ha una .Matche .Switchun'istruzione che puoi usare per un comportamento tipizzato sicuro del compilatore, ad esempio:

`` `

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

`` `


7

Non sono sicuro di aver compreso appieno il tuo obiettivo. In C, un'unione è una struttura che utilizza le stesse posizioni di memoria per più di un campo. Per esempio:

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

L' floatOrScalarunione potrebbe essere utilizzata come float o int, ma entrambi occupano lo stesso spazio di memoria. Cambiarne uno cambia l'altro. Puoi ottenere la stessa cosa con una struttura in C #:

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

La struttura di cui sopra utilizza 32 bit totali, anziché 64 bit. Questo è possibile solo con una struttura. Il tuo esempio sopra è una classe e, data la natura del CLR, non garantisce l'efficienza della memoria. Se cambi un Union<A, B, C>da un tipo a un altro, non stai necessariamente riutilizzando la memoria ... molto probabilmente, stai allocando un nuovo tipo sull'heap e rilasciando un diverso puntatore nel objectcampo sottostante . Contrariamente a una vera unione , il tuo approccio potrebbe effettivamente causare più heap thrashing di quanto potresti altrimenti ottenere se non usassi il tuo tipo Union.


Come ho detto nella mia domanda, la mia motivazione non era una migliore efficienza della memoria. Ho cambiato il titolo della domanda per riflettere meglio quale sia il mio obiettivo - il titolo originale di "C (ish) union" è fuorviante con il senno di poi
Chris Fewtrell,

Un'unione discriminata ha molto più senso per quello che stai cercando di fare. Per quanto riguarda il controllo in fase di compilazione ... vorrei esaminare .NET 4 e Contratti di codice. Con i contratti di codice, potrebbe essere possibile applicare un contratto in fase di compilazione.Requires che impone i tuoi requisiti sull'operatore .Is <T>.
jrista

Credo di dover ancora mettere in discussione l'uso di un'Unione, nella pratica generale. Anche in C / C ++, le unioni sono una cosa rischiosa e devono essere utilizzate con estrema cura. Sono curioso di sapere perché hai bisogno di portare un tale costrutto in C # ... che valore percepisci ricavarne?
jrista

2
char foo = 'B';

bool bar = foo is int;

Ciò si traduce in un avviso, non in un errore. Se stai cercando che le tue funzioni Ise Assiano analoghe per gli operatori C #, non dovresti comunque limitarle in quel modo.


2

Se si consentono più tipi, non è possibile ottenere l'indipendenza dai tipi (a meno che i tipi non siano correlati).

Non è possibile e non si otterrà alcun tipo di protezione dai tipi, è possibile ottenere la protezione dal valore di byte solo utilizzando FieldOffset.

Avrebbe molto più senso avere un generico ValueWrapper<T1, T2>con T1 ValueAe T2 ValueB, ...

PS: quando parlo di protezione dai tipi intendo la protezione dai tipi in fase di compilazione.

Se hai bisogno di un wrapper di codice (eseguendo la logica aziendale sulle modifiche puoi usare 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 che potresti 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

Il tuo suggerimento di rendere ValueWrapper generico sembra la risposta ovvia ma mi causa problemi in quello che sto facendo. In sostanza, il mio codice crea questi oggetti wrapper analizzando alcune righe di testo. Quindi ho un metodo come ValueWrapper MakeValueWrapper (stringa di testo). Se creo il wrapper generico, devo modificare la firma di MakeValueWrapper in modo che sia generico e quindi questo a sua volta significa che il codice chiamante deve sapere quali tipi sono previsti e non lo so in anticipo prima di analizzare il testo ...
Chris Fewtrell

... ma anche mentre stavo scrivendo l'ultimo commento, mi è sembrato di essermi perso qualcosa (o aver incasinato qualcosa) perché quello che sto cercando di fare non sembra essere così difficile come lo sto facendo. Penso che tornerò indietro e dedicherò alcuni minuti a lavorare su un wrapper generato e vedrò se posso adattare il codice di analisi attorno ad esso.
Chris Fewtrell,

Il codice che ho fornito dovrebbe essere solo per la logica aziendale. Il problema con il tuo approccio è che non sai mai quale valore è memorizzato nell'Unione in fase di compilazione. Significa che dovrai usare le istruzioni if ​​o switch ogni volta che accedi all'oggetto Union, poiché quegli oggetti non condividono una funzionalità comune! Come utilizzerai ulteriormente gli oggetti wrapper nel tuo codice? Inoltre puoi costruire oggetti generici in fase di esecuzione (lento, ma possibile). Un'altra opzione facile con è nel mio post modificato.
Jaroslav Jandek

In questo momento non hai praticamente alcun controllo del tipo significativo in fase di compilazione nel tuo codice - potresti anche provare oggetti dinamici (controllo dinamico del tipo in fase di esecuzione).
Jaroslav Jandek

2

Ecco il mio tentativo. Compila il controllo del tempo dei tipi, utilizzando 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>()
    }
}

Potrebbe essere necessario un po 'di abbellimento. Soprattutto, non sono riuscito a capire come sbarazzarmi dei parametri di tipo su As / Is / Set (non c'è un modo per specificare un parametro di tipo e lasciare che C # calcoli l'altro?)


2

Quindi ho riscontrato lo stesso problema molte volte e ho appena trovato una soluzione che ottiene la sintassi che desidero (a scapito di un po 'di bruttezza nell'implementazione del tipo Union.)

Ricapitolando: vogliamo questo tipo di utilizzo nel sito della 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 non vengano compilati, tuttavia, in modo da ottenere un minimo di sicurezza dei tipi.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Per un credito extra, evitiamo inoltre di occupare più spazio del necessario.

Detto questo, ecco la mia implementazione per due parametri di tipo generico. L'implementazione per i parametri di tipo tre, quattro e così via è semplice.

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

E il mio tentativo su una soluzione minima ma estensibile utilizzando l' annidamento di Union / Either type . Anche l'utilizzo di parametri predefiniti nel metodo Match abilita naturalmente lo scenario "O X o Default".

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1

Potresti lanciare eccezioni una volta che c'è un tentativo di accedere a variabili che non sono state inizializzate, cioè se è stato creato con un parametro A e successivamente c'è un tentativo di accedere a B o C, potrebbe lanciare, diciamo, UnsupportedOperationException. Avresti bisogno di un getter per farlo funzionare però.


Sì - la prima versione che ho scritto sollevava eccezioni nel metodo As - ma sebbene questo evidenzi certamente il problema nel codice, preferisco di gran lunga che mi venga detto di questo in fase di compilazione che in fase di runtime.
Chris Fewtrell,

0

Puoi esportare una funzione di corrispondenza di pseudo-pattern, come io uso per il tipo O nella mia libreria Sasa . Al momento c'è un sovraccarico di runtime, ma alla fine ho intenzione di aggiungere un'analisi CIL per incorporare tutti i delegati in un'istruzione case vera.


0

Non è possibile fare esattamente con la sintassi che hai usato, ma con un po 'più di verbosità e copia / incolla è facile fare in modo che la risoluzione del sovraccarico faccia il lavoro per te:


// 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 ovvio 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 controlli per estrarre il valore del tipo sbagliato, ad esempio:


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

Quindi potresti considerare di aggiungere i controlli necessari e lanciare eccezioni in questi casi.


0

Uso proprio di tipo Union.

Considera un esempio per renderlo più chiaro.

Immagina di avere la classe Contact:

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

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

Miglioriamoci il 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 queste classi ci saranno convalide durante la creazione e alla fine avremo modelli validi. Consturctor nella classe PersonaName richiede FirstName e LastName allo stesso tempo. Ciò significa che dopo la creazione, non può avere uno stato non valido.

E contattare la classe rispettivamente

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, l'oggetto della classe Contact potrebbe essere in uno stato non valido. Voglio dire che potrebbe avere EmailAddress ma non Nome

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

Risolviamolo e creiamo la classe Contact 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. Cosa succede se Persona ha solo EmailAdress e non PostalAddress?

Se ci pensiamo ci rendiamo conto che ci sono tre possibilità di stato valido dell'oggetto della classe Contact:

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

Scriviamo i modelli di dominio. Per l'inizio creeremo la classe Informazioni di contatto il cui stato corrisponderà ai casi precedenti.

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

Proviamo a usarlo:

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

Aggiungiamo il metodo Match nella 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 match, possiamo scrivere questo codice, perché lo stato della classe di contatto è controllato con i costruttori e può avere solo uno degli stati possibili.

Creiamo una classe ausiliaria, in modo che ogni volta non scriviamo più 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 una classe di questo tipo in anticipo per diversi tipi, come avviene con i delegati Func, Action. 4-6 parametri di tipo generico saranno completi per la classe Union.

Riscriviamo la ContactInfoclasse:

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à l'override di almeno un costruttore. Se dimentichiamo di sovrascrivere il resto dei costruttori non possiamo creare l'oggetto della classe ContactInfo con un altro stato. Questo ci proteggerà dalle 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()
            )
    );

È tutto. Spero che ti sia piaciuto.

Esempio tratto dal sito F # per divertimento e profitto


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.