web-dev-qa-db-fra.com

Union discriminée en C #

[Remarque: cette question avait pour titre d'origine le titre " Union de style C (ish) en C # " mais comme l'a dit le commentaire de Jeff, cette structure s'appelle apparemment "syndicat discriminé"]

Excusez la verbosité de cette question.

La question suivante a déjà été posée à SO, mais elles semblent se concentrer sur les avantages de l'union pour l'économie de mémoire ou sur son utilisation 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 à ceci

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

    public object ValueA;
    public object ValueB;
}

Des choses assez compliquées, je pense que vous en conviendrez. Le fait est que ValueA ne peut être que de certains types (disons string, int et Foo (qui est une classe) et ValueB peut être un autre petit groupe 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é du 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 Union car ce que j'essayais de réaliser me rappelait le concept de syndicat 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);
    }
}

En utilisant cette classe, ValueWrapper ressemble maintenant à ceci

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

ce qui est quelque chose comme ce que je voulais réaliser mais il me manque un élément crucial: la vérification de type imposée par le compilateur lors de l'appel des fonctions Is and 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>();
        }
    }

OMI Il n'est pas correct de demander à ValueA s'il s'agit d'une char car sa définition dit clairement que ce n'est pas le cas. Il s'agit d'une erreur de programmation et j'aimerais que le compilateur en parle. [Aussi, si je pouvais obtenir cela correctement alors (j'espère), j'aurais aussi intellisense - ce qui serait une aubaine.]

Afin de réaliser ceci, je voudrais dire au compilateur que le type T peut être 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 at-il une idée si ce que je veux réaliser est possible? Ou suis-je tout simplement stupide d'avoir écrit ce cours en premier lieu?

Merci d'avance.

75
Chris Fewtrell

Je n'aime pas vraiment les solutions de vérification de type et de transposition de type 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);
        // 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);
            }
        }
    }
}
92
Juliet

J'aime bien la direction de la solution acceptée, mais elle ne convient pas aux syndicats de plus de trois articles (par exemple, une union 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));
    }
}
27
cdiggins

Même s'il s'agit d'une vieille question, j'ai récemment écrit quelques articles de blog sur ce sujet qui pourraient être utiles.

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

  • Vous créez une interface ICartState que tous les états ont en commun (et il pourrait s'agir simplement d'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", dans laquelle vous passez un lambda pour chaque état ou cas que vous devez gérer.

Vous pouvez utiliser le runtime F # de C #, mais comme alternative plus légère, j’ai écrit un petit modèle T4 pour générer un 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 l'implémentation:

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

Supposons maintenant que vous étendiez CartStateEmpty et CartStateActive avec une méthode AddItem qui est not implémentée par CartStatePaid

Et aussi, disons que CartStateActive a une méthode Pay que les autres états n’ont pas.

Ensuite, voici un code qui le montre en cours d'utilisation - ajoutant deux éléments, puis payant pour 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 totalement typé - pas de transtypage ni de conditionnels, et des erreurs de compilation si vous essayez de payer pour un panier vide, par exemple.

14
Grundoon

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

Paquet d'installation OneOf

Il contient les types génériques pour effectuer des UD, par exemple. OneOf<T0, T1> jusqu’à OneOf<T0, ..., T9>. Chacun de ceux-ci a un .Match et une instruction .Switch que vous pouvez utiliser pour un comportement typé sans risque pour le compilateur, par exemple:

`` `

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

`` `

7
mcintyre321

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

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

L'union floatOrScalar pourrait être utilisée comme un float ou un int, mais toutes deux consomment 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 un total de 32 bits au lieu de 64 bits. Ceci n'est possible qu'avec une structure. Votre exemple ci-dessus est une classe et, compte tenu de la nature du CLR, ne garantit en rien 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 ... vous affectez probablement un nouveau type sur le tas et déposez un autre pointeur dans le champ object de sauvegarde. Contrairement à une union real, votre approche risque en réalité de générer plus de contusions que ce que vous auriez autrement obtenu si vous n’utilisiez pas votre type Union.

5
jrista

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

Il pourrait utiliser un peu de maquillage. Surtout, je ne pouvais pas comprendre comment se débarrasser des paramètres de type à As/Is/Set (n’existe-t-il pas un moyen de spécifier un paramètre de type et de laisser C # le représenter?)

2
Amnon

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 ne réaliserez aucun type de sécurité, vous ne pourrez atteindre que la sécurité en octets en utilisant FieldOffset.

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

P.S .: quand je parle de sécurité de type, je parle de sécurité de type à la compilation.

Si vous avez besoin d'un wrapper de code (qui exécute la logique de gestion sur les modifications), vous pouvez utiliser quelque chose dans les lignes suivantes:

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 sortie facile, vous pouvez utiliser (il y 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
2
Jaroslav Jandek

Et ma tentative de solution minimale mais extensible en utilisant imbrication de Union/type soit . De même, l’utilisation de paramètres par défaut dans la méthode Match active naturellement le scénario «Soit X ou 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);
            }
        }
    }
}
2
dadhi

