Unión discriminada en C #

[Nota: Esta pregunta tenía el título original ” C (ish) style union in C # “, pero como el comentario de Jeff me informó, aparentemente esta estructura se llama ‘unión discriminada’]

Disculpe la verbosidad de esta pregunta.

Hay un par de preguntas que suenan similares a las mías que ya están en SO, pero parecen concentrarse en los beneficios de ahorro de memoria de la unión o usarlo para la interoperabilidad. Aquí hay un ejemplo de tal pregunta .

Mi deseo de tener un tipo de unión es algo diferente.

Estoy escribiendo un código en este momento que genera objetos que se parecen un poco a esto

public class ValueWrapper { public DateTime ValueCreationDate; // ... other meta data about the value public object ValueA; public object ValueB; } 

Cosas bastante complicadas, creo que estarás de acuerdo. La ValueA es que ValueA solo puede ser de unos pocos tipos (digamos string , int y Foo (que es una clase) y ValueB puede ser otro conjunto pequeño de tipos. No me gusta tratar estos valores como objetos (quiero la cálida sensación de encoding con un poco de seguridad tipo).

Así que pensé en escribir una pequeña clase de contenedor para express el hecho de que ValueA lógicamente es una referencia a un tipo en particular. Llamé a la Union la clase porque lo que trato de lograr me recordó el concepto de unión en C.

 public class Union { private readonly Type type; public readonly A a; public readonly B b; public readonly C c; public AA{get {return a;}} public BB{get {return b;}} public CC{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; } ///  /// Returns true if the union contains a value of type T ///  /// The type of T must exactly match the type public bool Is() { return typeof(T) == type; } ///  /// Returns the union value cast to the given type. ///  /// If the type of T does not exactly match either X or Y, then the value default(T) is returned. public T As() { if(Is()) { 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()) { return (T)(object)b; } if(Is()) { return (T)(object)c; } return default(T); } } 

El uso de esta clase ValueWrapper ahora se ve así

 public class ValueWrapper2 { public DateTime ValueCreationDate; public Union ValueA; public Union ValueB; } 

que es algo así como lo que quería lograr, pero me falta un elemento bastante crucial: la verificación de tipo aplicada por el comstackdor al llamar a las funciones Is y As como lo demuestra el siguiente código

  public void DoSomething() { if(ValueA.Is()) { var s = ValueA.As(); // .... do somethng } if(ValueA.Is()) // I would really like this to be a compile error { char c = ValueA.As(); } } 

IMO No es válido preguntar a ValueA si es un char ya que su definición dice claramente que no es – este es un error de progtwigción y me gustaría que el comstackdor se de cuenta de esto. [También, si pudiera hacer esto correctamente, entonces (con suerte) obtendría intellisense también, lo cual sería una bendición.]

Para lograr esto, quisiera decirle al comstackdor que el tipo T puede ser uno de A, B o C

  public bool Is() where T : A or T : B // Yes I know this is not legal! or T : C { return typeof(T) == type; } 

¿Alguien tiene alguna idea de si lo que quiero lograr es posible? ¿O soy simplemente estúpido por escribir esta clase en primer lugar?

Gracias por adelantado.

Realmente no me gustan las soluciones de verificación de tipos y de conversión de tipos que se proporcionaron anteriormente, por lo que aquí hay una unión segura al 100% que arrojará errores de comstackción si intenta utilizar el tipo de datos incorrecto:

 using System; namespace Juliet { class Program { static void Main(string[] args) { Union3[] unions = new Union3[] { new Union3.Case1(5), new Union3.Case2('x'), new Union3.Case3("Juliet") }; foreach (Union3 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 { public abstract T Match(Func f, Func g, Func h); // private ctor ensures no external classes can inherit private Union3() { } public sealed class Case1 : Union3 { public readonly A Item; public Case1(A item) : base() { this.Item = item; } public override T Match(Func f, Func g, Func h) { return f(Item); } } public sealed class Case2 : Union3 { public readonly B Item; public Case2(B item) { this.Item = item; } public override T Match(Func f, Func g, Func h) { return g(Item); } } public sealed class Case3 : Union3 { public readonly C Item; public Case3(C item) { this.Item = item; } public override T Match(Func f, Func g, Func h) { return h(Item); } } } } 

Me gusta la dirección de la solución aceptada, pero no escala bien para uniones de más de tres elementos (por ejemplo, una unión de 9 elementos requeriría 9 definiciones de clase).

Aquí hay otro enfoque que también es 100% seguro en tiempo de comstackción, pero que es fácil de hacer crecer a grandes sindicatos.

 public class UnionBase { dynamic value; public UnionBase(A a) { value = a; } protected UnionBase(object x) { value = x; } protected T InternalMatch(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(Func fa) { return InternalMatch(fa); } } public class Union : UnionBase { public Union(A a) : base(a) { } public Union(B b) : base(b) { } protected Union(object x) : base(x) { } public T Match(Func fa, Func fb) { return InternalMatch(fa, fb); } } public class Union : Union { 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(Func fa, Func fb, Func fc) { return InternalMatch(fa, fb, fc); } } public class Union : Union { 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(Func fa, Func fb, Func fc, Func fd) { return InternalMatch(fa, fb, fc, fd); } } public class Union : Union { 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(Func fa, Func fb, Func fc, Func fd, Func fe) { return InternalMatch(fa, fb, fc, fd, fe); } } public class DiscriminatedUnionTest : IExample { public Union MakeUnion(int n) { return new Union(n); } public Union MakeUnion(bool b) { return new Union(b); } public Union MakeUnion(string s) { return new Union(s); } public Union MakeUnion(params int[] xs) { return new Union(xs); } public void Print(Union 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)); } } 

Aunque esta es una vieja pregunta, recientemente escribí algunas publicaciones en el blog sobre este tema que podrían ser útiles.

  • Tipos de unión en C #
  • Implementando Tic-Tac-Toe usando clases de estado

Supongamos que tiene un escenario de carrito de la compra con tres estados: “Vacío”, “Activo” y “Pagado”, cada uno con un comportamiento diferente .

  • Usted crea tiene una interfaz ICartState que todos los estados tienen en común (y podría ser simplemente una interfaz de marcador vacía)
  • Usted crea tres clases que implementan esa interfaz. (Las clases no tienen que estar en una relación de herencia)
  • La interfaz contiene un método de “doblez”, mediante el cual pasa una lambda para cada estado o caso que necesite manejar.

Podrías usar el tiempo de ejecución F # desde C # pero como una alternativa más liviana, he escrito una pequeña plantilla T4 para generar código como este.

Aquí está la interfaz:

 partial interface ICartState { ICartState Transition( Func cartStateEmpty, Func cartStateActive, Func cartStatePaid ); } 

Y aquí está la implementación:

 class CartStateEmpty : ICartState { ICartState ICartState.Transition( Func cartStateEmpty, Func cartStateActive, Func cartStatePaid ) { // I'm the empty state, so invoke cartStateEmpty return cartStateEmpty(this); } } class CartStateActive : ICartState { ICartState ICartState.Transition( Func cartStateEmpty, Func cartStateActive, Func cartStatePaid ) { // I'm the active state, so invoke cartStateActive return cartStateActive(this); } } class CartStatePaid : ICartState { ICartState ICartState.Transition( Func cartStateEmpty, Func cartStateActive, Func cartStatePaid ) { // I'm the paid state, so invoke cartStatePaid return cartStatePaid(this); } } 

Ahora supongamos que amplía CartStateEmpty y CartStateActive con un método AddItem que no está implementado por CartStatePaid .

Y también digamos que CartStateActive tiene un método Pay que otros estados no tienen.

Luego, aquí hay un código que lo muestra en uso: agregar dos elementos y luego pagar el carrito:

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

Tenga en cuenta que este código es completamente seguro, sin conversión ni condicionales en ningún lado, y compiler errors si intenta pagar un carrito vacío, por ejemplo.

He escrito una biblioteca para hacer esto en https://github.com/mcintyre321/OneOf

Install-Package OneOf

Tiene los tipos generics para hacer DU, por ejemplo, OneOf hasta OneOf . Cada uno de ellos tiene una .Match y una statement .Switch que puede usar para el comportamiento de .Switch segura del comstackdor, por ejemplo:

“ `

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

“ `

No estoy seguro de entender completamente tu objective. En C, una unión es una estructura que usa las mismas ubicaciones de memoria para más de un campo. Por ejemplo:

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

La unión floatOrScalar podría usarse como float, o como int, pero ambos consumen el mismo espacio de memoria. Cambiar uno cambia al otro. Puedes lograr lo mismo con una estructura en C #:

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

La estructura anterior usa 32 bits en total, en lugar de 64 bits. Esto solo es posible con una estructura. Su ejemplo anterior es una clase y, dada la naturaleza del CLR, no garantiza la eficacia de la memoria. Si cambia un Union de un tipo a otro, no necesariamente está reutilizando la memoria … lo más probable es que esté asignando un nuevo tipo en el montón y soltando un puntero diferente en el campo del object respaldo. Contrariamente a una unión real , su enfoque en realidad puede causar más agallas que lo que obtendría si no usara su tipo de Unión.

 char foo = 'B'; bool bar = foo is int; 

Esto da como resultado una advertencia, no un error. Si está buscando que sus funciones Is y As sean análogas para los operadores de C #, entonces no debería restringirlas de ninguna manera.

Si permite varios tipos, no puede lograr seguridad de tipo (a menos que los tipos estén relacionados).

No se puede lograr ni se logrará ningún tipo de seguridad de tipo, solo se puede lograr un valor de byte de seguridad usando FieldOffset.

Tendría mucho más sentido tener un ValueWrapper genérico ValueWrapper con T1 ValueA y T2 ValueB , …

PD: cuando hablo de seguridad de tipo me refiero a seguridad de tipo en tiempo de comstackción.

Si necesita un contenedor de código (realizando lógica de negocios en modificaciones, puede usar algo como:

 public class Wrapper { public ValueHolder v1 = 5; public ValueHolder v2 = 8; } public struct ValueHolder where T : struct { private T value; public ValueHolder(T value) { this.value = value; } public static implicit operator T(ValueHolder valueHolder) { return valueHolder.value; } public static implicit operator ValueHolder(T value) { return new ValueHolder(value); } } 

Para una salida fácil podría usar (tiene problemas de rendimiento, pero es muy simple):

 public class Wrapper { private object v1; private object v2; public T GetValue1() { if (v1.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v1; } public void SetValue1(T value) { v1 = value; } public T GetValue2() { if (v2.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v2; } public void SetValue2(T value) { v2 = value; } } //usage: Wrapper wrapper = new Wrapper(); wrapper.SetValue1("aaaa"); wrapper.SetValue2(456); string s = wrapper.GetValue1(); DateTime dt = wrapper.GetValue1();//InvalidCastException 

Aquí está mi bash. Comstack el control del tiempo de los tipos, utilizando restricciones de tipo genérico.

 class Union { public interface AllowedType { }; internal object val; internal System.Type type; } static class UnionEx { public static T As(this U x) where U : Union, Union.AllowedType { return x.type == typeof(T) ?(T)x.val : default(T); } public static void Set(this U x, T newval) where U : Union, Union.AllowedType { x.val = newval; x.type = typeof(T); } public static bool Is(this U x) where U : Union, Union.AllowedType { return x.type == typeof(T); } } class MyType : Union, Union.AllowedType, Union.AllowedType {} class TestIt { static void Main() { MyType bla = new MyType(); bla.Set(234); System.Console.WriteLine(bla.As()); System.Console.WriteLine(bla.Is()); System.Console.WriteLine(bla.Is()); bla.Set("test"); System.Console.WriteLine(bla.As()); System.Console.WriteLine(bla.Is()); System.Console.WriteLine(bla.Is()); // compile time errors! // bla.Set('a'); // bla.Is() } } 

Podría usar un poco de maquillaje. Especialmente, no pude descifrar cómo deshacerme de los parámetros de tipo en As / Is / Set (¿no hay una manera de especificar un parámetro de tipo y dejar que C # represente al otro?)

Así que me he topado con el mismo problema muchas veces, y acabo de presentar una solución que obtiene la syntax que quiero (a expensas de alguna fealdad en la implementación del tipo de Unión).

Para recapitular: queremos este tipo de uso en el sitio de llamadas.

 Union u; u = 1492; int yearColumbusDiscoveredAmerica = u; u = "hello world"; string traditionalGreeting = u; var answers = new SortedList>(); answers["life, the universe, and everything"] = 42; answers["D-Day"] = new DateTime(1944, 6, 6); answers["C#"] = "is awesome"; 

Sin embargo, no queremos comstackr los ejemplos siguientes para que tengamos un mínimo de seguridad tipo.

 DateTime dateTimeColumbusDiscoveredAmerica = u; Foo fooInstance = u; 

Para obtener crédito adicional, no tomemos más espacio de lo estrictamente necesario.

Con todo lo dicho, aquí está mi implementación para dos parámetros generics de tipo. La implementación de tres, cuatro, etc. parámetros de tipo es directa.

 public abstract class Union { 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 data) { return new FromT1(data); } public static implicit operator Union(T2 data) { return new FromT2(data); } public static implicit operator Union(Tuple data) { return new FromTuple(data); } public static implicit operator T1(Union source) { return source.AsT1(); } public static implicit operator T2(Union source) { return source.AsT2(); } private class FromT1 : Union { 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 { 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 { private readonly Tuple data; public FromTuple(Tuple 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(); } } } 

Y mi bash de solución mínima pero extensible utilizando el anidamiento de Union / Cualquiera de los dos tipos . Además, el uso de parámetros predeterminados en el método de coincidencia permite naturalmente el escenario “O X X predeterminado”.

 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.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>.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( this Either> source, Func a = null, Func b = null, Func c = null) { return source.Match(a, bc => bc.Match(b, c)); } } public abstract class Either { public static Either Of(A a) { return new CaseA(a); } public static Either Of(B b) { return new CaseB(b); } public abstract T Match(Func a = null, Func b = null); private sealed class CaseA : Either { private readonly A _item; public CaseA(A item) { _item = item; } public override T Match(Func a = null, Func b = null) { return a == null ? default(T) : a(_item); } } private sealed class CaseB : Either { private readonly B _item; public CaseB(B item) { _item = item; } public override T Match(Func a = null, Func b = null) { return b == null ? default(T) : b(_item); } } } } 

Puede lanzar excepciones una vez que haya un bash de acceder a las variables que no se han inicializado, es decir, si se crea con un parámetro A y más tarde hay un bash de acceder a B o C, podría arrojar, por ejemplo, UnsupportedOperationException. Sin embargo, necesitarías un getter para que funcione.

Puede exportar una función de coincidencia de pseudo-patrón, como la que uso para Cualquiera de los tipos en mi biblioteca Sasa . Actualmente hay sobrecarga en tiempo de ejecución, pero eventualmente planeo agregar un análisis CIL para alinear a todos los delegates en una statement de caso verdadero.

No es posible hacer exactamente con la syntax que ha usado, pero con un poco más de detalle y copiar / pegar es fácil hacer que la resolución de sobrecarga haga el trabajo por usted:

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

Por ahora debería ser bastante obvio cómo implementarlo:

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

No hay controles para extraer el valor del tipo incorrecto, por ejemplo:

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

Entonces, podría considerar agregar cheques necesarios y lanzar excepciones en tales casos.

Uso propio de Union Type.

Considera un ejemplo para hacerlo más claro.

Imagina que tenemos clase de contacto:

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

Todos estos se definen como cadenas simples, pero en realidad son solo cadenas. Por supuesto no. El nombre puede constar de nombre y apellido. ¿O es un correo electrónico simplemente un conjunto de símbolos? Sé que al menos debería contener @ y es necesariamente.

Vamos a mejorar el modelo de dominio de nosotros

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

En esta clase habrá validaciones durante la creación y eventualmente tendremos modelos válidos. Consturctor en la clase PersonaName requiere FirstName y LastName al mismo tiempo. Esto significa que después de la creación, no puede tener un estado inválido.

Y clase de contacto respectivamente

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

En este caso tenemos el mismo problema, el objeto de la clase de contacto puede estar en estado inválido. Quiero decir que puede tener EmailAddress pero no tiene nombre

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

Arreglemos y creemos la clase de contacto con el constructor que requiere PersonalName, EmailAddress y PostalAddress:

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

Pero aquí tenemos otro problema. ¿Qué pasa si la persona solo tiene EmailAdress y no tiene PostalAddress?

Si lo pensamos allí, nos damos cuenta de que hay tres posibilidades de estado válido de objeto de clase de contacto:

  1. Un contacto solo tiene una dirección de correo electrónico
  2. Un contacto solo tiene una dirección postal
  3. Un contacto tiene una dirección de correo electrónico y una postal

Vamos a escribir modelos de dominio. Para el comienzo crearemos la clase de información de contacto, que estado corresponderá con los casos anteriores.

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

Y clase de contacto:

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

Probemos usarlo:

 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 

Agreguemos el método de coincidencia en la clase ContactInfo

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

En el método de coincidencia, podemos escribir este código, porque el estado de la clase de contacto se controla con constructores y puede tener solo uno de los estados posibles.

Vamos a crear una clase auxiliar, para que cada vez no escriba tantos códigos.

 public abstract class Union 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( Func f1, Func f2, Func 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"); } } 

Podemos tener una clase de este tipo por adelantado para varios tipos, como se hace con los delegates Func, Action. 4-6 parámetros generics de tipo estarán completos para la clase Union.

Vamos a reescribir la clase ContactInfo :

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

Here the compiler will ask override for at least one constructor. If we forget to override the rest of the constructors we can’t create object of ContactInfo class with another state. This will protect us from runtime exceptions during Matching.

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

Eso es todo. I hope you enjoyed.

Example taken from the site F# for fun and profit

The C# Language Design Team discussed discriminated unions in January 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types

You can vote for the feature request at https://github.com/dotnet/csharplang/issues/113