Unión discriminada en C #

[Nota: esta pregunta tenía el título original "Unión de estilo C (ish) en C #"pero como me informó el comentario de Jeff, aparentemente esta estructura se llama 'unión discriminada']

Disculpe la verbosidad de esta pregunta.

Hay un par de preguntas que suenan similares a las mías ya en SO, pero parecen concentrarse en los beneficios de ahorro de memoria de la unión o en su uso para la interoperabilidad. Aquí hay un ejemplo de tal pregunta..

Mi deseo de tener algo parecido a un sindicato es algo diferente.

Estoy escribiendo un código en este momento que genera objetos que se parecen un poco a esto

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

    public object ValueA;
    public object ValueB;
}

Cosas bastante complicadas, creo que estarás de acuerdo. La cosa es que ValueA solo puede ser de algunos tipos determinados (digamos string, int y Foo (que es una clase) y ValueB puede ser otro pequeño conjunto de tipos. No me gusta tratar estos valores como objetos (quiero la cálida sensación de codificar con un poco de seguridad en los tipos).

Así que pensé en escribir una pequeña clase contenedora trivial para expresar el hecho de que ValueA es lógicamente una referencia a un tipo en particular. Llamé a la clase Union porque lo que estoy tratando de lograr me recordó el concepto de unión en 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);
    }
}

El uso de esta clase ValueWrapper ahora se ve así

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

que es algo así como lo que quería lograr, pero me falta un elemento bastante crucial: la verificación de tipo impuesta por el compilador al llamar a las funciones Is y As, como lo demuestra el siguiente código

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

OMI No es válido preguntar ValueA si es un char ya que su definición dice claramente que no lo es, esto es un error de programación y me gustaría que el compilador lo captara. [Además, si pudiera hacer esto correctamente, entonces (con suerte) obtendría intellisense también, lo cual sería una bendición].

Para lograr esto, me gustaría decirle al compilador que el tipo T puede ser uno de 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;
    } 

¿Alguien tiene alguna idea de si lo que quiero lograr es posible? ¿O simplemente soy estúpido por escribir esta clase en primer lugar?

Gracias de antemano.

preguntado el 30 de junio de 10 a las 14:06

Las uniones en C se pueden implementar en C # para tipos de valor usando StructLayout(LayoutKind.Explicit) y FieldOffset. Esto no se puede hacer con tipos de referencia, por supuesto. Lo que está haciendo no es como una Unión C en absoluto. -

Esto a menudo se llama unión discriminada. -

Gracias Jeff. No conocía este término, pero esto es precisamente lo que quiero lograr.

Probablemente no sea el tipo de respuesta que estás buscando, pero ¿has considerado F #? Tiene uniones de tipo seguro y coincidencia de patrones en el idioma, mucho más fácil de representar uniones que con C #. -

Otro nombre para la unión discriminada es un tipo de suma. -

15 Respuestas

Realmente no me gustan las soluciones de verificación de tipos y conversión de tipos proporcionadas anteriormente, así que aquí hay una unión 100% segura de tipos que arrojará errores de compilación si intenta usar el tipo de datos incorrecto:

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

Respondido 15 Abr '15, 11:04

Sí, si desea uniones discriminadas seguras, necesitará match, y esa es una forma tan buena de conseguirlo como cualquier otra. - Pavel Minaev

Y si todo ese código repetitivo lo deprime, puede probar esta implementación que etiqueta explícitamente los casos en su lugar: pastebin.com/EEdvVh2R . Por cierto, este estilo es muy similar a la forma en que F # y OCaml representan las uniones internamente. - Julieta

Me gusta el código más corto de Juliet, pero ¿y si los tipos son ? ¿Cómo llamarías al segundo constructor? - Roberto Jeppesen

@RobertJeppesen ¿Y cuál sería tu unión representar exactamente? ;) - nexo

@nexus considera este tipo en F #: type Result = Success of int | Error of int - AlexFoxGill

Me gusta la dirección de la solución aceptada, pero no se adapta bien a las uniones de más de tres elementos (por ejemplo, una unión de 9 elementos requeriría 9 definiciones de clase).

Aquí hay otro enfoque que también es 100% seguro para los tipos en tiempo de compilación, pero que es fácil de ampliar a uniones grandes.

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

Respondido el 05 de Septiembre de 11 a las 01:09

+1 Esto debería obtener más aprobaciones; Me gusta la forma en que lo ha hecho lo suficientemente flexible como para permitir uniones de todo tipo de aridades. - Paul d'Aoust

