Discrimination syndicale en C#
[Note: cette question avait le titre original c (ish) style union in C# " mais comme le commentaire de Jeff m'a informé, apparemment cette structure est appelé un "syndicat discriminé"]
Excusez la verbosité de cette question.
Il ya un couple de questions de sondage similaires à la mienne déjà dans SO, mais ils semblent se concentrer sur les avantages de la mémorisation de l'union 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érent.
j'écris actuellement un 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;
}
c'est assez compliqué, je pense que vous serez d'accord. La chose est que ValueA
ne peut être que de quelques types certains (disons string
, int
et Foo
(qui est une classe) et ValueB
peut-être un autre petit ensemble de types. Je n'aime pas traiter ces valeurs comme des objets (je veux le sentiment chaleureux de codage avec un peu de sécurité de type).
alors j'ai pensé à écrire une classe de petit emballage trivial pour exprimer le fait que ValueA logiquement est une référence à un type particulier. J'ai appelé la classe Union
parce que ce que j'essaie de réaliser m'a rappelé le concept de l'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);
}
}
en utilisant ce Classe ValueWrapper ressemble maintenant à cela
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 accomplir mais je manque un élément assez crucial - qui est compilateur contrôle de type forcé lors de l'appel de L'Is et des fonctions comme le code suivant dé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>();
}
}
IMO il n'est pas valable de demander à ValueA si c'est un char
puisque sa définition dit clairement qu'il n'est pas - il s'agit d'une erreur de programmation et je voudrais le compilateur pour ramasser sur ce. [Aussi si je pouvais obtenir ce correct puis (espérons) je recevrais intellisense aussi - qui serait une aubaine.]
pour y parvenir 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;
}
est-ce que quelqu'un sait si ce que je veux accomplir est possible? Ou suis-je simplement stupide d'avoir écrit ce cours?
Merci d'avance.
15 réponses
Je n'aime pas vraiment les solutions de vérification de type et de type-casting fournies ci-dessus, alors voici 100% type-safe union qui jettera des erreurs de compilation si vous tentez 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);
}
}
}
}
j'aime la direction de la solution acceptée, mais elle n'est pas à bonne échelle pour les unions 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 également 100% type-sûr au moment de la compilation, mais qui est facile à développer pour les 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));
}
}
même si c'est une vieille question, j'ai récemment écrit quelques billets de blog sur ce sujet qui pourraient être utiles.
disons que vous avez un scénario de panier avec trois états: "vide", "actif" et "payé", chacun avec différent comportement.
- vous créez ont une interface
ICartState
que tous les États ont en commun (et il pourrait juste être une interface de marqueur vide) - vous créez trois classes qui implémentent cette interface. (Les classes n'ont pas à être dans une relation d'héritage)
- l'interface contient une méthode" fold", par laquelle vous passez une lambda dans pour chaque État ou cas que vous devez manipuler.
vous pouvez utiliser le F # runtime à 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);
}
}
maintenant, disons que vous prolongez le CartStateEmpty
et CartStateActive
avec une méthode AddItem
qui est pas mis en œuvre par CartStatePaid
.
et aussi disons que CartStateActive
a une méthode Pay
que les autres États n'ont pas.
puis voici un code qui le montre en cours d'utilisation -- en ajoutant deux articles et en 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 complètement typesafe -- aucun casting ou conditionals n'importe où, et erreurs de compilateur si vous essayez de payer pour un panier vide, disons.
j'ai écrit une bibliothèque pour faire ceci à https://github.com/mcintyre321/OneOf
Install-Package OneOf
il a les types génériques dans lui pour faire des DUs par exemple OneOf<T0, T1>
tout le chemin pour
OneOf<T0, ..., T9>
. Chacun de ceux-ci a un .Match
, et un .Switch
déclaration que vous pouvez utiliser pour le compilateur comportement dactylographié sûr, par exemple:
""
OneOf<string, ColorName, Color> backgroundColor = getBackground();
Color c = backgroundColor.Match(
str => CssHelper.GetColorFromString(str),
name => new Color(name),
col => col
);
""
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 plus d'un domaine. Par exemple:
typedef union
{
float real;
int scalar;
} floatOrScalar;
le floatOrScalar
union pourrait être utilisé comme un flotteur, 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. Ce est seulement possible avec une structure. Votre exemple ci-dessus est une classe, et compte tenu de la nature du CLR, ne fait 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 attribuez un nouveau type sur le tas et en laissant tomber un pointeur différent dans le champ de soutien object
. Contrairement à un une véritable union , votre approche peut causer plus de tas raclée que vous obtenez si vous n'avez pas utilisé votre type de Syndicat.
char foo = 'B';
bool bar = foo is int;
il en résulte un avertissement, pas une erreur. Si vous cherchez que vos fonctions Is
et As
soient des analogues pour les opérateurs C#, alors vous ne devriez pas les restreindre de cette façon de toute façon.
si vous autorisez plusieurs types, vous ne pouvez pas atteindre la sécurité du type (sauf si les types sont apparentés).
vous ne pouvez pas et ne pourrez pas obtenir n'importe quel type de sécurité de type, vous pourriez seulement atteindre byte-valeur-sécurité en utilisant FieldOffset.
Il serait beaucoup plus logique d'avoir un générique ValueWrapper<T1, T2>
avec T1 ValueA
et T2 ValueB
, ...
P. S.: Quand on parle de sécurité de type, je veux dire sécurité de type de compilation-time.
si vous avez besoin d'un code wrapper (exécution de la logique bussiness sur les modifications, vous pouvez utiliser quelque chose comme:
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 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 compile la vérification de temps 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 préchauffage. En particulier, Je ne pouvais pas trouver comment se débarrasser des paramètres de type à As/Is/Set (n'y a-t-il pas une façon de spécifier un paramètre de type et laisser C# figure l'autre?)
donc j'ai frappé ce même problème plusieurs fois, et je viens de trouver une solution qui obtient la syntaxe que je veux (au détriment de la laideur dans la mise en œuvre du type de L'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 compilent pas, cependant, de sorte que nous obtenions un minimum de sécurité de type.
DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;
pour les crédits supplémentaires, ne prenons pas non plus plus d'espace que ce qui est absolument nécessaire.
avec tout ce que cela dit, Voici mon implémentation pour deux paramètres de type génériques. La mise en œuvre pour trois, quatre, et ainsi de suite 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 mon essai sur la solution minimale mais extensible en utilisant la nidification de L'Union/L'un ou l'autre type . De plus, l'utilisation des paramètres par défaut dans la méthode Match permet naturellement le scénario "Either X Or Default".
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 pouvez lancer des exceptions une fois qu'il y a une tentative d'accéder à des variables qui n'ont pas été initialisées, c'est à dire si elle est créée avec un paramètre A et plus tard il y a une tentative d'accéder à B ou C, elle pourrait lancer, disons, UnsupportedOperationException. Il faudrait un getter pour que ça marche.
vous pouvez exporter une fonction de correspondance pseudo-motif, comme j'utilise pour L'un ou l'autre type dans ma bibliothèque Sasa . Il y a actuellement des frais généraux de fonctionnement, mais j'ai éventuellement l'intention d'ajouter une analyse CIL à la liste de tous les délégués dans une déclaration de Cas Véritable.
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 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());
donc vous pourriez envisager d'ajouter les contrôles nécessaires et de jeter des exceptions dans de tels cas.
j'utilise propre de type Union.
considérez un exemple pour le rendre plus clair.
Imaginez que nous sommes en Contact de la classe:
public class Contact
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public string PostalAdrress { get; set; }
}
ce sont tous définis comme des cordes simples, mais sont-ils vraiment que des cordes? Bien sûr que non. Le nom peut comprendre le prénom et le nom de famille. Ou un e-Mail tout un ensemble de symboles? Je sais qu'au moins il doit contenir @ et il 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 cette classe sera 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 avoir l'état invalide.
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 le même problème, objet de la classe de Contact peut être dans l'état invalide. Je veux dire il peut avoir EmailAddress mais n'ont pas de nom
var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };
fixons-le et créons une classe de 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 une personne n'a qu'une adresse e-mail et n'a pas D'adresse postale?
si nous y réfléchissons, nous nous rendons compte qu'il y a trois possibilités d'état de Contact valable objet de classe:
- à Une personne de contact a une adresse e-mail
- à Une personne de contact a une adresse postale
- une personne-ressource a à la fois une adresse électronique et une adresse postale
écrivons des modèles de domaines. Pour le début nous allons créer la classe D'information de Contact quel état correspondra avec les 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("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
ajoutons la méthode de correspondance 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, parce que l'état de la classe contact est contrôlé avec les constructeurs et il peut n'avoir qu'un des états possibles.
créons une classe auxiliaire, de sorte que chaque fois n'écrivent 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 fait avec les délégués Func, Action. 4-6 les paramètres de type générique seront complets pour la classe Union.
réécrivons ContactInfo
classe:
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 va demander l'annulation d'au moins un constructeur. Si nous oublions de surcharger le reste des constructeurs, nous ne pouvons pas créer l'objet de la classe ContactInfo avec un autre état. Cela nous protégera des exceptions d'exécution pendant 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 tiré du site F# pour le plaisir et le profit
l'équipe C# Language Design a discuté des syndicats discriminés en janvier 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
vous pouvez voter pour la demande de caractéristique à https://github.com/dotnet/csharplang/issues/113