C# 中的受歧视联合
[注:这个问题的原标题是“C(ish) style union in 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 是否是 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;
}
有谁知道我想要实现的目标是否可能实现?还是我一开始写这门课就很愚蠢?
[Note: This question had the original title "C (ish) style union in C#"
but as Jeff's comment informed me, apparently this structure is called a 'discriminated union']
Excuse the verbosity of this question.
There are a couple of similar sounding questions to mine already in SO but they seem to concentrate on the memory saving benefits of the union or using it for interop.
Here is an example of such a question.
My desire to have a union type thing is somewhat different.
I am writing some code at the moment which generates objects that look a bit like this
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Pretty complicated stuff I think you will agree. The thing is that ValueA
can only be of a few certain types (let's say string
, int
and Foo
(which is a class) and ValueB
can be another small set of types. I don't like treating these values as objects (I want the warm snugly feeling of coding with a bit of type safety).
So I thought about writing a trivial little wrapper class to express the fact that ValueA logically is a reference to a particular type. I called the class Union
because what I am trying to achieve reminded me of the union concept in 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);
}
}
Using this class ValueWrapper now looks like this
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
which is something like what I wanted to achieve but I am missing one fairly crucial element - that is compiler enforced type checking when calling the Is and As functions as the following code demonstrates
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 It is not valid to ask ValueA if it is a char
since its definition clearly says it is not - this is a programming error and I would like the compiler to pick up on this. [Also if I could get this correct then (hopefully) I would get intellisense too - which would be a boon.]
In order to achieve this I would want to tell the compiler that the type T
can be one of A, B or 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;
}
Does anyone have any idea if what I want to achieve is possible? Or am I just plain stupid for writing this class in the first place?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(17)
我不太喜欢上面提供的类型检查和类型转换解决方案,因此这里是 100% 类型安全的联合,如果您尝试使用错误的数据类型,它将引发编译错误:
I don't really like the type-checking and type-casting solutions provided above, so here's 100% type-safe union which will throw compilation errors if you attempt to use the wrong datatype:
我喜欢所接受的解决方案的方向,但它对于超过三个项目的联合不能很好地扩展(例如,9 个项目的联合将需要 9 个类定义)。
这是另一种方法,它在编译时也是 100% 类型安全的,但很容易增长为大型联合。
I like the direction of the accepted solution but it doesn't scale well for unions of more than three items (e.g. a union of 9 items would require 9 class definitions).
Here is another approach that is also 100% type-safe at compile-time, but that is easy to grow to large unions.
我在 https://github.com/mcintyre321/OneOf 编写了一个库来执行此操作
它具有用于执行 DU 的通用类型,例如
OneOf
一直到OneOf
。其中每个都有一个.Match
和一个.Switch
语句,您可以使用它们来实现编译器安全的类型行为,例如:```
```
I have written a library for doing this at https://github.com/mcintyre321/OneOf
It has the generic types in it for doing DUs e.g.
OneOf<T0, T1>
all the way toOneOf<T0, ..., T9>
. Each of those has a.Match
, and a.Switch
statement which you can use for compiler safe typed behaviour, e.g.:```
```
我写了一些关于这个主题的博客文章,可能有用:
假设您有一个具有三种状态的购物车场景:“空”、“活动”和“已付费”,每种状态都有不同的行为。
ICartState
接口(它可能只是一个空标记接口)。您可以使用 C# 中的 F# 运行时,但作为更轻量级的替代方案,我编写了一个小 T4 模板来生成这样的代码。
接口如下:
下面是实现:
现在假设您使用
AddItem
方法扩展CartStateEmpty
和CartStateActive
,该方法不是< /em> 由CartStatePaid
实现。另外,我们还假设
CartStateActive
有一个其他州没有的Pay
方法。然后,这里有一些代码显示了它的使用情况 - 添加两个商品,然后为购物车付款:
请注意,此代码是完全类型安全的 - 任何地方都没有强制转换或条件,如果您尝试为空购物车付款,则会出现编译器错误。
I wrote some blog posts on this subject that might be useful:
Let's say you have a shopping cart scenario with three states: "Empty", "Active" and "Paid", each with different behavior.
ICartState
interface that all states have in common (and it could just be an empty marker interface)You could use the F# runtime from C# but as a lighter weight alternative, I have written a little T4 template for generating code like this.
Here's the interface:
And here's the implementation:
Now let's say you extend the
CartStateEmpty
andCartStateActive
with anAddItem
method which is not implemented byCartStatePaid
.And also let's say that
CartStateActive
has aPay
method that the other states dont have.Then here's some code that shows it in use -- adding two items and then paying for the cart:
Note that this code is completely typesafe -- no casting or conditionals anywhere, and compiler errors if you try to pay for an empty cart, say.
我不确定我完全理解你的目标。在 C 语言中,联合是一种对多个字段使用相同内存位置的结构。例如:
floatOrScalar
联合可以用作 float 或 int,但它们都消耗相同的内存空间。改变一者就会改变另一者。您可以使用 C# 中的结构实现相同的效果:上面的结构总共使用 32 位,而不是 64 位。这只能通过结构体实现。上面的示例是一个类,考虑到 CLR 的性质,不能保证内存效率。如果将
Union
从一种类型更改为另一种类型,则不一定会重用内存...最有可能的是,您正在堆上分配一个新类型并删除一个不同的类型支持object
字段中的指针。与真正的联合相反,您的方法实际上可能会导致比不使用联合类型时更多的堆抖动。I am not sure I fully understand your goal. In C, a union is a structure that uses the same memory locations for more than one field. For example:
The
floatOrScalar
union could be used as a float, or an int, but they both consume the same memory space. Changing one changes the other. You can achieve the same thing with a struct in C#:The above structure uses 32bits total, rather than 64bits. This is only possible with a struct. Your example above is a class, and given the nature of the CLR, makes no guarantee about memory efficiency. If you change a
Union<A, B, C>
from one type to another, you are not necessarily reusing memory...most likely, you are allocating a new type on the heap and dropping a different pointer in the backingobject
field. Contrary to a real union, your approach may actually cause more heap thrashing than you would otherwise get if you did not use your Union type.这会导致警告,而不是错误。如果您正在寻找与 C# 运算符类似的
Is
和As
函数,那么您无论如何都不应该以这种方式限制它们。This results in a warning, not an error. If you're looking for your
Is
andAs
functions to be analogs for the C# operators, then you shouldn't be restricting them in that way anyhow.如果允许多种类型,则无法实现类型安全(除非类型相关)。
您不能也不会实现任何类型的安全,您只能使用 FieldOffset 实现字节值安全。
使用带有
T1 ValueA
和T2 ValueB
的通用ValueWrapper
会更有意义,...PS:当谈论类型安全我指的是编译时类型安全。
如果您需要一个代码包装器(对修改执行业务逻辑,您可以使用以下内容:
对于您可以使用的简单方法(它有性能问题,但非常简单):
If you allow multiple types, you cannot achieve type safety (unless the types are related).
You can't and won't achieve any kind of type safety, you could only achieve byte-value-safety using FieldOffset.
It would make much more sense to have a generic
ValueWrapper<T1, T2>
withT1 ValueA
andT2 ValueB
, ...P.S.: when talking about type-safety I mean compile-time type-safety.
If you need a code wrapper (performing bussiness logic on modifications you can use something along the lines of:
For an easy way out you could use (it has performance issues, but it is very simple):
这是我的尝试。它使用泛型类型约束对类型进行编译时检查。
它可以使用一些美化。特别是,我不知道如何摆脱 As/Is/Set 的类型参数(是否有一种方法可以指定一个类型参数并让 C# 计算另一个类型参数?)
Here is my attempt. It does compile time checking of types, using generic type constraints.
It could use some prettying-up. Especially, I couldn't figure out how to get rid of the type parameters to As/Is/Set (isn't there a way to specify one type parameter and let C# figure the other one?)
所以我多次遇到同样的问题,我只是想出了一个解决方案来获得我想要的语法(以 Union 类型的实现中的一些丑陋为代价。)
回顾一下:我们想要这种用法在呼叫站点。
但是,我们希望以下示例无法编译,以便获得一定程度的类型安全性。
为了额外的好处,我们也不要占用超过绝对需要的空间。
话虽如此,这是我对两个泛型类型参数的实现。三个、四个等类型参数的实现非常简单。
So I've hit this same problem many times, and I just came up with a solution that gets the syntax I want (at the expense of some ugliness in the implementation of the Union type.)
To recap: we want this sort of usage at the call site.
We want the following examples to fail to compile, however, so that we get a modicum of type safety.
For extra credit, let's also not take up more space than absolutely needed.
With all that said, here's my implementation for two generic type parameters. The implementation for three, four, and so on type parameters is straight-forward.
我尝试使用嵌套联合/任一类型来实现最小但可扩展的解决方案。
此外,在 Match 方法中使用默认参数自然会启用“X 或默认”场景。
And my attempt on minimal yet extensible solution using nesting of Union/Either type.
Also usage of default parameters in Match method naturally enables "Either X Or Default" scenario.
一旦尝试访问尚未初始化的变量,即如果它是使用 A 参数创建的,然后尝试访问 B 或 C,则可能会抛出 UnsupportedOperationException。不过,你需要一个吸气剂才能使其工作。
You could throw exceptions once there's an attempt to access variables that haven't been initialized, ie if it's created with an A parameter and later on there's an attempt to access B or C, it could throw, say, UnsupportedOperationException. You'd need a getter to make it work though.
C# 语言设计团队于 2017 年 1 月讨论了受歧视的联合 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-angled -types
您可以在 https://github.com/ 为功能请求投票dotnet/csharplang/issues/113
The C# Language Design Team discussed discriminated unions in January 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
You can vote for the feature request at https://github.com/dotnet/csharplang/issues/113
这是我的解决方案,我相信它涵盖了所有主要用例。它经过完全类型检查,并允许 is、as 和 switch 功能。添加 4+ 可能类型的联合只需要添加另一个子类。
隐式转换允许生产者非常自然地使用:
并且扩展方法允许消费者自然使用:
Here is my solution, which I believe covers all the major use cases. It is fully typed-checked, and allows for is, as, and switch functionality. Adding unions of 4+ possible types would just require adding another subclass.
The implicit conversions allow for very natural usage for producers:
And the extension methods allow for natural usage for consumers:
您可以导出伪模式匹配函数,就像我在 莎莎图书馆。目前存在运行时开销,但我最终计划添加 CIL 分析以将所有委托内联到真实的 case 语句中。
You can export a pseudo-pattern matching function, like I use for the Either type in my Sasa library. There's currently runtime overhead, but I eventually plan to add a CIL analysis to inline all the delegates into a true case statement.
不可能完全使用您所使用的语法,但通过更详细的操作和复制/粘贴,很容易使重载解析为您完成工作:
到目前为止,如何实现它应该非常明显:
没有检查提取错误类型的值,例如:
因此您可能会考虑添加必要的检查并在这种情况下抛出异常。
It's not possible to do with exactly the syntax you've used but with a bit more verbosity and copy/paste it's easy to make overload resolution do the job for you:
By now it should be pretty obvious how to implement it:
There are no checks for extracting the value of the wrong type, e.g.:
So you might consider adding necessary checks and throw exceptions in such cases.
我目前正在尝试在 .NET 中创建 Julia 运行时。 Julia 有像 Union{Int, String}... 等类型。我目前正在尝试模拟这个 .NET(不做奇怪的 IL,无法从 c# 调用)。
这是结构体联合的编译时实现。我将为对象联合、跨对象和结构联合创建更多联合(这将是最复杂的情况)。
您可以像下面这样使用上面的内容:
我还将为交叉对象和结构联合创建动态联合生成器或对齐联合。
看一下:生成的结构体联合输出 查看我正在使用的当前编译时联合。
如果您想创建任何大小的联合,请查看 结构联合生成器
如果有人对上述内容有任何改进,请告诉我!将 julia 实现到 .NET 中是一项极其艰巨的任务!
I am currently trying to create a Julia Runtime in .NET. Julia has types like Union{Int, String}... Etc. I am currently trying to simulate this .NET (without doing weird IL that would not be able to be called from c#).
Here is a compile time implementation of a union of structures. I will be creating more unions for object unions, and cross object and struct unions (this will be the most complex case).
You can use the above like the following:
I will also be creating dynamic union generators or aligned unions for the cross object and struct union.
Take a look at:Generated Struct Union Output to see the current compile time unions I am using.
If you want to create a union of any size take a look at Generator for Struct Unions
If anyone has any improvements for the above let me know! Implementing julia into .NET is an extraordinarily hard task!
我使用自己的联合类型。
考虑一个例子以使其更清楚。
想象一下我们有 Contact 类:
这些都被定义为简单的字符串,但它们真的只是字符串吗?
当然不是。名称可以由名字和姓氏组成。或者电子邮件只是一组符号?我知道至少它应该包含@并且这是必然的。
让我们改进领域模型。
在此类中,将在创建过程中进行验证,我们最终将拥有有效的模型。 PersonaName 类中的构造函数同时需要 FirstName 和 LastName。这意味着创建后,它不能有无效状态。
和联系人类分别
在这种情况下我们有同样的问题,联系人类的对象可能处于无效状态。我的意思是它可能有 EmailAddress 但没有 Name
让我们修复它并使用需要 PersonalName、EmailAddress 和 PostalAddress 的构造函数创建 Contact 类:
但这里我们还有另一个问题。如果 Person 只有 EmailAdress 而没有 PostalAddress 怎么办?
如果我们考虑一下,我们就会意识到 Contact 类对象的有效状态存在三种可能:
让我们写出域模型。首先,我们将创建 Contact Info 类,其状态将与上述情况相对应。
和 Contact 类:
让我们尝试使用它:
让我们在 ContactInfo 类中添加 Match 方法
我们创建一个辅助类,这样每次就不用写那么多代码了。
让我们重写
ContactInfo
类:就这样。
我希望你喜欢。
示例取自网站 F# for fun andprofit
I use own of Union Type.
Consider an example to make it clearer.
Imagine we have Contact class:
These are all defined as simple strings, but really are they just strings?
Of course not. The Name can consist of First Name and Last Name. Or is an Email just a set of symbols? I know that at least it should contain @ and it is necessarily.
Let's improve us domain model
In this classes will be validations during creating and we will eventually have valid models. Consturctor in PersonaName class require FirstName and LastName at the same time. This means that after the creation, it can not have invalid state.
And contact class respectively
In this case we have same problem, object of Contact class may be in invalid state. I mean it may have EmailAddress but haven't Name
Let's fix it and create Contact class with constructor which requires PersonalName, EmailAddress and PostalAddress:
But here we have another problem. What if Person have only EmailAdress and haven't PostalAddress?
If we think about it there we realize that there are three possibilities of valid state of Contact class object:
Let's write out domain models. For the beginning we will create Contact Info class which state will be corresponding with above cases.
And Contact class:
Let's try use it:
Let's add Match method in ContactInfo class
Let's create an auxiliary class, so that each time do not write as many code.
Let's rewrite
ContactInfo
class:That's all.
I hope you enjoyed.
Example taken from the site F# for fun and profit