[Nota: Esta pregunta tenía el título original " Unión de estilo C (ish) en C # ", pero como me informó el comentario de Jeff, 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 ya en SO, pero parecen concentrarse en los beneficios de ahorro de memoria de la unión o en su uso para la interoperabilidad. Aquí hay un ejemplo de tal pregunta .
Mi deseo de tener algo tipo sindicato 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. El caso es que ValueA
solo puede ser de algunos tipos determinados (digamos string
, int
y Foo
(que es una clase) y ValueB
puede ser otro pequeño conjunto de tipos. No me gusta tratar estos valores como objetos (quiero la sensación cálida y acogedora de codificación con un poco de seguridad de tipo).
Así que pensé en escribir una pequeña clase contenedora trivial para expresar el hecho de que ValueA lógicamente es una referencia a un tipo en particular. Llamé a la clase Union
porque lo que estoy tratando de lograr me recordó el concepto de unión 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);
}
}
El uso de esta clase ValueWrapper ahora se ve así
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
que es algo así como lo que quería lograr, pero me falta un elemento bastante crucial: la verificación de tipos impuesta por el compilador al llamar a las funciones Is y As, como lo demuestra el siguiente código
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>();
}
}
En mi opinión, no es válido preguntar a ValueA si es un, char
ya que su definición dice claramente que no lo es; esto es un error de programación y me gustaría que el compilador lo captara . [Además, si pudiera hacer esto correctamente, entonces (con suerte) obtendría intellisense también, lo cual sería una bendición].
Para lograr esto, me gustaría decirle al compilador que el tipo T
puede ser uno de A, B o 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;
}
¿Alguien tiene alguna idea de si lo que quiero lograr es posible? ¿O simplemente soy estúpido por escribir esta clase en primer lugar?
Gracias por adelantado.
fuente
StructLayout(LayoutKind.Explicit)
yFieldOffset
. Esto no se puede hacer con tipos de referencia, por supuesto. Lo que está haciendo no es como una Unión C en absoluto.Respuestas:
Realmente no me gustan las soluciones de verificación de tipos y conversión de tipos proporcionadas anteriormente, así que aquí hay una unión 100% segura de tipos que arrojará errores de compilación si intenta utilizar el tipo de datos incorrecto:
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); } } } }
fuente
match
, y esa es una forma tan buena de conseguirlo como cualquier otra.type Result = Success of int | Error of int
Me gusta la dirección de la solución aceptada, pero no se adapta bien a las 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 para tipos en tiempo de compilación, pero que es fácil de ampliar a uniones grandes.
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)); } }
fuente
dynamic
& genéricos enUnionBase<A>
y la cadena de herencia parece innecesario. HagaUnionBase<A>
no genérico, elimine el constructor tomando anA
y hagavalue
anobject
(que es de todos modos; no hay ningún beneficio adicional al declararlodynamic
). Luego, obtenga cadaUnion<…>
clase directamente deUnionBase
. Esto tiene la ventaja de que solo seMatch<T>(…)
expondrá el método adecuado . (Como está ahora, por ejemplo,Union<A, B>
expone una sobrecargaMatch<T>(Func<A, T> fa)
que está garantizada para generar una excepción si el valor adjunto no es unA
. Eso no debería suceder).Escribí algunas publicaciones de blog sobre este tema que podrían ser útiles:
Supongamos que tiene un escenario de carrito de compras con tres estados: "Vacío", "Activo" y "Pagado", cada uno con un comportamiento diferente .
ICartState
interfaz que todos los estados tienen en común (y podría ser simplemente una interfaz de marcador vacía)Puede usar el tiempo de ejecución de F # de 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, ICartState> cartStateEmpty, Func<CartStateActive, ICartState> cartStateActive, Func<CartStatePaid, ICartState> cartStatePaid ); }
Y aquí está la implementación:
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); } }
Ahora digamos que amplía el
CartStateEmpty
yCartStateActive
con unAddItem
método que no está implementado porCartStatePaid
.Y también digamos que
CartStateActive
tiene unPay
método que los otros estados no tienen.Luego, aquí hay un código que lo muestra en uso: agregar dos artículos 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 para tipos: no hay conversiones ni condicionales en ninguna parte, y errores de compilación si intenta pagar por un carrito vacío, por ejemplo.
fuente
He escrito una biblioteca para hacer esto en https://github.com/mcintyre321/OneOf
Tiene los tipos genéricos para hacer DU, por ejemplo,
OneOf<T0, T1>
hasta el finalOneOf<T0, ..., T9>
. Cada uno de ellos tiene una declaración.Match
, y una.Switch
que puede usar para el comportamiento de tipo seguro del compilador, por ejemplo:''
OneOf<string, ColorName, Color> backgroundColor = getBackground(); Color c = backgroundColor.Match( str => CssHelper.GetColorFromString(str), name => new Color(name), col => col );
''
fuente
No estoy seguro de comprender completamente su objetivo. 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
floatOrScalar
unión podría usarse como flotante o como int, pero ambos consumen el mismo espacio de memoria. Cambiar uno cambia el otro. Puede 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 utiliza un total de 32 bits, 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 eficiencia de la memoria. Si cambia un
Union<A, B, C>
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 colocando un puntero diferente en elobject
campo de respaldo . Contrariamente a una unión real , su enfoque en realidad puede causar más palizas de las que obtendría si no usara su tipo de Unión.fuente
char foo = 'B'; bool bar = foo is int;
Esto da como resultado una advertencia, no un error. Si usted está buscando para sus
Is
yAs
funciones que debe análogos para los operadores de C #, entonces no debe ser restringirlos de esa manera de todos modos.fuente
Si permite varios tipos, no puede lograr la seguridad de tipos (a menos que los tipos estén relacionados).
No puede y no logrará ningún tipo de seguridad de tipo, solo podría lograr seguridad de valor de byte usando FieldOffset.
Tendría mucho más sentido tener un genérico
ValueWrapper<T1, T2>
conT1 ValueA
yT2 ValueB
, ...PD: cuando hablo de seguridad de tipos me refiero a seguridad de tipos en tiempo de compilación.
Si necesita un contenedor de código (realizando la lógica de negocios en las modificaciones, puede usar algo como:
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); } }
Para una salida fácil, puede usar (tiene problemas de rendimiento, pero es muy 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
fuente
Aquí está mi intento. Compila la verificación de tiempo de tipos, utilizando restricciones de tipo genérico.
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>() } }
Le vendría bien un poco de belleza. Especialmente, no pude averiguar 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 # calcule el otro?)
fuente
Así que me encontré con este mismo problema muchas veces y se me ocurrió una solución que obtiene la sintaxis que quiero (a expensas de algo de fealdad en la implementación del tipo Union).
En resumen: queremos este tipo de uso en el sitio de la llamada.
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";
Sin embargo, queremos que los siguientes ejemplos no se compilen para obtener un mínimo de seguridad de tipos.
Para obtener crédito adicional, tampoco ocupemos más espacio del absolutamente necesario.
Dicho todo esto, aquí está mi implementación para dos parámetros de tipo genérico. La implementación de tres, cuatro, etc. parámetros de tipo es sencilla.
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(); } } }
fuente
Y mi intento de una solución mínima pero extensible usando anidamiento de Union / Either type . Además, el uso de parámetros predeterminados en el método Match habilita naturalmente el escenario "X o 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<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); } } } }
fuente
Podría lanzar excepciones una vez que haya un intento de acceder a variables que no se han inicializado, es decir, si se crea con un parámetro A y luego hay un intento de acceder a B o C, podría lanzar, digamos, UnsupportedOperationException. Sin embargo, necesitaría un captador para que funcione.
fuente
El equipo de diseño de lenguaje C # discutió sobre sindicatos discriminados en enero de 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Puede votar por la solicitud de función en https://github.com/dotnet/csharplang/issues/113
fuente
Puede exportar una función de coincidencia de pseudopatrones, como la que uso para el tipo Either en mi biblioteca Sasa . Actualmente hay una sobrecarga de tiempo de ejecución, pero eventualmente planeo agregar un análisis CIL para integrar a todos los delegados en una declaración de caso real.
fuente
No es posible hacerlo exactamente con la sintaxis 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()); }
A estas alturas, 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 { }
No hay comprobaciones para extraer el valor del tipo incorrecto, por ejemplo:
var u = Union(10); string s = u.Value(Get.ForType());
Por lo tanto, podría considerar agregar las verificaciones necesarias y lanzar excepciones en tales casos.
fuente
Yo uso el propio de Union Type.
Considere un ejemplo para aclararlo.
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 solo un conjunto de símbolos? Sé que al menos debería contener @ y es necesario.
Mejoremos nuestro modelo de dominio
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 estas clases 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 Contact puede estar en un estado no válido. Quiero decir, puede tener EmailAddress pero no tiene Name
var contact = new Contact { EmailAddress = new EmailAddress("[email protected]") };
Arreglemoslo 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 Person solo tiene EmailAdress y no PostalAddress?
Si lo pensamos allí, nos damos cuenta de que hay tres posibilidades de estado válido del objeto de la clase Contact:
Escribamos modelos de dominio. Para empezar, crearemos una clase de información de contacto cuyo estado corresponderá con los casos anteriores.
public class ContactInfo { public ContactInfo(EmailAddress emailAddress) { ... } public ContactInfo(PostalAddress postalAddress) { ... } public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... } }
Y clase de contacto:
public class Contact { public Contact( PersonalName personalName, ContactInfo contactInfo ) { ... } }
Intentemos usarlo:
var contact = new Contact( new PersonalName("James", "Bond"), new ContactInfo( new EmailAddress("[email protected]") ) ); Console.WriteLine(contact.PersonalName()); // James Bond Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases
Agreguemos el método Match en la clase 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) { ... } ... } }
Creemos una clase auxiliar, para que cada vez no escriba tanto código.
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"); } }
Reescribamos la
ContactInfo
clase: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) { } }
var contact = new Contact( new PersonalName("James", "Bond"), new ContactInfo( new EmailAddress("[email protected]") ) ); Console.WriteLine(contact.PersonalName()); // James Bond Console .WriteLine( contact .ContactInfo() .Match( (emailAddress) => emailAddress.Address, (postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(), (emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString() ) );
Eso es todo. Espero que lo hayas disfrutado.
Ejemplo tomado del sitio F # por diversión y beneficio
fuente