Chr*_*ell 83 c# type-safety discriminated-union
[注意:这个问题的原始标题是" C#中的C(ish)风格联盟 ",但正如杰夫的评论告诉我的那样,显然这种结构被称为"歧视联盟"]
请原谅这个问题的冗长.
我们已经在SO中提出了几个类似的声音问题,但他们似乎专注于联盟的内存节约优势或将其用于互操作. 这是一个这样的问题的例子.
我希望有一个联合类型的东西有点不同.
我现在正在编写一些代码来生成看起来有点像这样的对象
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Run Code Online (Sandbox Code Playgroud)
很复杂的东西,我想你会同意的.事情是,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);
}
}
Run Code Online (Sandbox Code Playgroud)
使用此类ValueWrapper现在看起来像这样
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
Run Code Online (Sandbox Code Playgroud)
这是我想要实现的东西,但我缺少一个相当重要的元素 - 这是在调用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>();
}
}
Run Code Online (Sandbox Code Playgroud)
IMO无论如何询问ValueA是否有效,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;
}
Run Code Online (Sandbox Code Playgroud)
有没有人知道我想要实现的目标是否可行?或者我只是因为首先写这门课而感到愚蠢?
提前致谢.
Jul*_*iet 102
我真的不喜欢上面提供的类型检查和类型转换的解决方案,所以这里的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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
cdi*_*ins 32
我喜欢接受的解决方案的方向,但是对于超过三个项目的联合,它不能很好地扩展(例如,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));
}
}
Run Code Online (Sandbox Code Playgroud)
Gru*_*oon 19
虽然这是一个老问题,但我最近写了一篇关于这个主题的博客文章可能有用.
假设您有一个购物车场景,其中包含三种状态:"空","活动"和"付费",每种状态都有不同的行为.
ICartState
所有状态都有共同的接口(它可能只是一个空的标记接口)您可以使用C#中的F#运行时,但作为一个更轻量级的替代方案,我编写了一个小T4模板来生成这样的代码.
这是界面:
partial interface ICartState
{
ICartState Transition(
Func<CartStateEmpty, ICartState> cartStateEmpty,
Func<CartStateActive, ICartState> cartStateActive,
Func<CartStatePaid, ICartState> cartStatePaid
);
}
Run Code Online (Sandbox Code Playgroud)
这是实施:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
现在让我们假设您使用未实现的方法扩展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
);
}
Run Code Online (Sandbox Code Playgroud)
请注意,此代码完全是类型安全的 - 在任何地方都没有强制转换或条件,如果您尝试为空购物车付费,则会出现编译错误.
我在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
);
Run Code Online (Sandbox Code Playgroud)
```
我不确定我完全理解你的目标.在C中,union是一个结构,它对多个字段使用相同的内存位置.例如:
typedef union
{
float real;
int scalar;
} floatOrScalar;
Run Code Online (Sandbox Code Playgroud)
该floatOrScalar
联合可以用作浮子,或int,但它们都消耗相同的内存空间.改变一个会改变另一个.您可以使用C#中的结构实现相同的功能:
[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
[FieldOffset(0)]
public float Real;
[FieldOffset(0)]
public int Scalar;
}
Run Code Online (Sandbox Code Playgroud)
上述结构总共使用32位,而不是64位.这只适用于结构.上面的示例是一个类,并且鉴于CLR的性质,不保证内存效率.如果将a Union<A, B, C>
从一种类型更改为另一种类型,则不必重用内存...很可能是,您在堆上分配新类型并在备份object
字段中删除不同的指针.与真正的联合相反,如果你没有使用你的联盟类型,你的方法实际上可能导致更多的堆颠簸.
归档时间: |
|
查看次数: |
29421 次 |
最近记录: |