[Примечание: этот вопрос имел первоначальное название « Объединение в стиле C (ish) в C # », но, как мне сообщил комментарий Джеффа, очевидно, эта структура называется «размеченным объединением»]
Извините за многословность этого вопроса.
Есть несколько похожих вопросов, которые я могу задать в SO, но они, похоже, сосредоточены на преимуществах объединения памяти или использовании его для взаимодействия. Вот пример такого вопроса .
Мое желание иметь что-то типа профсоюзов несколько иное.
В данный момент я пишу код, который генерирует объекты, похожие на этот
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Довольно сложный материал, я думаю, вы согласитесь. Дело в том, что ValueA
может быть только несколько определенных типов (скажем string
, int
и Foo
(который является классом), а ValueB
может быть еще один небольшой набор типов. Мне не нравится рассматривать эти значения как объекты (я хочу теплого, уютного ощущения кодирование с некоторой степенью безопасности типов).
Итак, я подумал о написании тривиального небольшого класса-оболочки, чтобы выразить тот факт, что ValueA логически является ссылкой на определенный тип. Я позвонил в класс, Union
потому что то, что я пытаюсь достичь, напомнило мне концепцию объединения в 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);
}
}
Использование этого класса ValueWrapper теперь выглядит так
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
чего-то вроде того, чего я хотел достичь, но мне не хватает одного довольно важного элемента - это принудительная проверка типа компилятором при вызове функций Is и As, как показано в следующем коде
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 Недопустимо спрашивать ValueA, является ли это a, char
поскольку в его определении ясно сказано, что это не так - это ошибка программирования, и я хотел бы, чтобы компилятор уловил это. [Также, если бы я мог сделать это правильно, то (надеюсь) я бы тоже получил intellisense - что было бы благом.]
Чтобы добиться этого, я хотел бы сказать компилятору, что тип T
может быть одним из A, B или 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;
}
Кто-нибудь знает, возможно ли то, чего я хочу достичь? Или я просто глуп, что вообще написал этот класс?
Заранее спасибо.
источник
StructLayout(LayoutKind.Explicit)
иFieldOffset
. Конечно, этого нельзя сделать со ссылочными типами. То, что вы делаете, совсем не похоже на C Union.Ответы:
Мне не очень нравятся представленные выше решения для проверки типов и приведения типов, поэтому вот 100% типобезопасное объединение, которое вызовет ошибки компиляции, если вы попытаетесь использовать неправильный тип данных:
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); } } } }
источник
match
, и это такой же хороший способ получить его, как и любой другой.type Result = Success of int | Error of int
Мне нравится направление принятого решения, но оно плохо масштабируется для объединений более трех элементов (например, объединение 9 элементов потребует 9 определений классов).
Вот еще один подход, который также на 100% безопасен для типов во время компиляции, но его легко развить до больших объединений.
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)); } }
источник
dynamic
& generics вUnionBase<A>
цепочке наследования кажется ненужным. СделатьUnionBase<A>
необщего, убить конструктор принимаяA
и сделать (что это в любом случае, нет никаких дополнительное преимущество в объявлении его ). Затем создайте каждый класс непосредственно из . Это имеет то преимущество, что будет показан только правильный метод. (Как и сейчас, например, выявляет перегрузку, которая гарантированно вызовет исключение, если заключенное значение не является . Этого не должно происходить.)value
object
dynamic
Union<…>
UnionBase
Match<T>(…)
Union<A, B>
Match<T>(Func<A, T> fa)
A
Я написал несколько сообщений в блоге на эту тему, которые могут быть полезны:
Скажем , у вас есть корзина сценарий с тремя состояниями: «Пустой», «Активный» и «Paid», каждый с разным поведением.
ICartState
интерфейс, который является общим для всех состояний (и это может быть просто пустой интерфейс маркера)Вы можете использовать среду выполнения F # из C #, но в качестве более легкой альтернативы я написал небольшой шаблон T4 для генерации подобного кода.
Вот интерфейс:
partial interface ICartState { ICartState Transition( Func<CartStateEmpty, ICartState> cartStateEmpty, Func<CartStateActive, ICartState> cartStateActive, Func<CartStatePaid, ICartState> cartStatePaid ); }
А вот и реализация:
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); } }
Теперь предположим, что вы расширили
CartStateEmpty
иCartStateActive
с помощьюAddItem
метода, который не реализуетсяCartStatePaid
.А также предположим, что у
CartStateActive
него естьPay
метод, которого нет в других штатах.Тогда вот код, который показывает его использование - добавление двух предметов и оплата корзины:
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 ); }
Обратите внимание, что этот код полностью типизирован - нигде нет приведений или условных выражений, а также ошибок компилятора, если вы попытаетесь заплатить, скажем, за пустую корзину.
источник
Я написал библиотеку для этого на https://github.com/mcintyre321/OneOf
В нем есть общие типы для выполнения DU, например,
OneOf<T0, T1>
полностьюOneOf<T0, ..., T9>
. У каждого из них есть оператор.Match
и.Switch
оператор, который можно использовать для безопасного типизированного поведения компилятора, например:``
OneOf<string, ColorName, Color> backgroundColor = getBackground(); Color c = backgroundColor.Match( str => CssHelper.GetColorFromString(str), name => new Color(name), col => col );
``
источник
Я не уверен, что полностью понимаю вашу цель. В C объединение - это структура, которая использует одни и те же ячейки памяти для более чем одного поля. Например:
typedef union { float real; int scalar; } floatOrScalar;
floatOrScalar
Союз может быть использован как поплавок, или Int, но оба они потребляют один и тот же объем памяти. Изменение одного меняет другое. Вы можете добиться того же с помощью структуры на C #:[StructLayout(LayoutKind.Explicit)] struct FloatOrScalar { [FieldOffset(0)] public float Real; [FieldOffset(0)] public int Scalar; }
В приведенной выше структуре используется всего 32 бита, а не 64 бита. Это возможно только со структурой. Приведенный выше пример является классом и, учитывая характер среды CLR, не дает никаких гарантий относительно эффективности памяти. Если вы меняете a
Union<A, B, C>
с одного типа на другой, вы не обязательно повторно используете память ... скорее всего, вы выделяете новый тип в куче и отбрасываете другой указатель вobject
поле поддержки . В отличие от настоящего объединения , ваш подход может фактически вызвать большее количество разметки кучи, чем если бы вы не использовали свой тип Union.источник
char foo = 'B'; bool bar = foo is int;
Это приводит к предупреждению, а не к ошибке. Если вы ищете для ваших
Is
иAs
функций , которые будут аналогов для C # операторов, то вы не должны ограничивать их таким образом в любом случае.источник
Если вы разрешаете несколько типов, вы не можете обеспечить безопасность типов (если типы не связаны).
Вы не можете и не сможете добиться какой-либо безопасности типов, вы можете достичь безопасности байтовых значений только с помощью FieldOffset.
Было бы гораздо разумнее иметь общий
ValueWrapper<T1, T2>
сT1 ValueA
иT2 ValueB
, ...PS: когда говорят о безопасности типов, я имею в виду безопасность типов во время компиляции.
Если вам нужна оболочка кода (выполняющая бизнес-логику для модификаций, вы можете использовать что-то вроде:
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); } }
Для простого выхода вы можете использовать (у него проблемы с производительностью, но это очень просто):
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
источник
Вот моя попытка. Он выполняет проверку типов во время компиляции с использованием общих ограничений типа.
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>() } }
Это может потребовать некоторого прихорашивания. В частности, я не мог понять, как избавиться от параметров типа As / Is / Set (разве нет способа указать один параметр типа и позволить C # определять другой?)
источник
Итак, я сталкивался с этой же проблемой много раз, и я просто придумал решение, которое получает желаемый синтаксис (за счет некоторого уродства в реализации типа Union).
Подведем итоги: мы хотим, чтобы на месте вызова было такое использование.
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";
Однако мы хотим, чтобы следующие примеры не компилировались, чтобы получить хоть какую-то безопасность типов.
В качестве дополнительного вознаграждения давайте также не будем занимать больше места, чем абсолютно необходимо.
С учетом всего сказанного, вот моя реализация для двух параметров универсального типа. Реализация трех, четырех и т.д. параметров типа проста.
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(); } } }
источник
И моя попытка минимального, но расширяемого решения с использованием вложенности типа Union / Either . Кроме того, использование параметров по умолчанию в методе Match, естественно, включает сценарий «Либо X, либо по умолчанию».
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); } } } }
источник
Вы можете генерировать исключения при попытке доступа к переменным, которые не были инициализированы, т.е. если он создан с параметром A, а позже есть попытка доступа к B или C, это может вызвать, скажем, UnsupportedOperationException. Однако вам понадобится геттер, чтобы он работал.
источник
Команда разработчиков языка C # обсудила дискриминационные союзы в январе 2017 г. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Вы можете проголосовать за запрос функции на https://github.com/dotnet/csharplang/issues/113.
источник
Вы можете экспортировать функцию сопоставления псевдошаблонов, как я использую для типа Either в моей библиотеке Sasa . В настоящее время существуют накладные расходы времени выполнения, но в конечном итоге я планирую добавить анализ CIL, чтобы встроить все делегаты в истинный оператор case.
источник
Невозможно использовать именно тот синтаксис, который вы использовали, но с немного большей многословностью и копированием / вставкой легко заставить разрешение перегрузки сделать эту работу за вас:
// 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()); }
К настоящему времени должно быть довольно очевидно, как это реализовать:
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 { }
Нет никаких проверок на извлечение значения неправильного типа, например:
var u = Union(10); string s = u.Value(Get.ForType());
Таким образом, вы можете подумать о добавлении необходимых проверок и исключении в таких случаях.
источник
Я использую свой Union Type.
Рассмотрим пример, чтобы было понятнее.
Представьте, что у нас есть класс Contact:
public class Contact { public string Name { get; set; } public string EmailAddress { get; set; } public string PostalAdrress { get; set; } }
Все они определены как простые строки, но действительно ли это просто строки? Конечно, нет. Имя может состоять из имени и фамилии. Или электронное письмо - это просто набор символов? Я знаю, что по крайней мере он должен содержать @ и это обязательно.
Давайте улучшим модель предметной области
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) { ... } }
В этих классах будут проверки во время создания, и в конечном итоге у нас будут действующие модели. Конструктору в классе PersonaName требуется одновременно FirstName и LastName. Это означает, что после создания он не может иметь недопустимое состояние.
И контактный класс соответственно
public class Contact { public PersonalName Name { get; set; } public EmailAdress EmailAddress { get; set; } public PostalAddress PostalAddress { get; set; } }
В этом случае у нас такая же проблема, объект класса Contact может находиться в недопустимом состоянии. Я имею в виду, что у него может быть адрес электронной почты, но не имя
var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };
Давайте исправим это и создадим класс Contact с конструктором, который требует PersonalName, EmailAddress и PostalAddress:
public class Contact { public Contact( PersonalName personalName, EmailAddress emailAddress, PostalAddress postalAddress ) { ... } }
Но здесь возникает другая проблема. Что делать, если у человека есть только адрес электронной почты, а не почтовый адрес?
Если мы подумаем об этом, то поймем, что есть три возможности действительного состояния объекта класса Contact:
Выпишем модели предметной области. Для начала создадим класс Contact Info, состояние которого будет соответствовать вышеуказанным случаям.
public class ContactInfo { public ContactInfo(EmailAddress emailAddress) { ... } public ContactInfo(PostalAddress postalAddress) { ... } public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... } }
И контактный класс:
public class Contact { public Contact( PersonalName personalName, ContactInfo contactInfo ) { ... } }
Попробуем использовать:
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
Добавим метод Match в класс 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) { ... } ... } }
Создадим вспомогательный класс, чтобы каждый раз не писать столько кода.
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"); } }
Перепишем
ContactInfo
класс: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("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() ) );
Вот и все. Надеюсь, тебе понравилось.
Пример взят с сайта F # для развлечения и наживы
источник