+1 por la flexibilidad y brevedad de su solución. Sin embargo, hay algunos detalles que me molestan. Publicaré cada uno como un comentario separado: - stakx - ya no contribuye

1. El uso de la reflexión puede incurrir en una penalización de desempeño demasiado grande en algunos escenarios, dado que las uniones discriminadas, debido a su naturaleza fundamental, pueden ser utilizadas con mucha frecuencia. - stakx - ya no contribuye

2. El uso de dynamic y genéricos en UnionBase<A> y la cadena de la herencia parece innecesaria. Fabricar UnionBase<A> no genérico, mata al constructor tomando un A, y hacer value an object (que es de todos modos; no hay ningún beneficio adicional en declararlo dynamic). Luego deriva cada Union<…> clase directamente de UnionBase. Esto tiene la ventaja de que solo el Match<T>(…) se expondrá el método. (Como está ahora, p. Ej. Union<A, B> expone una sobrecarga Match<T>(Func<A, T> fa) que está garantizado para lanzar una excepción si el valor adjunto no es un A. Eso no debería suceder.) - stakx - ya no contribuye

Puede encontrar útil mi biblioteca OneOf, hace más o menos esto, pero está en Nuget :) github.com/mcintyre321/OneOf - mcintyre321

Escribí algunas publicaciones de blog sobre este tema que podrían ser útiles:

Supongamos que tiene un escenario de carrito de la compra con tres estados: "Vacío", "Activo" y "Pagado", cada uno con una experiencia diferente comportamiento.

  • Tu creas tener un ICartState interfaz que todos los estados tienen en común (y podría ser simplemente una interfaz de marcador vacía)
  • Creas tres clases que implementan esa interfaz. (Las clases no tienen que estar en una relación de herencia)
  • La interfaz contiene un método de "plegado", mediante el cual pasa una lambda para cada estado o caso que necesita manejar.

Puede usar el tiempo de ejecución de F # de C #, pero como una alternativa más liviana, he escrito una pequeña plantilla T4 para generar código como este.

Aquí está la interfaz:

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

Y aquí está la implementación:

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

Ahora digamos que extiendes el CartStateEmpty y CartStateActive con una AddItem método que es no Implementado por CartStatePaid.

Y también digamos que CartStateActive tiene un Pay método que los otros estados no tienen.

Luego, aquí hay un código que lo muestra en uso: agregar dos artículos y luego pagar el carrito:

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

Tenga en cuenta que este código es completamente seguro para tipos: no hay conversiones ni condicionales en ninguna parte, y errores de compilación si intenta pagar por un carrito vacío, por ejemplo.

Respondido el 10 de junio de 19 a las 16:06

Caso de uso interesante. Para mí, implementar las uniones discriminadas en los objetos en sí se vuelve bastante detallado. Aquí hay una alternativa de estilo funcional que usa expresiones de cambio, según su modelo: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866. Puede ver que las DU no son realmente necesarias si solo hay una ruta "feliz", pero se vuelven muy útiles cuando un método puede devolver un tipo u otro, según las reglas de la lógica empresarial. - david cucia

He escrito una biblioteca para hacer esto en https://github.com/mcintyre321/OneOf

Paquete de instalación OneOf

Tiene los tipos genéricos para hacer DU, por ejemplo OneOf<T0, T1> todo el camino hasta OneOf<T0, ..., T9>. Cada uno de ellos tiene un .Match, Y un .Switch declaración que puede utilizar para el comportamiento de tipo seguro del compilador, por ejemplo:

`` `

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

`` `

Respondido 07 ago 17, 10:08

No estoy seguro de entender completamente su objetivo. En C, una unión es una estructura que usa las mismas ubicaciones de memoria para más de un campo. Por ejemplo:

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

El floatOrScalar union podría usarse como flotante o como int, pero ambos consumen el mismo espacio de memoria. Cambiar uno cambia el otro. Puede lograr lo mismo con una estructura en C #:

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

La estructura anterior utiliza un total de 32 bits, en lugar de 64 bits. Esto solo es posible con una estructura. Su ejemplo anterior es una clase y, dada la naturaleza del CLR, no garantiza la eficiencia de la memoria. Si cambia un Union<A, B, C> de un tipo a otro, no necesariamente está reutilizando la memoria ... lo más probable es que esté asignando un nuevo tipo en el montón y colocando un puntero diferente en el respaldo object campo. Contrariamente a un unión real, su enfoque en realidad puede causar más golpes de pila de los que obtendría si no usara su tipo de Unión.

Respondido el 30 de junio de 10 a las 18:06

