Union discriminée en C #

92

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

Excusez la verbosité de cette question.

Il y a quelques questions similaires aux miennes déjà dans SO, mais elles semblent se concentrer sur les avantages d'économie de mémoire du syndicat ou de l'utiliser pour l'interopérabilité. Voici un exemple d'une telle question .

Mon désir d'avoir un truc de type syndical est quelque peu différent.

J'écris actuellement du code qui génère des objets qui ressemblent un peu à ça

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

    public object ValueA;
    public object ValueB;
}

Des trucs assez compliqués, je pense que vous serez d'accord. Le fait est que ValueAcela ne peut être que de certains types (disons string, intet Foo(qui est une classe) et ValueBpeut être un autre petit ensemble de types. Je n'aime pas traiter ces valeurs comme des objets (je veux la sensation chaleureuse de codage avec un peu de sécurité de type).

J'ai donc pensé à écrire une petite classe wrapper triviale pour exprimer le fait que ValueA est logiquement une référence à un type particulier. J'ai appelé la classe Unionparce que ce que j'essaie d'accomplir m'a rappelé le concept d'union 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);
    }
}

Utiliser 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 réaliser mais il me manque un élément assez crucial - c'est la vérification de type appliquée par le compilateur lors de l'appel des fonctions Is et As, comme le montre 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 valide de demander à ValueA si c'est un charcar sa définition dit clairement que ce n'est pas le cas - c'est une erreur de programmation et j'aimerais que le compilateur s'en aperçoive. [Aussi, si je pouvais obtenir cela correctement, alors (j'espère) j'obtiendrais aussi de l'intellisense - ce qui serait une aubaine.]

Pour ce faire, je voudrais dire au compilateur que le type Tpeut être l'un de 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-t-il une idée si ce que je veux réaliser est possible? Ou suis-je tout simplement stupide pour avoir écrit ce cours en premier lieu?

Merci d'avance.

Chris Fewtrell
la source
3
Les unions en C peuvent être implémentées en C # pour les types valeur en utilisant StructLayout(LayoutKind.Explicit)et FieldOffset. Cela ne peut pas être fait avec les types de référence, bien sûr. Ce que vous faites n'est pas du tout un syndicat C.
Brian
4
C'est ce qu'on appelle souvent un syndicat discriminatoire .
Jeff Hardy
Merci Jeff - Je n'étais pas au courant de ce terme mais c'est à peu près précisément ce que je veux réaliser
Chris Fewtrell
7
Ce n'est probablement pas le genre de réponse que vous recherchez, mais avez-vous envisagé F #? Il a des unions de type sécurisé et des correspondances de modèles directement dans le langage, beaucoup plus faciles à représenter des unions qu'avec C #.
Juliet
1
Un autre nom pour l'union discriminée est un type de somme.
cdiggins

Réponses:

113

Je n'aime pas vraiment les solutions de vérification de type et de conversion de type fournies ci-dessus, alors voici l'union de type 100% sûre qui provoquera des erreurs de compilation si vous essayez d'utiliser le mauvais type de données:

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);
            }
        }
    }
}
Juliette
la source
3
Oui, si vous voulez des syndicats discriminés et sécurisés, vous en aurez besoin match, et c'est un aussi bon moyen de l'obtenir que n'importe quel autre.
Pavel Minaev
20
Et si tout ce code standard vous dérange, vous pouvez essayer cette implémentation qui balise explicitement les cas à la place: pastebin.com/EEdvVh2R . Incidemment, ce style est très similaire à la façon dont F # et OCaml représentent les syndicats en interne.
Juliet
4
J'aime le code plus court de Juliet, mais que faire si les types sont <int, int, string>? Comment appelleriez-vous le deuxième constructeur?
Robert Jeppesen le
2
Je ne sais pas comment cela n'a pas 100 votes positifs. C'est une chose de beauté!
Paolo Falabella
5
@nexus considère ce type en F #:type Result = Success of int | Error of int
AlexFoxGill
33

J'aime la direction de la solution acceptée, mais elle ne s'adapte pas bien aux unions de plus de trois items (par exemple, une union de 9 items exigerait 9 définitions de classe).

