Question

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

Excusez la verbosité de cette question.

Il y a quelques questions de sondage semblable à la mienne déjà en SO, mais ils semblent se concentrer sur les avantages d'économie mémoire du syndicat ou de l'utiliser pour Interop. Voici un exemple d'une telle question .

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

Je suis en train d'écrire un code au moment qui génère des objets qui ressemblent un peu comme ça

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

    public object ValueA;
    public object ValueB;
}

choses assez compliqué je pense que vous serez d'accord. La chose est que ValueA ne peut être de quelques certains types (disons string, int et Foo (qui est une classe) et ValueB peut être un autre petit ensemble de types. Je ne aime pas traiter ces valeurs comme des objets (je veux le sentiment chaud parfaitement de codage avec un peu de sécurité de type).

Alors je pensais à écrire une classe peu d'emballage trivial pour exprimer le fait que ValueA est logiquement une référence à un type particulier. J'ai appelé le Union de classe parce que ce que je suis en train de réaliser m'a rappelé le concept syndical 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 je manque un élément assez important - qui est compilateur appliqué la vérification de type lors de l'appel est et comme fonctions que le code suivant montre

    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 valable de demander ValueA si elle est un char puisque sa définition dit clairement qu'il n'est pas - ce qui est une erreur de programmation et je souhaite que le compilateur remarque à ce sujet. [Aussi, si je pouvais obtenir ce correct alors (je l'espère) je me IntelliSense trop -. Ce qui serait une aubaine]

Pour parvenir à cela, je veux dire au compilateur que le T de type 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;
    } 

Est-ce que quelqu'un a une idée si ce que je veux atteindre est possible? Ou suis-je tout simplement stupide pour écrire cette classe en premier lieu?

Merci à l'avance.

Était-ce utile?

La solution

Je ne aime pas vraiment le type de vérification et des solutions de type coulée fourni ci-dessus, voici donc 100% union de type sécurité qui jetteront des erreurs de compilation si vous essayez d'utiliser le 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);
            }
        }
    }
}

Autres conseils

Je aime la direction de la solution retenue, mais elle n'échelle pas bien pour les syndicats de plus de trois éléments (par exemple une union de 9 éléments nécessiterait 9 définitions de classe).

Voici une autre approche qui est aussi 100% à la compilation de type sécurisé, mais qui est facile à atteindre de grands syndicats.

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

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

Le mot Let vous avez un scénario de panier à trois états. « Vide », « Active » et « payé », chacun avec différents comportement

  • Vous créez une interface ICartState que tous les États ont en commun (et il pourrait être juste une interface marqueur vide)
  • Vous créez trois classes qui implémentent cette interface. (Les classes ne doivent pas être dans une relation d'héritage)
  • L'interface contient un « pli », ce par quoi on passe d'un lambda dans chaque état ou le cas où il faut poignée.

Vous pouvez utiliser le F # C # de l'exécution, mais comme une alternative de poids plus léger, je l'ai écrit un petit modèle de 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);
  }
}

Maintenant, nous allons vous dire étendre la CartStateEmpty et CartStateActive avec une méthode AddItem qui est pas mis en œuvre par CartStatePaid.

Et disons que CartStateActive a une méthode Pay que les autres Etats dont ont.

Alors, voici un code qui le montre en cours d'utilisation - l'ajout de deux articles et payer 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 complètement typesafe -. Pas coulée ou conditionals partout, et les erreurs du compilateur si vous essayez de payer pour un chariot vide, par exemple

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

  

Installer-Package OneOf

Il a les types génériques pour ce faire par exemple DDVP OneOf<T0, T1> tout le chemin à OneOf<T0, ..., T9>. Chacun de ces a une .Match, et une déclaration de .Switch que vous pouvez utiliser pour le compilateur comportement typé sécurité, par exemple:.

`` `

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

`` `

Je comprends pas que je complètement votre objectif. En C, une union est une structure qui utilise les mêmes emplacements de mémoire de plus d'un champ. Par exemple:

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

Le syndicat floatOrScalar pourrait être utilisé comme un flotteur, ou un int, mais ils ont tous deux consomment le même espace mémoire. Le changement d'un change l'autre. Vous pouvez obtenir la même chose avec un struct en C #:

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

total de l'utilisation de la structure ci-dessus, au lieu de 64 bits. Ceci est seulement possible avec une struct. Votre exemple ci-dessus est une classe, et compte tenu de la nature du CLR, ne fait aucune garantie quant à l'efficacité de la mémoire. Si vous modifiez un Union<A, B, C> d'un type à l'autre, vous n'êtes pas nécessairement réutilisez la mémoire ... plus probablement, vous allouez un nouveau type sur le tas et laisser tomber un autre pointeur dans le champ object de soutien. Contrairement à une véritable union , votre approche peut effectivement causer plus raclée tas que vous autrement obtenir si vous n'avez pas utilisé votre type Union.

char foo = 'B';

bool bar = foo is int;

Il en résulte un avertissement, pas une erreur. Si vous cherchez vos fonctions de Is et As d'être analogues pour les opérateurs C #, alors vous ne devriez pas les limiter de cette façon de toute façon.

