C#中的可区分联合
Posted
技术标签:
【中文标题】C#中的可区分联合【英文标题】:Discriminated union in C# 【发布时间】:2010-06-30 17:18:03 【问题描述】:[注意:这个问题的原标题是“C#中的C (ish) style union” 但正如 Jeff 的评论告诉我的那样,显然这种结构被称为“有区别的联合”]
请原谅这个问题的冗长。
在 SO 中已经有几个类似的问题需要挖掘,但它们似乎专注于联合的内存节省优势或将其用于互操作。 Here is an example of such a question.
我想要一个联合类型的东西有点不同。
我现在正在编写一些代码来生成看起来有点像这样的对象
public class ValueWrapper
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
相当复杂的东西我想你会同意的。问题是ValueA
只能是几种特定类型(比如说string
、int
和Foo
(这是一个类)和ValueB
可以是另一小组类型。我不知道t 喜欢将这些值视为对象(我想要带有一点类型安全的编码的温暖舒适感)。
所以我考虑编写一个简单的小包装类来表达 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 Aget return a;
public B Bget return b;
public C Cget 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 是否为char
是无效的,因为它的定义清楚地表明它不是 - 这是一个编程错误,我希望编译器能够解决这个问题。 [另外,如果我能做到这一点,那么(希望)我也会得到智能感知——这将是一个福音。]
为了实现这一点,我想告诉编译器 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;
有没有人知道我想要实现的目标是否可行?还是我一开始就写这门课是愚蠢的?
提前致谢。
【问题讨论】:
C 中的联合可以使用StructLayout(LayoutKind.Explicit)
和 FieldOffset
在 C# 中为值类型实现。当然,这不能用引用类型来完成。你所做的根本不像一个 C Union。
这通常被称为有区别的联合。
谢谢 Jeff - 我不知道这个词,但这正是我想要实现的目标
可能不是您正在寻找的那种响应,但您是否考虑过 F#?它具有类型安全的联合和模式匹配语言,比使用 C# 更容易表示联合。
可区分联合的另一个名称是 sum 类型。
【参考方案1】:
我不太喜欢上面提供的类型检查和类型转换解决方案,所以这里有一个 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
,这是获得它的好方法。
如果所有的样板代码让你失望,你可以尝试这个显式标记案例的实现:pastebin.com/EEdvVh2R。顺便说一句,这种风格与 F# 和 OCaml 在内部表示联合的方式非常相似。
我喜欢 Juliet 的短代码,但是如果类型是 type Result = Success of int | Error of int
【参考方案2】:
我喜欢接受的解决方案的方向,但它不适用于三个以上项目的联合(例如,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));
【讨论】:
+1 这应该得到更多的认可;我喜欢你让它足够灵活以允许各种类型的联合的方式。 +1 为您的解决方案提供灵活性和简洁性。不过,有一些细节让我很困扰。我会将每一个作为单独的评论发布: 1. 在某些情况下,使用反射可能会导致太大的性能损失,因为有区别的联合,由于其基本性质,可能会经常使用。 2. 在UnionBase<A>
和继承链中使用dynamic
和泛型似乎是不必要的。将UnionBase<A>
设为非泛型,终止构造函数以获取A
,并将value
设为object
(无论如何都是这样;声明它dynamic
并没有额外的好处)。然后直接从UnionBase
派生每个Union<…>
类。这样做的好处是只有正确的Match<T>(…)
方法会被公开。 (就像现在一样,例如Union<A, B>
暴露了一个重载Match<T>(Func<A, T> fa)
,如果封闭的值不是A
,则保证抛出异常。这不应该发生。)
你可能会发现我的图书馆 OneOf 很有用,它或多或少是这样做的,但在 Nuget 上:) github.com/mcintyre321/OneOf【参考方案3】:
我写了一些关于这个主题的博文,可能有用:
Union Types in C# Implementing Tic-Tac-Toe Using State Classes假设您有一个具有三种状态的购物车场景:“空”、“活动”和“已付款”,每种状态都有不同的行为。
您创建了一个所有状态共有的ICartState
接口(它可能只是一个空标记接口)
您创建了三个实现该接口的类。 (这些类不必处于继承关系中)
该接口包含一个“折叠”方法,您可以通过该方法为您需要处理的每个状态或案例传递一个 lambda。
您可以使用 C# 中的 F# 运行时,但作为更轻量级的替代方案,我编写了一个小 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);
现在假设您使用AddItem
方法扩展CartStateEmpty
和CartStateActive
,该方法不由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
);
请注意,此代码是完全类型安全的 - 任何地方都没有强制转换或条件,如果您尝试为空购物车付费,编译器会出错。
【讨论】:
有趣的用例。对我来说,在对象本身上实现有区别的联合变得非常冗长。这是一个基于您的模型使用开关表达式的函数式替代方案:gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866。您可以看到,如果只有一个“快乐”路径,则 DU 并不是真正必要的,但当方法可能返回一种或另一种类型时,它们会变得非常有用,具体取决于业务逻辑规则。【参考方案4】:我在https://github.com/mcintyre321/OneOf写了一个库来做这件事
Install-Package 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
);
```
【讨论】:
【参考方案5】:我不确定我是否完全理解您的目标。在 C 中,联合是一种结构,它为多个字段使用相同的内存位置。例如:
typedef union
float real;
int scalar;
floatOrScalar;
floatOrScalar
联合可以用作浮点数或整数,但它们都占用相同的内存空间。改变一个改变另一个。您可以使用 C# 中的结构来实现相同的目的:
[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
[FieldOffset(0)]
public float Real;
[FieldOffset(0)]
public int Scalar;
上述结构总共使用 32 位,而不是 64 位。这仅适用于结构。您上面的示例是一个类,鉴于 CLR 的性质,不能保证内存效率。如果您将Union<A, B, C>
从一种类型更改为另一种类型,则不一定会重用内存……很可能是在堆上分配新类型并在支持object
字段中放置不同的指针。与真正的联合相反,如果你不使用联合类型,你的方法实际上可能会导致更多的堆抖动。
【讨论】:
正如我在问题中提到的,我的动机并不是提高内存效率。我已更改问题标题以更好地反映我的目标 - “C(ish) union”的原始标题事后具有误导性 一个有区别的联合让你想要做的事情更有意义。至于让它在编译时检查...我会研究 .NET 4 和代码合同。使用代码合同,可以强制执行编译时 Contract.Requires 来强制执行您对 .Ischar foo = 'B';
bool bar = foo is int;
这会导致警告,而不是错误。如果您正在寻找与 C# 运算符类似的 Is
和 As
函数,那么无论如何您都不应该以这种方式限制它们。
【讨论】:
【参考方案7】:如果允许多种类型,则无法实现类型安全(除非类型相关)。
您不能也不会实现任何类型的类型安全,您只能使用 FieldOffset 实现字节值安全。
拥有一个通用的ValueWrapper<T1, T2>
和T1 ValueA
和T2 ValueB
会更有意义,...
P.S.:当谈到类型安全时,我指的是编译时类型安全。
如果您需要一个代码包装器(对修改执行业务逻辑,您可以使用以下内容:
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
【讨论】:
您关于使 ValueWrapper 通用的建议似乎是显而易见的答案,但它给我的工作带来了问题。本质上,我的代码是通过解析一些文本行来创建这些包装对象。所以我有一个像 ValueWrapper MakeValueWrapper(string text) 这样的方法。如果我使包装器通用,那么我需要将 MakeValueWrapper 的签名更改为通用,然后这反过来意味着调用代码需要知道预期的类型,而我只是在解析文本之前事先不知道这一点... ...但即使在我写最后一条评论时,我也感觉我可能错过了一些东西(或搞砸了一些东西),因为我想做的事情并不像它应该做的那样困难,因为我正在做。我想我会回去花几分钟研究一个通用的包装器,看看我是否可以围绕它调整解析代码。 我提供的代码应该只是用于业务逻辑。您的方法的问题是您永远不知道在编译时将什么值存储在 Union 中。这意味着您在访问 Union 对象时必须使用 if 或 switch 语句,因为这些对象不共享通用功能!您将如何在代码中进一步使用包装器对象?您还可以在运行时构造通用对象(缓慢,但可能)。另一个简单的选择是在我编辑的帖子中。 您现在的代码中基本上没有有意义的编译时类型检查 - 您也可以尝试动态对象(运行时动态类型检查)。【参考方案8】:这是我的尝试。它使用泛型类型约束对类型进行编译时检查。
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# 计算另一个?)
【讨论】:
【参考方案9】:所以我多次遇到同样的问题,我只是想出了一个获得我想要的语法的解决方案(以在 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";
但是,我们希望以下示例无法编译,以便获得一点类型安全性。
DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;
为了额外的功劳,我们也不要占用比绝对需要更多的空间。
说了这么多,这是我对两个泛型类型参数的实现。三个、四个等类型参数的实现很简单。
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();
【讨论】:
【参考方案10】:我尝试使用 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);
【讨论】:
【参考方案11】:一旦尝试访问尚未初始化的变量,您可能会抛出异常,即如果它是使用 A 参数创建的,然后尝试访问 B 或 C,它可能会抛出 UnsupportedOperationException。不过,您需要一个吸气剂才能使其工作。
【讨论】:
是的——我写的第一个版本确实在 As 方法中引发了异常——虽然这肯定突出了代码中的问题,但我更喜欢在编译时而不是在运行时被告知这一点。 【参考方案12】:C# 语言设计团队在 2017 年 1 月讨论了可区分联合 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为功能请求投票
【讨论】:
【参考方案13】:您可以导出伪模式匹配函数,就像我在 Sasa library 中使用的 Either 类型一样。目前存在运行时开销,但我最终计划添加 CIL 分析以将所有委托内联到真实的案例语句中。
【讨论】:
【参考方案14】:不可能完全按照您使用的语法来做,但是稍微冗长和复制/粘贴很容易让重载解决方案为您完成这项工作:
// 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());
因此您可能会考虑添加必要的检查并在这种情况下抛出异常。
【讨论】:
【参考方案15】:我使用自己的联合类型。
考虑一个例子以使其更清楚。
假设我们有 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 类的对象可能处于无效状态。我的意思是它可能有 EmailAddress 但没有 Name
var contact = new Contact EmailAddress = new EmailAddress("foo@bar.com") ;
让我们修复它并使用需要 PersonalName、EmailAddress 和 PostalAddress 的构造函数创建 Contact 类:
public class Contact
public Contact(
PersonalName personalName,
EmailAddress emailAddress,
PostalAddress postalAddress
)
...
但是这里我们还有另一个问题。如果 Person 只有 EmailAdress 而没有 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
让我们在 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)
...
...
在match方法中,我们可以写这段代码,因为contact类的状态是由构造函数控制的,它可能只有一种可能的状态。
让我们创建一个辅助类,这样每次就不用写那么多代码了。
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");
我们可以为几种类型预先设置这样的类,就像委托 Func、Action 一样。 4-6个泛型类型参数将全部用于Union类。
让我们重写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)
这里编译器将要求至少一个构造函数覆盖。如果我们忘记覆盖其余的构造函数,我们将无法创建具有其他状态的 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()
.Match(
(emailAddress) => emailAddress.Address,
(postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
(emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
)
);
就是这样。 希望你喜欢。
示例取自网站F# for fun and profit
【讨论】:
以上是关于C#中的可区分联合的主要内容,如果未能解决你的问题,请参考以下文章