Voici une autre approche qui est également sécurisée à 100% au moment de la compilation, mais qui est facile à transformer en grandes unions.

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));
    }
}
cdiggins
la source
+1 Cela devrait obtenir plus d'approbations; J'aime la façon dont vous l'avez rendu suffisamment flexible pour permettre des syndicats de toutes sortes de domaines.
Paul d'Aoust
+1 pour la flexibilité et la brièveté de votre solution. Certains détails me dérangent cependant. Je
publierai
1
1. Le recours à la réflexion peut entraîner une pénalité de performance trop importante dans certains scénarios, étant donné que les syndicats discriminés, en raison de leur nature fondamentale, peuvent être utilisés très souvent.
stakx - ne contribue plus le
4
2. L'utilisation de dynamic& génériques dans UnionBase<A>et la chaîne d'héritage semble inutile. Rendre UnionBase<A>non générique, tuer le constructeur prenant un A, et créer valueun object(ce qui est de toute façon; il n'y a aucun avantage supplémentaire à le déclarer dynamic). Puis dérivez chaque Union<…>classe directement à partir de UnionBase. Cela présente l'avantage que seule la Match<T>(…)méthode appropriée sera exposée. (Tel qu'il est maintenant, par exemple, Union<A, B>expose une surcharge Match<T>(Func<A, T> fa)qui est garantie de lever une exception si la valeur incluse n'est pas un A. Cela ne devrait pas arriver.)
stakx - ne contribue plus le
3
Vous trouverez peut-être ma bibliothèque OneOf utile, elle fait plus ou moins cela, mais elle est sur Nuget :) github.com/mcintyre321/OneOf
mcintyre321
20

J'ai écrit quelques articles de blog sur ce sujet qui pourraient être utiles:

Supposons que vous ayez un scénario de panier avec trois états: "Vide", "Actif" et "Payé", chacun avec un comportement différent .

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

Vous pouvez utiliser le runtime F # à partir de C #, mais comme alternative plus légère, j'ai écrit un petit modèle T4 pour générer du 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);
  }
}

Disons maintenant que vous étendez le CartStateEmptyet CartStateActiveavec une AddItemméthode qui n'est pas implémentée parCartStatePaid .

Et disons aussi que cela CartStateActivea unPay méthode que les autres États n'ont pas.

Ensuite, voici un code qui le montre en cours d'utilisation - en ajoutant deux articles puis en payant le 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 sûr de type - aucun casting ou conditionnel nulle part, et des erreurs de compilation si vous essayez de payer pour un panier vide, par exemple.

Grundoon
la source
Cas d'utilisation intéressant. Pour moi, la mise en œuvre des unions discriminées sur les objets eux-mêmes devient assez verbeuse. Voici une alternative de style fonctionnel qui utilise des expressions de commutation, basée sur votre modèle: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Vous pouvez voir que les DU ne sont pas vraiment nécessaires s'il n'y a qu'un seul chemin "heureux", mais ils deviennent très utiles lorsqu'une méthode peut renvoyer un type ou un autre, en fonction des règles de logique métier.
David Cuccia il y a
12

J'ai écrit une bibliothèque pour faire cela à https://github.com/mcintyre321/OneOf

Package d'installation OneOf

Il contient les types génériques pour faire des DU, par exemple OneOf<T0, T1>jusqu'à OneOf<T0, ..., T9>. Chacun de ceux-ci a un .Match, et une .Switchinstruction que vous pouvez utiliser pour un comportement typé sûr du compilateur, par exemple:

''

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

''

mcintyre321
la source
7

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

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

L' floatOrScalarunion peut être utilisée comme un float ou un int, mais ils consomment tous les deux le même espace mémoire. Changer l'un change l'autre. Vous pouvez réaliser la même chose avec une structure en C #:

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

La structure ci-dessus utilise 32 bits au total, plutôt que 64 bits. Ceci n'est possible qu'avec une structure. Votre exemple ci-dessus est une classe, et étant donné la nature du CLR, ne donne aucune garantie sur l'efficacité de la mémoire. Si vous changez un Union<A, B, C>d'un type à un autre, vous ne réutilisez pas nécessairement la mémoire ... très probablement, vous allouez un nouveau type sur le tas et déposez un pointeur différent dans le objectchamp de sauvegarde . Contrairement à une véritable union , votre approche peut en fait provoquer plus de débordements de tas que vous n'en auriez autrement si vous n'utilisiez pas votre type Union.

jrista
la source
Comme je l'ai mentionné dans ma question, ma motivation n'était pas une meilleure efficacité de la mémoire. J'ai changé le titre de la question pour mieux refléter mon objectif - le titre original de "C (ish) union" est avec le recul trompeur
Chris Fewtrell
Un syndicat discriminé a beaucoup plus de sens pour ce que vous essayez de faire. En ce qui concerne la vérification au moment de la compilation ... Je me pencherais sur .NET 4 et les contrats de code. Avec les contrats de code, il peut être possible d'appliquer un contrat au moment de la compilation.Requires qui applique vos exigences à l'opérateur .Is <T>.
jrista
Je suppose que je dois encore remettre en question l'utilisation d'un syndicat, dans la pratique générale. Même en C / C ++, les unions sont une chose risquée et doivent être utilisées avec une extrême prudence. Je suis curieux de savoir pourquoi vous devez intégrer une telle construction en C # ... quelle valeur pensez-vous en retirer?
jrista
2
char foo = 'B';

