在C#中辨别联盟

[注意:这个问题的原始标题是“ C(ish)风格的联合在C# ”,但杰夫的评论告诉我,显然这种结构被称为“歧视联盟”]

请原谅这个问题的冗长。

在我们已经有了一些类似的问题,但他们似乎集中在工会的记忆储蓄的好处或使用它的互操作性。 这是一个这样的问题的例子 。

我希望有一个工会types的东西有些不同。

我正在编写一些代码,生成看起来有点像这样的对象

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

相当复杂的东西,我认为你会同意。 问题是, ValueA只能是一些特定的types(比如stringintFoo (这是一个类),而ValueB可以是另一个小的types集合),我不喜欢把这些值当作对象(我想编码的温暖舒适的感觉与一些types的安全)。

所以我想写一个简单的小包装类来expression一个事实,即ValueA在逻辑上是对特定types的引用。 我打电话给class级Union因为我想要实现的是联盟概念。

 public class Union<A, B, C> { 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; } /// <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; } 

这是我想达到的东西,但我缺less一个相当重要的元素 – 这是编译器强制执行types检查调用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它是否是一个char是无效的,因为它的定义清楚地表明它不是 – 这是一个编程错误,我希望编译器能够拿起来。 [也是如果我能得到这个正确的(希望),我也会得到intellisense – 这将是一个福音。]

为了实现这一点,我想告诉编译器,typesT可以是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; } 

有没有人有任何想法,如果我想实现是可能的? 还是我刚刚写这个课程愚蠢起来呢?

提前致谢。

我不太喜欢上面提供的types检查和types转换解决scheme,所以这里是100%的types安全的联合,如果你尝试使用错误的数据types,会导致编译错误:

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

我喜欢接受解决scheme的方向,但是对于三个以上项目的工会来说,这并不好(例如,9个项目的联合需要9个阶级定义)。

这是另一种在编译时也是100%types安全的方法,但是很容易发展到大型工会。

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

尽pipe这是一个老问题,但我最近写了一些关于这个主题的博客post,可能是有用的。

  • C#中的联合types
  • 使用状态类实现井字游戏

假设您有三种状态的购物车场景:“空”,“活动”和“付费”,每种状态都有不同的行为。

  • 你创build一个ICartState接口,所有的状态都有共同点(它可能只是一个空的标记接口)
  • 你创build了三个实现这个接口的类。 (这些类不一定是inheritance关系)
  • 该接口包含一个“折叠”方法,从而为每个需要处理的状态或情况传递一个lambda。

你可以使用C#中的F#运行时,但作为一个轻量级的select,我已经写了一个小的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); } } 

现在让我们说扩展CartStateEmptyCartStateActive与一个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 ); } 

请注意,这个代码是完全types安全的 – 没有任何地方的铸造或条件,以及编译器错误,如果你试图支付一个空的购物车,说。

我不确定我完全理解你的目标。 在C中,联合是一个使用相同的内存位置为多个字段的结构。 例如:

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

floatOrScalar联合可以用作float或int,但是它们都占用相同的内存空间。 改变一个改变另一个。 你可以用C#中的结构来实现同样的事情:

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

上述结构总共使用32位,而不是64位。 这是唯一可能的结构。 上面的例子是一个类,并且考虑到CLR的本质,不能保证内存效率。 如果将Union<A, B, C>从一个types更改为另一个types,则不一定会重用内存……最有可能的是,您正在堆中分配一个新types,并在后备object字段中放入不同的指针。 与真正的联盟相反,如果不使用Uniontypes,您的方法实际上可能会导致更多的堆抖动。

我在https://github.com/mcintyre321/OneOf上写了一个这样的库;

安装程序包OneOf

它具有用于执行OneOf<T0, T1>的通用types,例如OneOf<T0, T1>一直到OneOf<T0, ..., T9> 。 每个人都有一个.Match和一个.Switch语句,你可以用它来编译安全types的行为,例如:

“`

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

“`

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

