103 votes

Union discriminée en C #

[Note: Cette question avait le titre original "C (ish) style de l'union en C#" mais comme Jeff commentaire m'a informé, apparemment, cette structure est appelée une " victime de l'est de l'union]

Excuse le niveau de détail de cette question.

Il y a un couple qui se ressemblent questions à mine de, dans la mais ils semblent se concentrer sur la mémoire des économies de l'union ou de l'utiliser pour l'interopérabilité. Voici un exemple d'une telle question.

Mon désir d'avoir une union de type de chose est un peu différente.

Je suis en train d'écrire un peu de code pour le moment qui génère des objets qui ressemblent un peu à ce

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

    public object ValueA;
    public object ValueB;
}

Assez compliqué tout ça je pense que vous serez d'accord. Le truc, c'est qu' ValueA ne peut être d'un peu de certains types (disons string, int et Foo (qui est une classe) et ValueB peut-être un autre petit ensemble de types. Je n'aime pas le traitement de ces valeurs comme des objets (je veux du chaud confortablement sentiment de codage avec un peu de type de sécurité).

J'ai donc pensé à écrire un trivial peu de classe wrapper pour exprimer le fait que ValueA logiquement est une référence à un type particulier. J'ai appelé la classe Union parce que ce que je suis en train de réaliser me rappelle de l'union concept 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);
    }
}

À l'aide de cette classe ValueWrapper ressemble maintenant à ceci

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

qui est quelque chose comme ce que je voulais obtenir mais il me manque une assez élément essentiel - c'est-à compilateur forcée type de vérification lors de l'appel de l'Est et en tant Que fonctions, comme l'illustre le code suivant

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }

IMO Il n'est pas valable pour demander ValueA si c'est un char depuis sa définition clairement dit qu'il n'est pas - c'est une erreur de programmation et je voudrais le compilateur pour ramasser sur ce. [Aussi, si je pouvais avoir ce correcte (je l'espère), je voudrais obtenir intellisense trop - ce qui serait une aubaine.]

Afin d'atteindre ce que je voudrais dire au compilateur que le type T peut être l'un des A, B ou 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;
    } 

Quelqu'un a une idée si ce que je veux réaliser est possible? Ou suis-je tout simplement stupide pour l'écriture de cette classe à la première place?

Merci à l'avance.

123voto

Juliet Points 40758

Je n'aime pas vraiment les solutions de vérification et de transposition de types fournies ci-dessus. Voici donc l'union 100% sécurisée contre les types qui génère des erreurs de compilation si vous essayez d'utiliser un type de données incorrect:

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

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

35voto

cdiggins Points 5549

J'aime la direction de la solution acceptée, mais elle ne convient pas aux syndicats de plus de trois articles (par exemple, un syndicat de 9 articles nécessiterait 9 définitions de classe).

Voici une autre approche 100% sécurisée au moment de la compilation, mais facile à transformer en syndicats importants.

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

20voto

Grundoon Points 311

Même si c'est une vieille question, récemment, j'ai écrit quelques billets de blog sur ce sujet qui pourrait être utile.

Disons que vous avez un panier scénario avec trois états: "Vide", "Active" et "Payé", chacun avec différentes comportement.

  • Vous créez ont un ICartState interface que tous les états ont en commun (et il pourrait juste être un marqueur vide de l'interface)
  • Vous créez trois classes qui implémentent cette interface. (Les classes n'ont pas à être dans une relation d'héritage)
  • L'interface contient un "pli" de la méthode, par laquelle vous passez un lambda pour chaque état ou de l'affaire que vous devez gérer.

Vous pouvez utiliser le F# runtime à partir de C#, mais comme un poids plus léger, j'ai écrit un peu de T4 modèle pour la génération de code comme celui-ci.

Voici l'interface:

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

Et voici la mise en œuvre:

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

Maintenant, disons que vous étendez l' CartStateEmpty et CartStateActive avec un AddItem méthode qui n'est pas mis en œuvre par CartStatePaid.

Et aussi, disons qu' CartStateActive a Pay méthode que les autres etats n'ont pas.

Alors voici un code qui montre qu'il s'en cours d'utilisation -- l'ajout de deux éléments et le paiement du panier:

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

Notez que ce code est complètement typesafe-pas de casting ou le conditionnel n'importe où, et les erreurs de compilation si vous essayez de payer pour un panier vide, dire.

7voto

jrista Points 20950

Je ne suis pas sûr que je comprends parfaitement votre objectif. En C, une union est une structure qui utilise les mêmes emplacements de mémoire pour plus d'un domaine. Par exemple:

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

L' floatOrScalar de l'union pourrait être utilisé comme un float ou un int, mais ils consomment la même quantité d'espace mémoire. Une seule modification de l'autre. Vous pouvez réaliser la même chose avec une struct en C#:

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

La structure ci-dessus utilise 32bits total, plutôt que de 64bits. Ce n'est possible qu'avec une struct. Votre exemple ci-dessus est une classe, et compte tenu de la nature de la CLR, ne fait aucune garantie quant à l'efficacité de mémoire. Si vous modifiez un Union<A, B, C> d'un type à un autre, vous n'êtes pas nécessairement la réutilisation de la mémoire...plus probablement, vous êtes à l'allocation d'un nouveau type sur le tas et l'abandon d'un autre pointeur dans la sauvegarde object champ. Contrairement à une union réelle, votre approche peut causer plus de tas raclée que vous obtenez si vous n'utilisez pas votre type d'Union.

5voto

Peter Ruderman Points 6151

Eh bien, je ne pense pas que ce soit stupide de chercher à améliorer la sécurité des caractères, mais je pense que vous êtes sur la mauvaise voie. Qu'en est-il de simplement créer des accesseurs et des passeurs au lieu d'utiliser des champs publics?

Par exemple:

 public object ValueA { get; private set; }

public void SetValueA( int value ) { ValueA = value; }
public void SetValueA( string value ) { ValueA = value; }
public void SetValueA( Foo value ) { ValueA = value; }
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X