Unión discriminada en C #


93

[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 tipo 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. El caso es que ValueAsolo puede ser de algunos tipos determinados (digamos string, inty Foo(que es una clase) y ValueBpuede ser otro pequeño conjunto de tipos. No me gusta tratar estos valores como objetos (quiero la sensación cálida y acogedora de codificación con un poco de seguridad de tipo).

Así que pensé en escribir una pequeña clase contenedora trivial para expresar el hecho de que ValueA lógicamente es una referencia a un tipo en particular. Llamé a la clase Unionporque 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 tipos 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>();
        }
    }

En mi opinión, no es válido preguntar a ValueA si es un, charya 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 Tpuede 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 por adelantado.


3
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.
Brian

5
Esto a menudo se denomina sindicato discriminado .
Jeff Hardy

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

7
Probablemente no sea el tipo de respuesta que está buscando, pero ¿ha 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 #.
Julieta

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

Respuestas:


114

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

3
Sí, si quieres uniones discriminadas seguras en tipos, las necesitarás match, y esa es una forma tan buena de conseguirlo como cualquier otra.
Pavel Minaev

21
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

4
Me gusta el código más corto de Juliet, pero ¿y si los tipos son <int, int, string>? ¿Cómo llamarías al segundo constructor?
Robert Jeppesen

2
No sé cómo esto no tiene 100 votos a favor. ¡Es una cosa hermosa!
Paolo Falabella

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

33

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

+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 contribuyo

1
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 el

4
2. El uso de dynamic& genéricos en UnionBase<A>y la cadena de herencia parece innecesario. Haga UnionBase<A>no genérico, elimine el constructor tomando an Ay haga valuean object(que es de todos modos; no hay ningún beneficio adicional al declararlo dynamic). Luego, obtenga cada Union<…>clase directamente de UnionBase. Esto tiene la ventaja de que solo se Match<T>(…)expondrá el método adecuado . (Como está ahora, por ejemplo, Union<A, B>expone una sobrecarga Match<T>(Func<A, T> fa)que está garantizada para generar una excepción si el valor adjunto no es un A. Eso no debería suceder).
stakx - ya no contribuye el

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

20

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

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

  • Crea una ICartStateinterfaz 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 un 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 amplía el CartStateEmptyy CartStateActivecon un AddItemmétodo que no está implementado por CartStatePaid.

Y también digamos que CartStateActivetiene un Paymé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.


Caso de uso interesante. Para mí, implementar las uniones discriminadas en los objetos mismos 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 Cuccia

13

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>hasta el final OneOf<T0, ..., T9>. Cada uno de ellos tiene una declaración .Match, y una .Switchque puede usar 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
);

''


7

No estoy seguro de comprender 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;

La floatOrScalarunión 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 objectcampo de respaldo . Contrariamente a una unión real , su enfoque en realidad puede causar más palizas de las que obtendría si no usara su tipo de Unión.


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. El título original de "Unión C (ish)" es engañoso en retrospectiva
Chris Fewtrell

Una unión discriminada 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. Requisitos que imponen sus requisitos en el operador .Is <T>.
jrista

Creo 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

2
char foo = 'B';

bool bar = foo is int;

Esto da como resultado una advertencia, no un error. Si usted está buscando para sus Isy Asfunciones que debe análogos para los operadores de C #, entonces no debe ser restringirlos de esa manera de todos modos.


2

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 ValueAy 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

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 crea 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 declaraciones if o switch siempre 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 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

2

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?)


2

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

2

Y mi intento de una solución mínima pero extensible usando anidamiento de Union / Either type . 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);
            }
        }
    }
}

1

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.


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


0

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.


0

No es posible hacerlo exactamente con la sintaxis que ha usado, 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 verificaciones necesarias y lanzar excepciones en tales casos.


0

Yo uso el 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 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. Consturctor 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 de contacto 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 una 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.

Reescribamos la ContactInfoclase:

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 nos olvidamos de 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 beneficio

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.