这会导致警告,而不是错误。 如果你正在寻找你的IsAs函数来模拟C#操作符,那么你不应该以任何方式限制它们。

如果允许多种types,则不能实现types安全(除非types相关)。

您不能也不会实现任何types的安全性,您只能使用FieldOffset实现字节值安全性。

使用T1 ValueAT2 ValueB的genericsValueWrapper<T1, T2>会更有意义…

PS:在谈到types安全时,我的意思是编译时types安全。

如果你需要一个代码包装器(对修改进行商业逻辑,你可以使用以下几行:

 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 

所以我也遇到了同样的问题,我只是想出了一个解决scheme来获得我想要的语法(牺牲了联盟types的一些丑陋)。

回顾一下:我们希望在呼叫站点使用这种用法。

 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"; 

但是,我们希望下面的例子不能编译,所以我们得到了一个types安全的小小的东西。

 DateTime dateTimeColumbusDiscoveredAmerica = u; Foo fooInstance = u; 

为了额外的功劳,我们也不要占用比绝对需要更多的空间。

综上所述,这里是我的两个genericstypes参数的实现。 三,四等types参数的实现是直接的。

 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 / Eithertypes的最小但可扩展的解决scheme。 Match方法中默认参数的使用自然会启用“X或默认”scheme。

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

一旦尝试访问尚未初始化的variables,即可以使用A参数创buildvariables,然后尝试访问B或C,则可以抛出UnsupportedOperationExceptionexception。 你需要一个getter来使它工作。

这是我的尝试。 它使用genericstypes约束来编译时间检查types。

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

它可以使用一些漂亮的东西。 特别是,我不知道如何摆脱types参数为As / Is / Set(是不是有一种方法来指定一个types参数,让C#的数字另一个?)

你可以导出一个伪模式匹配函数,就像我在Sasa库中使用的那样 。 目前有运行时间的开销,但是我最终打算添加一个CIL分析来将所有代表内联到一个真实的case语句中。

用你使用过的语法是不可能的,但有一点冗长,复制/粘贴它很容易使重载解决scheme做你的工作:

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

现在应该是非常明显的如何实现它:

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

没有检查提取错误types的值,例如:

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

所以你可能会考虑在这种情况下添加必要的检查和抛出exception。

我使用自己的联盟​​types。

考虑一个例子,使其更清晰。

想象一下,我们有联系人类:

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

这些都被定义为简单的string,但他们真的只是string? 当然不是。 名称可以由名字和姓氏组成。 或者是一个电子邮件只是一组符号? 我知道,至less它应该包含@,这是必然的。

让我们来改进我们的领域模型

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

在这个类将在创build过程中validation,我们最终将有有效的模型。 PersonaName类中的构造函数同时需要FirstName和LastName。 这意味着在创build之后,它不能有无效的状态。

和联系人分别

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

在这种情况下,我们也有同样的问题,Contact类的对象可能处于无效状态。 我的意思是它可能有EmailAddress,但没有名称

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

让我们来修复它,并创build联系类的构造,它需要PersonalName,EmailAddress和PostalAddress:

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

但在这里我们还有另一个问题。 如果Person只有EmailAdress并且没有PostalAddress?

如果我们想一想,那么我们意识到Contact类对象的有效状态有三种可能性:

  1. 联系人只有一个电子邮件地址
  2. 联系人只有一个邮政地址
  3. 联系人既有电子邮件地址,也有邮寄地址

我们写出域模型。 一开始我们将创build联系人信息类,其状态将与以上情况相对应。

 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 

让我们在ContactInfo类中添加Match方法

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

在匹配方法中,我们可以编写这段代码,因为接触类的状态是由构造函数控制的,它可能只有一个可能的状态。

我们来创build一个辅助类,这样每次都不会写出尽可能多的代码。

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

我们可以预先为几种types提供这样的课程,就像代表Func,Action一样。 Union类的4-6个genericstypes参数将被全部填满。

我们来重写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) { } } 

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

就这样。 I hope you enjoyed.

Example taken from the site F# for fun and profit