Si vous autorisez plusieurs types, vous ne pouvez pas assurer la sécurité de type (à moins que les types sont liés).

Vous ne pouvez pas et ne réaliser aucune sorte de sécurité de type, vous ne pouvez obtenir octet valeur de sécurité en utilisant FieldOffset.

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

P.S .: quand on parle de type sécurité, je veux dire de type sécurité compilation.

Si vous avez besoin d'une enveloppe de code (exécuter la logique de bussiness sur les modifications que vous pouvez utiliser quelque chose le long des lignes de:

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 un moyen facile, vous pouvez utiliser (il a des problèmes de performance, mais il 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

Voici ma tentative. Il ne la compilation vérification des types, en utilisant des contraintes de type générique.

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 une prettying-up. Surtout, je ne pouvais pas comprendre comment se débarrasser des paramètres de type à As / Is / Set (n'est pas là un moyen de spécifier un paramètre de type C # et laisser l'autre chiffre?)

Je l'ai frappé ce même problème plusieurs fois, et je viens est venu avec 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ésumer: nous voulons ce genre 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 parviennent pas à compiler, cependant, de sorte que nous obtenons un minimum de sécurité de type.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Pour les crédits supplémentaires, nous allons pas non plus prendre plus de place que absolument nécessaire.

Avec tout ce que dit, voici ma mise en œuvre de deux paramètres de type générique. La mise en œuvre de trois, quatre, et ainsi sur les paramètres de type 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();
        }
    }
}

Et ma tentative de solution minimale encore extensible en utilisant imbrication de l'Union / ou l'autre type . En outre l'utilisation des paramètres par défaut de méthode permet naturellement match scénario « 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);
            }
        }
    }
}

Vous pourriez lancer des exceptions une fois qu'il ya une tentative de variables d'accès qui ne sont pas initialisés, à savoir si elle est créée avec un paramètre A et plus tard, il y a une tentative d'accès B ou C, il pourrait jeter, par exemple, UnsupportedOperationException. Vous auriez besoin d'un getter pour le faire fonctionner bien.

Vous pouvez exporter une fonction d'appariement pseudo-modèle, comme je l'utilise pour le type soit dans mon bibliothèque Sasa . Il y a actuellement exécution en tête, mais je prévois d'ajouter éventuellement une analyse de CIL à inline tous les délégués dans une déclaration véritable cas.

Il est impossible de le faire avec exactement la syntaxe que vous avez utilisé, mais avec un peu plus verbosité et copier / coller il est facile de faire de la résolution de surcharge faire 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());
}

Maintenant, il devrait être assez évident comment la 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'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 envisager d'ajouter des contrôles nécessaires et lancer des exceptions dans de tels cas.

J'utilise propre de type Union.

Prenons un exemple pour le rendre plus clair.

Imaginez que nous avons la classe Contact:

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

Elles sont toutes définies comme des chaînes simples, mais sont vraiment ils seulement des chaînes? Bien sûr que non. Le nom peut se composer de Prénom et Nom. Ou est un e-mail juste un ensemble de symboles? Je sais qu'au moins il doit contenir @ et il est nécessairement.

nous allons nous améliorer 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 ce cours sera au cours de la création et la validation nous finirons par avoir des modèles valides. Consturctor en classe PersonaName nécessitent FirstName et LastName en même temps. Cela signifie que, après la création, il ne peut pas avoir un état non valide.

Et la 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 même problème, objet de la classe Le contact peut être dans un état non valide. Je veux dire qu'il peut avoir EmailAddress mais pas de nom

var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };

Fix Let et créer 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 personne ont seulement emailadress et pas PostalAddress?

Si nous pensons à ce sujet là, nous rendons compte qu'il ya trois possibilités d'état de contact valide objet de classe:

  1. Un contact a uniquement une adresse e-mail
  2. Un contact a seulement une adresse postale
  3. Un contact a une adresse e-mail et une adresse postale

write Let sur les modèles de domaine. Pour commencer, nous allons créer la classe Contact Info quel état sera correspondant à des cas ci-dessus.

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

Et la classe Contact:

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

Soit l'utilisation de l'essayer:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases

add Let méthode Match 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 les constructeurs et il peut avoir qu'un seul des états possibles.

Créons une classe auxiliaire, de sorte que chaque fois ne pas écrire 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 cela se fait avec les délégués Func, Action. 4-6 paramètres de type générique seront en pleine classe pour l'Union.

Ressaisissez classe ContactInfo Let:

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 remplacement pour au moins un constructeur. Si l'on oublie de passer outre le reste des constructeurs que nous ne pouvons pas créer un objet de classe ContactInfo avec un autre Etat. Cela nous protéger des exceptions d'exécution lors de l'appariement.

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console
    .WriteLine(
        contact
            .ContactInfo()
            .Match(
                (emailAddress) => emailAddress.Address,
                (postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
                (emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
            )
    );

C'est tout. J'espère que vous avez apprécié.

Exemple pris sur le site F # pour le plaisir et le profit

La conception du langage C # l'équipe ont discuté discriminé les syndicats en Janvier 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions- via-types-fermé

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top