J'ai donc rencontré le même problème à plusieurs reprises et je viens juste de proposer une solution qui obtienne la syntaxe que je souhaite (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";

Toutefois, nous souhaitons que les exemples suivants ne parviennent pas à être compilés afin d'obtenir un minimum de sécurité.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Pour un crédit supplémentaire, ne prenons pas plus de place que nécessaire.

Ceci dit, voici mon implémentation pour deux paramètres de type génériques. L'implémentation pour les paramètres de type trois, quatre, etc., 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();
        }
    }
}
2
Philip Taron
char foo = 'B';

bool bar = foo is int;

Cela entraîne un avertissement, pas une erreur. Si vous souhaitez que vos fonctions Is et As soient analogues aux opérateurs C #, vous ne devriez pas les restreindre de cette façon de toute façon.

2
Adam Robinson

Vous pouvez lancer des exceptions après avoir tenté d'accéder à des variables non initialisées, c'est-à-dire que si elles sont créées avec un paramètre A, puis si vous essayez d'accéder à B ou à C, vous pouvez générer, par exemple, UnsupportedOperationException. Vous aurez besoin d'un getter pour le faire fonctionner cependant.

1
mr popo

Il n'est pas possible de faire exactement avec 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 la 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());
}

A présent, il devrait être assez évident de savoir comment le mettre en œuvre:


    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’existe pas de vérification pour extraire la valeur du type incorrect, 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.

0

L'équipe de conception du langage C # a discuté de syndicats discriminés en janvier 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed -les types

Vous pouvez voter pour la demande de fonctionnalité sur https://github.com/dotnet/csharplang/issues/113

0
Colonel Panic

J'utilise propre de Union Type.

Prenons un exemple pour le rendre plus clair.

Imaginons 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 sont-elles vraiment des chaînes? Bien sûr que non. Le nom peut être composé du prénom et du nom de famille. Ou un courriel est-il juste un ensemble de symboles? Je sais qu’au moins cela devrait contenir @ et c’est nécessairement.

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 seront des validations lors de la création et nous aurons éventuellement des modèles valides. Consturctor dans la classe PersonaName nécessite FirstName et LastName en même temps. Cela signifie qu'après la création, il ne peut pas y avoir d'é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, objet de la classe Contact peut être dans un état invalide. Je veux dire qu'il peut avoir EmailAddress mais pas Nom

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

Corrigeons-le et créons une classe Contact avec 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 se passe-t-il si la personne a seulement EmailAdress et pas PostalAddress? 

Si nous y réfléchissons, nous réalisons qu’il existe trois possibilités d’état valide d’objet de la classe Contact:

  1. Un contact a seulement une adresse email
  2. Un contact a seulement une adresse postale
  3. Un contact a à la fois une adresse email et une adresse postale

Écrivons des modèles de domaine. Pour le début, nous allons créer une 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 de correspondance, nous pouvons écrire ce code, car l'état de la classe de contact est contrôlé par des constructeurs et peut ne comporter qu'un seul des états possibles.

Créons une classe auxiliaire, de sorte que 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");
    }
}

Nous pouvons 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ériques seront intégralement définis pour la classe Union.

Réécrivons la classe ContactInfo:

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 le remplacement d'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 lors de 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'espère que vous avez apprécié.

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

0
kogoia

Vous pouvez exporter une fonction de correspondance de pseudo-motif, comme celle que j'utilise pour le type soit dans la 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 déclaration de cas véritable.

0
naasking