bool bar = foo is int;

Cela entraîne un avertissement, pas une erreur. Si vous cherchez que vos fonctions Iset Assoient des analogues pour les opérateurs C #, vous ne devriez pas les restreindre de cette façon de toute façon.

Adam Robinson
la source
2

Si vous autorisez plusieurs types, vous ne pouvez pas atteindre la sécurité de type (sauf si les types sont liés).

Vous ne pouvez pas et n'obtiendrez aucun type de sécurité de type, vous ne pouvez obtenir une sécurité de valeur d'octet qu'en utilisant FieldOffset.

Il serait beaucoup plus logique d'avoir un générique ValueWrapper<T1, T2>avec T1 ValueAetT2 ValueB , ...

PS: quand je parle de sécurité de type, je veux dire de sécurité de type au moment de la compilation.

Si vous avez besoin d'un wrapper de code (exécutant une logique commerciale sur les modifications, vous pouvez utiliser quelque chose du type:

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

Pour une solution simple, vous pouvez utiliser (il a des problèmes de performances, mais c'est très 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
Jaroslav Jandek
la source
Votre suggestion de rendre ValueWrapper générique semble être la réponse évidente, mais cela me pose des problèmes dans ce que je fais. Essentiellement, mon code crée ces objets wrapper en analysant une ligne de texte. J'ai donc une méthode comme ValueWrapper MakeValueWrapper (texte de chaîne). Si je rend le wrapper générique, je dois changer la signature de MakeValueWrapper pour qu'elle soit générique et cela signifie à son tour que le code appelant doit savoir quels types sont attendus et je ne le sais tout simplement pas à l'avance avant d'analyser le texte ...
Chris Fewtrell
... mais alors même que j'écrivais le dernier commentaire, j'avais l'impression d'avoir peut-être manqué quelque chose (ou foiré quelque chose) parce que ce que j'essaie de faire ne me semble pas aussi difficile que je le fais. Je pense que je vais revenir en arrière et passer quelques minutes à travailler sur un wrapper généré et voir si je peux adapter le code d'analyse autour de lui.
Chris Fewtrell
Le code que j'ai fourni est censé être juste pour la logique commerciale. Le problème avec votre approche est que vous ne savez jamais quelle valeur est stockée dans l'Union au moment de la compilation. Cela signifie que vous devrez utiliser des instructions if ou switch chaque fois que vous accédez à l'objet Union, car ces objets ne partagent pas une fonctionnalité commune! Comment allez-vous utiliser les objets wrapper plus loin dans votre code? Vous pouvez également construire des objets génériques lors de l'exécution (lent, mais possible). Une autre option facile avec est dans mon article édité.
Jaroslav Jandek
Vous n'avez pratiquement aucune vérification de type à la compilation significative dans votre code pour le moment - vous pouvez également essayer des objets dynamiques (vérification de type dynamique à l'exécution).
Jaroslav Jandek
2

Voici ma tentative. Il compile la vérification temporelle des types, en utilisant des contraintes de type génériques.

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

Cela pourrait nécessiter quelques jolies. Surtout, je ne pouvais pas comprendre comment se débarrasser des paramètres de type dans As / Is / Set (n'y a-t-il pas un moyen de spécifier un paramètre de type et de laisser C # figurer l'autre?)

Amnon
la source
2

J'ai donc rencontré le même problème plusieurs fois et je viens de trouver une solution qui obtient la syntaxe que je veux (au détriment d'une certaine laideur dans la mise en œuvre du type Union.)

Pour récapituler: nous voulons ce type d'utilisation sur le site d'appel.

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

Nous voulons que les exemples suivants ne soient pas compilés, cependant, afin que nous obtenions un minimum de sécurité de type.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Pour un crédit supplémentaire, n'occupons pas non plus plus d'espace que nécessaire.

Cela dit, voici mon implémentation pour deux paramètres de type générique. L'implémentation des paramètres de type trois, quatre et ainsi de suite est simple.

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();
        }
    }
}
Philippe Taron
la source
2

Et ma tentative de solution minimale mais extensible utilisant l' imbrication de type Union / Either . De plus, l'utilisation des paramètres par défaut dans la méthode Match active naturellement le scénario «Soit X soit par défaut».

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);
            }
        }
    }
}
papa
la source
1

Vous pouvez lancer des exceptions une fois qu'il y a une tentative d'accès à des variables qui n'ont pas été initialisées, c'est-à-dire que s'il est créé avec un paramètre A et que plus tard, il y a une tentative d'accès à B ou C, il peut lancer, par exemple, une exception UnsupportedOperationException. Vous auriez besoin d'un getter pour le faire fonctionner.