Como mencioné en mi pregunta, mi motivación no fue una mejor eficiencia de la memoria. He cambiado el título de la pregunta para reflejar mejor cuál es mi objetivo. En retrospectiva, el título original de "Unión C (ish)" es engañoso. Chris Fewtrell

Un sindicato discriminado tiene mucho más sentido para lo que está tratando de hacer. En cuanto a hacer que se verifique en tiempo de compilación ... Buscaría en .NET 4 y Code Contracts. Con los contratos de código, es posible hacer cumplir un contrato en tiempo de compilación. operador. - jrista

Supongo que todavía tengo que cuestionar el uso de una Unión, en la práctica general. Incluso en C / C ++, las uniones son riesgosas y deben usarse con sumo cuidado. Tengo curiosidad por saber por qué necesita traer una construcción de este tipo a C # ... ¿qué valor percibe obtener de ella? - jrista

char foo = 'B';

bool bar = foo is int;

Esto da como resultado una advertencia, no un error. Si estas buscando tu Is y As funciones para ser análogas a los operadores de C #, entonces no debería restringirlas de esa manera de todos modos.

Respondido el 30 de junio de 10 a las 18:06

Si permite varios tipos, no puede lograr la seguridad de tipos (a menos que los tipos estén relacionados).

No puede y no logrará ningún tipo de seguridad de tipo, solo podría lograr seguridad de valor de byte usando FieldOffset.

Tendría mucho más sentido tener un genérico ValueWrapper<T1, T2> con T1 ValueA y T2 ValueB...

PD: cuando hablo de seguridad de tipos me refiero a seguridad de tipos en tiempo de compilación.

Si necesita un contenedor de código (realizando la lógica de negocios en las modificaciones, puede usar algo como:

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

Para una salida fácil, puede usar (tiene problemas de rendimiento, pero es muy simple):

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

Respondido 01 Jul 10, 00:07

Su sugerencia de hacer que ValueWrapper sea genérico parece la respuesta obvia, pero me causa problemas en lo que estoy haciendo. Esencialmente, mi código está creando estos objetos de envoltura analizando alguna línea de texto. Entonces tengo un método como ValueWrapper MakeValueWrapper (texto de cadena). Si hago que el contenedor sea genérico, entonces necesito cambiar la firma de MakeValueWrapper para que sea genérica y, a su vez, esto significa que el código de llamada necesita saber qué tipos se esperan y simplemente no lo sé de antemano antes de analizar el texto. ... - Chris Fewtrell

... pero incluso mientras escribía el último comentario, sentí que tal vez me perdí algo (o arruiné algo) porque lo que estoy tratando de hacer no parece tan difícil como lo estoy haciendo. Creo que volveré y pasaré unos minutos trabajando en un contenedor genérico y veré si puedo adaptar el código de análisis a su alrededor. - Chris Fewtrell

Se supone que el código que he proporcionado es solo para la lógica empresarial. El problema con su enfoque es que nunca se sabe qué valor se almacena en Union en tiempo de compilación. Significa que tendrá que usar instrucciones if o switch cada vez que acceda al objeto Union, ya que esos objetos no comparten una funcionalidad común. ¿Cómo vas a usar más los objetos de envoltura en tu código? También puede construir objetos genéricos en tiempo de ejecución (lento, pero posible). Otra opción fácil con está en mi publicación editada. - Jaroslav Jandek

Básicamente, no tiene comprobaciones de tipo en tiempo de compilación significativas en su código en este momento; también puede probar objetos dinámicos (comprobación de tipo dinámica en tiempo de ejecución). - Jaroslav Jandek

Aquí está mi intento. Compila la verificación de tiempo de tipos, utilizando restricciones de tipo genérico.

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

Le vendría bien un poco de belleza. Especialmente, no pude averiguar cómo deshacerme de los parámetros de tipo en As / Is / Set (¿no hay una manera de especificar un parámetro de tipo y dejar que C # calcule el otro?)

Respondido 01 Jul 10, 10:07

Así que me encontré con este mismo problema muchas veces, y se me ocurrió una solución que obtiene la sintaxis que quiero (a expensas de algo de fealdad en la implementación del tipo Union).

En resumen: queremos este tipo de uso en el sitio de la llamada.

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

Sin embargo, queremos que los siguientes ejemplos no se compilen, de modo que obtengamos un mínimo de seguridad de tipos.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Para obtener crédito adicional, tampoco ocupemos más espacio del absolutamente necesario.

Dicho todo esto, aquí está mi implementación para dos parámetros de tipo genérico. La implementación de tres, cuatro, etc. parámetros de tipo es sencilla.

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

Respondido 30 Abr '11, 11:04

Y mi intento de solución mínima pero extensible usando anidamiento de Unión / cualquier tipo. Además, el uso de parámetros predeterminados en el método Match habilita naturalmente el escenario "X o predeterminado".

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

Respondido 19 ago 14, 16:08

Podría lanzar excepciones una vez que haya un intento de acceder a variables que no se han inicializado, es decir, si se crea con un parámetro A y luego hay un intento de acceder a B o C, podría lanzar, digamos, UnsupportedOperationException. Sin embargo, necesitaría un captador para que funcione.

Respondido el 30 de junio de 10 a las 18:06

Sí, la primera versión que escribí generó una excepción en el método As, pero aunque esto ciertamente resalta el problema en el código, prefiero que me informen sobre esto en tiempo de compilación que en tiempo de ejecución. - Chris Fewtrell

El equipo de diseño del lenguaje C # discutió sobre sindicatos discriminados en enero de 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types

Puede votar por la solicitud de función en https://github.com/dotnet/csharplang/issues/113

Respondido 30 Jul 18, 16:07

Puede exportar una función de coincidencia de pseudopatrones, como la que uso para el tipo Either en mi Biblioteca Sasa. Actualmente hay una sobrecarga de tiempo de ejecución, pero eventualmente planeo agregar un análisis CIL para integrar a todos los delegados en una declaración de caso real.

Respondido el 30 de junio de 10 a las 19:06

No es posible hacerlo exactamente con la sintaxis que ha utilizado, pero con un poco más de detalle y copiar / pegar es fácil hacer que la resolución de sobrecarga haga el trabajo por usted:


// 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 estas alturas debería ser bastante obvio cómo 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
    {
    }

No hay comprobaciones para extraer el valor del tipo incorrecto, por ejemplo:


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

Por lo tanto, podría considerar agregar las comprobaciones necesarias y lanzar excepciones en tales casos.

Respondido 01 Jul 10, 08:07

Yo uso propio de Union Type.

Considere un ejemplo para aclararlo.

Imagina que tenemos clase de contacto:

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

Todos estos se definen como cadenas simples, pero ¿en realidad son solo cadenas? Por supuesto que no. El nombre puede constar de nombre y apellido. ¿O es un correo electrónico solo un conjunto de símbolos? Sé que al menos debería contener @ y es necesario.

Mejoremos nuestro modelo de 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) { ... } 
}

