Unión discriminada en C#
type-safety discriminated-union (14)
Aquí está mi intento. Compila el control del tiempo de los 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>()
}
}
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?)
[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 codificación con un poco de seguridad tipo).
Así que pensé en escribir una pequeña clase de contenedor para expresar 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<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 tipo aplicada 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>();
}
}
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 programación y me gustaría que el compilador 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 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 soy simplemente estúpido por escribir esta clase en primer lugar?
Gracias por adelantado.
Así que me he topado con el mismo problema muchas veces, y acabo de presentar una solución que obtiene la sintaxis 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<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, no queremos compilar 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 genéricos de tipo. La implementación de tres, cuatro, etc. parámetros de tipo es directa.
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();
}
}
}
Aunque esta es una vieja pregunta, recientemente escribí algunas publicaciones en el blog sobre este tema que podrían ser útiles.
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, 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 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 errores del compilador si intenta pagar un carrito vacío, por ejemplo.
He escrito una biblioteca para hacer esto en github.com/mcintyre321/OneOf
Install-Package OneOf
Tiene los tipos genéricos para hacer DU, por ejemplo, OneOf<T0, T1>
hasta OneOf<T0, ..., T9>
. Cada uno de ellos tiene una .Match
y una declaración .Switch
que puede usar para el comportamiento de .Switch
segura del compilador, por ejemplo:
`` `
OneOf<string, ColorName, Color> backgroundColor = getBackground();
Color c = backgroundColor.Match(
str => CssHelper.GetColorFromString(str),
name => new Color(name),
col => col
);
`` `
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 compilación, pero que es fácil de hacer crecer a grandes sindicatos.
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));
}
}
No es posible hacer 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());
}
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
{
}
No hay controles para extraer el valor del tipo incorrecto, por ejemplo:
var u = Union(10);
string s = u.Value(Get.ForType());
Entonces, podría considerar agregar cheques necesarios y lanzar excepciones en tales casos.
No estoy seguro de entender completamente tu 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 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<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 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.
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 delegados en una declaración de caso verdadero.
Puede lanzar excepciones una vez que haya un intento 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 intento de acceder a B o C, podría arrojar, por ejemplo, UnsupportedOperationException. Sin embargo, necesitarías un getter para que funcione.
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 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);
}
}
}
}
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<T1, T2>
genérico ValueWrapper<T1, T2>
con T1 ValueA
y T2 ValueB
, ...
PD: cuando hablo de seguridad de tipo me refiero a seguridad de tipo en tiempo de compilación.
Si necesita un contenedor de código (realizando lógica de negocios en 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 podría 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
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("[email protected]") };
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:
- Un contacto solo tiene una dirección de correo electrónico
- Un contacto solo tiene una dirección postal
- 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<EmailAddress,PostalAddress> 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("[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 de coincidencia 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)
{
...
}
...
}
}
In the match method, we can write this code, because the state of the contact class is controlled with constructors and it may have only one of the possible states.
Let''s create an auxiliary class, so that each time do not write as many 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");
}
}
We can have such a class in advance for several types, as is done with delegates Func, Action. 4-6 generic type parameters will be in full for Union class.
Let''s rewrite ContactInfo
class:
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) { }
}
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("[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. I hope you enjoyed.
Example taken from the site F# for fun and profit
Y mi intento 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<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);
}
}
}
}
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.