monsieur popo
la source
Oui - la première version que j'ai écrite a soulevé une exception dans la méthode As - mais même si cela met certainement en évidence le problème dans le code, je préfère de beaucoup être informé de cela au moment de la compilation qu'au moment de l'exécution.
Chris Fewtrell
0

Vous pouvez exporter une fonction de correspondance de pseudo-motifs, comme je l'utilise pour le type Either dans ma bibliothèque Sasa . Il y a actuellement une surcharge d'exécution, mais je prévois éventuellement d'ajouter une analyse CIL pour intégrer tous les délégués dans une véritable déclaration de cas.

nager
la source
0

Il n'est pas possible de faire exactement la syntaxe que vous avez utilisée, mais avec un peu plus de verbosité et de copier / coller, il est facile de faire en sorte que la résolution de surcharge fasse le travail pour vous:


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

À présent, il devrait être assez évident comment l'implémenter:


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

Il n'y a pas de contrôle pour extraire la valeur du mauvais type, par exemple:


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

Vous pouvez donc envisager d'ajouter les vérifications nécessaires et de lever des exceptions dans de tels cas.

Konstantin Oznobihin
la source
0

J'utilise le propre de Union Type.

Prenons un exemple pour le rendre plus clair.

Imaginez que nous ayons la classe Contact:

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

Celles-ci sont toutes définies comme de simples chaînes, mais ne sont-elles vraiment que des chaînes? Bien sûr que non. Le nom peut être composé du prénom et du nom. Ou un e-mail n'est-il qu'un ensemble de symboles? Je sais qu'au moins il devrait contenir @ et c'est nécessairement le cas.

Améliorons-nous le modèle de domaine

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

Dans ces classes, il y aura des validations lors de la création et nous aurons éventuellement des modèles valides. Le consturctor de la classe PersonaName requiert le prénom et le nom en même temps. Cela signifie qu'après la création, il ne peut pas avoir un état invalide.

Et classe de contact respectivement

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

Dans ce cas, nous avons le même problème, l'objet de la classe Contact peut être dans un état invalide. Je veux dire qu'il peut avoir EmailAddress mais pas Name

var contact = new Contact { EmailAddress = new EmailAddress("[email protected]") };

Corrigeons le problème et créons la classe Contact avec le constructeur qui nécessite PersonalName, EmailAddress et PostalAddress:

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

Mais ici, nous avons un autre problème. Que faire si la personne n'a que EmailAdress et n'a pas PostalAddress?

Si nous y réfléchissons, nous nous rendons compte qu'il existe trois possibilités d'état valide de l'objet de classe Contact:

  1. Un contact n'a qu'une adresse e-mail
  2. Un contact n'a qu'une adresse postale
  3. Un contact a à la fois une adresse e-mail et une adresse postale

Écrivons des modèles de domaine. Pour le début, nous allons créer la classe Contact Info dont l'état correspondra aux cas ci-dessus.

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

Et classe de contact:

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

Essayons de l'utiliser:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("[email protected]")
                  )
               );
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

Ajoutons la méthode Match dans la classe ContactInfo

public class ContactInfo 
{
   // constructor 
   public TResult Match<TResult>(
                      Func<EmailAddress,TResult> f1,
                      Func<PostalAddress,TResult> f2,
                      Func<Tuple<EmailAddress,PostalAddress>> f3
                  )
   {
        if (_emailAddress != null) 
        {
             return f1(_emailAddress);
        } 
        else if(_postalAddress != null)
        {
             ...
        } 
        ...
   }
}

Dans la méthode match, nous pouvons écrire ce code, car l'état de la classe de contact est contrôlé par des constructeurs et elle peut n'avoir qu'un seul des états possibles.

Créons une classe auxiliaire, pour qu'à chaque fois n'écrivez pas autant de code.

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

On peut avoir une telle classe à l'avance pour plusieurs types, comme c'est le cas avec les délégués Func, Action. 4-6 paramètres de type générique seront complets pour la classe Union.

Réécrivons la ContactInfoclasse:

public sealed class ContactInfo : Union<
                                     EmailAddress,
                                     PostalAddress,
                                     Tuple<EmaiAddress,PostalAddress>
                                  >
{
    public Contact(EmailAddress emailAddress) : base(emailAddress) { }
    public Contact(PostalAddress postalAddress) : base(postalAddress) { }
    public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}

Ici, le compilateur demandera un remplacement pour au moins un constructeur. Si nous oublions de remplacer le reste des constructeurs, nous ne pouvons pas créer d'objet de la classe ContactInfo avec un autre état. Cela nous protégera des exceptions d'exécution pendant la mise en correspondance.

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("[email protected]")
                  )
               );
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()
            )
    );

C'est tout. J'éspère que tu as apprécié.

Exemple tiré du site F # pour le plaisir et le profit

kogoia
la source