En estas clases habrá validaciones durante la creación y eventualmente tendremos modelos válidos. El constructor en la clase PersonaName requiere FirstName y LastName al mismo tiempo. Esto significa que después de la creación, no puede tener un estado inválido.

Y clase de contacto respectivamente

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

En este caso, tenemos el mismo problema, el objeto de la clase Contact puede estar en un estado no válido. Quiero decir, puede tener EmailAddress pero no tiene Name

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

Arreglemoslo y creemos la clase Contact con el constructor que requiere PersonalName, EmailAddress y PostalAddress:

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

Pero aquí tenemos otro problema. ¿Qué pasa si Person solo tiene EmailAdress y no PostalAddress?

Si lo pensamos allí, nos damos cuenta de que hay tres posibilidades de estado válido del objeto de la clase Contact:

  1. Un contacto solo tiene una dirección de correo electrónico
  2. Un contacto solo tiene una dirección postal
  3. Un contacto tiene una dirección de correo electrónico y una dirección postal

Escribamos modelos de dominio. Para empezar, crearemos la clase de información de contacto cuyo estado corresponderá con los casos anteriores.

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

Y clase de contacto:

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

Intentemos 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

Agreguemos el método Match en la clase 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)
        {
             ...
        } 
        ...
   }
}

En el método de coincidencia, podemos escribir este código, porque el estado de la clase de contacto se controla con constructores y puede tener solo uno de los estados posibles.

Creemos una clase auxiliar, para que cada vez no escriba tanto código.

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

Podemos tener dicha clase de antemano para varios tipos, como se hace con los delegados Func, Action. 4-6 parámetros de tipo genérico estarán completos para la clase Union.

Vamos a reescribir ContactInfo clase:

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

Aquí el compilador pedirá anular al menos un constructor. Si olvidamos anular el resto de los constructores, no podemos crear el objeto de la clase ContactInfo con otro estado. Esto nos protegerá de las excepciones de tiempo de ejecución durante la coincidencia.

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

Eso es todo. Espero que lo hayas disfrutado.

Ejemplo tomado del sitio F # por diversión y ganancias

Respondido 23 Jul 17, 15:07

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.