Saltar al contenido

Unión discriminada en C #

Luego de mucho trabajar hemos encontrado el resultado de este rompecabezas que algunos los lectores de este espacio tienen. Si tienes algo más que aportar no dejes de compartir tu conocimiento.

Solución:

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[] unions = new Union3[]
                
                    new Union3.Case1(5),
                    new Union3.Case2('x'),
                    new Union3.Case3("Juliet")
                ;

            foreach (Union3 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
    
        public abstract T Match(Func f, Func g, Func h);
        // private ctor ensures no external classes can inherit
        private Union3()   

        public sealed class Case1 : Union3
        
            public readonly A Item;
            public Case1(A item) : base()  this.Item = item; 
            public override T Match(Func f, Func g, Func h)
            
                return f(Item);
            
        

        public sealed class Case2 : Union3
        
            public readonly B Item;
            public Case2(B item)  this.Item = item; 
            public override T Match(Func f, Func g, Func h)
            
                return g(Item);
            
        

        public sealed class Case3 : Union3
        
            public readonly C Item;
            public Case3(C item)  this.Item = item; 
            public override T Match(Func f, Func g, Func h)
            
                return h(Item);
            
        
    

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

    dynamic value;

    public UnionBase(A a)  value = a;  
    protected UnionBase(object x)  value = x; 

    protected T InternalMatch(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(Func fa)  return InternalMatch(fa); 


public class Union : UnionBase

    public Union(A a) : base(a)  
    public Union(B b) : base(b)  
    protected Union(object x) : base(x)  
    public T Match(Func fa, Func fb)  return InternalMatch(fa, fb); 


public class Union : Union

    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(Func fa, Func fb, Func fc)  return InternalMatch(fa, fb, fc); 


public class Union : Union

    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(Func fa, Func fb, Func fc, Func fd)  return InternalMatch(fa, fb, fc, fd); 


public class Union : Union

    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(Func fa, Func fb, Func fc, Func fd, Func fe)  return InternalMatch(fa, fb, fc, fd, fe); 


public class DiscriminatedUnionTest : IExample

    public Union MakeUnion(int n)
    
        return new Union(n);
    

    public Union MakeUnion(bool b)
    
        return new Union(b);
    

    public Union MakeUnion(string s)
    
        return new Union(s);
    

    public Union MakeUnion(params int[] xs)
    
        return new Union(xs);
    

    public void Print(Union 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));
    

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

  • Tipos de unión en C #
  • Implementación de Tic-Tac-Toe usando clases estatales

Supongamos que tiene un escenario de carrito de la compra con tres estados: “Vacío”, “Activo” y “Pagado”, cada uno con 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,
        Func cartStateActive,
        Func cartStatePaid
        );

Y aquí está la implementación:

class CartStateEmpty : ICartState

  ICartState ICartState.Transition(
        Func cartStateEmpty,
        Func cartStateActive,
        Func cartStatePaid
        )
  
        // I'm the empty state, so invoke cartStateEmpty 
      return cartStateEmpty(this);
  


class CartStateActive : ICartState

  ICartState ICartState.Transition(
        Func cartStateEmpty,
        Func cartStateActive,
        Func cartStatePaid
        )
  
        // I'm the active state, so invoke cartStateActive
      return cartStateActive(this);
  


class CartStatePaid : ICartState

  ICartState ICartState.Transition(
        Func cartStateEmpty,
        Func cartStateActive,
        Func cartStatePaid
        )
  
        // I'm the paid state, so invoke cartStatePaid
      return cartStatePaid(this);
  

Ahora digamos que extiendes el CartStateEmpty y CartStateActive con un 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.

Si te sientes estimulado, tienes la habilidad dejar una crónica acerca de qué le añadirías a este enunciado.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *