简体   繁体   English

C#中的可区分联合

[英]Discriminated union in C#

[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'] [注意:这个问题的原标题是“C# 中的C (ish) 风格联合”,但正如 Jeff 的评论告诉我的那样,显然这种结构被称为“有区别的联合”]

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.在 SO 中已经有几个类似的问题需要挖掘,但他们似乎专注于 memory 节省工会的好处或将其用于互操作。 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).问题是ValueA只能是几种特定类型(比如说stringintFoo (这是一个类),而ValueB可以是另一小组类型。我不喜欢将这些值视为对象(我想要带有一点类型安全性的编码的温暖舒适感)。

So I thought about writing a trivial little wrapper class to express the fact that ValueA logically is a reference to a particular type.所以我想写一个简单的小包装器 class 来表达 ValueA 在逻辑上是对特定类型的引用这一事实。 I called the class Union because what I am trying to achieve reminded me of the union concept in C.我调用了 class 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);
    }
}

Using this class ValueWrapper now looks like this使用这个 class ValueWrapper 现在看起来像这样

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这与我想要实现的目标相似,但我缺少一个相当关键的元素 - 即在调用 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 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. IMO 询问 ValueA 是否是char是无效的,因为它的定义清楚地表明它不是 - 这是一个编程错误,我希望编译器能够解决这个问题。 [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为了实现这一点,我想告诉编译器类型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;
    } 

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?还是我一开始就写这个 class 是愚蠢的?

Thanks in advance.提前致谢。

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:我真的不喜欢上面提供的类型检查和类型转换解决方案,所以这里是 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);
            }
        }
    }
}

I like the direction of the accepted solution but it doesn't scale well for unions of more than three items (eg a union of 9 items would require 9 class definitions).我喜欢接受的解决方案的方向,但它不能很好地扩展超过三个项目的联合(例如,9 个项目的联合将需要 9 个类定义)。

Here is another approach that is also 100% type-safe at compile-time, but that is easy to grow to large unions.这是另一种在编译时也是 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));
    }
}

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.假设您有一个购物车场景,它具有三种状态:“空”、“活动”和“付费”,每种状态都有不同的行为。

  • You create have a ICartState interface that all states have in common (and it could just be an empty marker interface)您创建了一个ICartState接口,所有状态都有其共同点(它可能只是一个空的标记接口)
  • You create three classes that implement that interface.您创建了三个实现该接口的类。 (The classes do not have to be in an inheritance relationship) (类不必具有继承关系)
  • The interface contains a "fold" method, whereby you pass a lambda in for each state or case that you need to handle.该接口包含一个“折叠”方法,您可以通过该方法为需要处理的每个状态或案例传递一个 lambda。

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.您可以使用 C# 中的 F# 运行时,但作为更轻量级的替代方案,我编写了一个小 T4 模板来生成这样的代码。

Here's the interface:这是界面:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}

And here's the implementation:这是实现:

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

Now let's say you extend the CartStateEmpty and CartStateActive with an AddItem method which is not implemented by CartStatePaid .现在让我们假设你延长CartStateEmptyCartStateActiveAddItem方法,它被执行CartStatePaid

And also let's say that CartStateActive has a Pay method that the other states dont have.并且假设CartStateActive有一个其他州没有的Pay方法。

Then here's some code that shows it in use -- adding two items and then paying for the cart:然后这里有一些代码显示它在使用中——添加两个项目然后支付购物车:

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

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.请注意,此代码是完全类型安全的 —— 任何地方都没有强制转换或条件,并且如果您尝试为空购物车付款,则会出现编译器错误,例如。

I have written a library for doing this at https://github.com/mcintyre321/OneOf我在https://github.com/mcintyre321/OneOf编写了一个库来执行此操作

Install-Package OneOf安装包 OneOf

It has the generic types in it for doing DUs eg OneOf<T0, T1> all the way to OneOf<T0, ..., T9> .它具有用于执行OneOf<T0, T1>的通用类型,例如OneOf<T0, T1>一直到OneOf<T0, ..., T9> Each of those has a .Match , and a .Switch statement which you can use for compiler safe typed behaviour, eg:每个都有一个.Match和一个.Switch语句,您可以将其用于编译器安全的类型化行为,例如:

``` ``

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

``` ``

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.在 C 中,联合是一种对多个字段使用相同内存位置的结构。 For example:例如:

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

The floatOrScalar union could be used as a float, or an int, but they both consume the same memory space. floatOrScalar union 可以用作 float 或 int,但它们都消耗相同的内存空间。 Changing one changes the other.改变一个改变另一个。 You can achieve the same thing with a struct in C#:您可以使用 C# 中的结构实现相同的功能:

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

The above structure uses 32bits total, rather than 64bits.上述结构总共使用了 32 位,而不是 64 位。 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.上面的示例是一个类,鉴于 CLR 的性质,不能保证内存效率。 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 backing object field.如果将Union<A, B, C>从一种类型更改为另一种类型,则不一定要重用内存……很可能是在堆上分配新类型并在支持object字段中放置不同的指针。 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.真正的 union相反,如果您不使用 Union 类型,您的方法实际上可能会导致更多的堆颠簸。

And my attempt on minimal yet extensible solution using nesting of Union/Either type .我尝试使用Union/Either type 嵌套来尝试最小但可扩展的解决方案。 Also usage of default parameters in Match method naturally enables "Either X Or Default" scenario.此外,在 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);
            }
        }
    }
}
char foo = 'B';

bool bar = foo is int;

This results in a warning, not an error.这会导致警告,而不是错误。 If you're looking for your Is and As functions to be analogs for the C# operators, then you shouldn't be restricting them in that way anyhow.如果您正在寻找与 C# 运算符类似的IsAs函数,那么无论如何您都不应该以这种方式限制它们。

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.您不能也不会实现任何类型的安全,只能使用 FieldOffset 实现字节值安全。

It would make much more sense to have a generic ValueWrapper<T1, T2> with T1 ValueA and T2 ValueB , ...使用具有T1 ValueAT2 ValueB的通用ValueWrapper<T1, T2>会更有意义,...

PS: when talking about type-safety I mean compile-time type-safety. PS:在谈论类型安全时,我指的是编译时类型安全。

If you need a code wrapper (performing bussiness logic on modifications you can use something along the lines of:如果您需要代码包装器(对修改执行业务逻辑,您可以使用以下内容:

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

For an easy way out you could use (it has performance issues, but it is very simple):您可以使用一种简单的方法(它有性能问题,但非常简单):

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

Here is my attempt.这是我的尝试。 It does compile time checking of types, using generic type constraints.它使用泛型类型约束对类型进行编译时检查。

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

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?)特别是,我无法弄清楚如何摆脱 As/Is/Set 的类型参数(没有办法指定一个类型参数并让 C# 计算另一个类型参数吗?)

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.)所以我多次遇到同样的问题,我只是想出了一个获得我想要的语法的解决方案(以牺牲 Union 类型实现中的一些丑陋为代价。)

To recap: we want this sort of usage at the call site.回顾一下:我们希望在呼叫站点使用这种用法。

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

We want the following examples to fail to compile, however, so that we get a modicum of type safety.但是,我们希望以下示例无法编译,以便我们获得一点类型安全。

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

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.三、四等类型参数的实现是直接的。

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

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.一旦尝试访问尚未初始化的变量,您就可以抛出异常,即,如果它是使用 A 参数创建的,然后又尝试访问 B 或 C,则可能会抛出 UnsupportedOperationException。 You'd need a getter to make it work though.你需要一个吸气剂来让它工作。

You can export a pseudo-pattern matching function, like I use for the Either type in my Sasa library .您可以导出一个伪模式匹配函数,就像我在Sasa 库中用于任一类型一样。 There's currently runtime overhead, but I eventually plan to add a CIL analysis to inline all the delegates into a true case statement.目前存在运行时开销,但我最终计划添加一个 CIL 分析来将所有委托内联到一个真实的 case 语句中。

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:不可能完全使用您使用过的语法,但是通过更加冗长和复制/粘贴,很容易使重载解析为您完成这项工作:


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

By now it should be pretty obvious how to implement it:到目前为止,如何实现它应该很明显了:


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

There are no checks for extracting the value of the wrong type, eg:没有提取错误类型的值的检查,例如:


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

So you might consider adding necessary checks and throw exceptions in such cases.因此,您可能会考虑在这种情况下添加必要的检查并抛出异常。

I am currently trying to create a Julia Runtime in .NET.我目前正在尝试在 .NET 中创建 Julia 运行时。 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#). Julia 具有 Union{Int, String}... 等类型。我目前正在尝试模拟这个 .NET(不做无法从 c# 调用的奇怪 IL)。

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).我将为 object 联合创建更多联合,并交叉 object 和结构联合(这将是最复杂的情况)。


public struct Union<T1,T2> where T1 : struct where T2 : struct{
        private byte type;
        [FieldOffset(1)] private T1 a1;
        [FieldOffset(1)] private T2 a2;
        public T1 A1 {
            get => a1;
            set {
                a1 = value;
                type = 1;
            }
        }

        public T2 A2 {
            get => a2;
            set {
                a2 = value;
                type = 2;
            }
        }

        public Union(int _ = 0) {
            type = 0;
            a1 = default;
            a2 = default;
        }
        public Union(T1 a) : this() => A1 = a;
        public Union(T2 a) : this() => A2 = a;
        public bool HasValue => type < 1 || type > 2;
        public bool IsNull => !HasValue;
        public bool IsT1 => type == 1;
        public bool IsT2 => type == 2;
        public Type GetType() {
            switch (type) {
                case 1: return typeof(T1);
                case 2: return typeof(T2);
                default: return null;
            }
        }
    }

You can use the above like the following:您可以使用上述内容,如下所示:

   
   Union<int, long> myUnion(5); \\Set int inside
   myUnion.a2 = 5;
   Type theTypeInside = myUnion.GetType();  //long
   myUnion.a1 = 5;
   theTypeInside = myUnion.GetType(); //int

I will also be creating dynamic union generators or aligned unions for the cross object and struct union.我还将为交叉 object 和结构联合创建动态联合生成器或对齐联合。

Take a look at: Generated Struct Union Output to see the current compile time unions I am using.看看: Generated Struct Union Output看看我正在使用的当前编译时联合。

If you want to create a union of any size take a look at Generator for Struct Unions如果您想创建任何大小的联合,请查看Generator for Struct Unions

If anyone has any improvements for the above let me know!如果有人对上述内容有任何改进,请告诉我! Implementing julia into .NET is an extraordinarily hard task!将 julia 实施到 .NET 是一项非常艰巨的任务!

I use own of Union Type.我使用自己的联合类型。

Consider an example to make it clearer.考虑一个例子来使它更清楚。

Imagine we have Contact class:想象一下我们有 Contact 类:

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

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让我们改进我们的领域模型

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

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. PersonaName 类中的 Consturctor 同时需要 FirstName 和 LastName。 This means that after the creation, it can not have invalid state.这意味着创建后,它不能有无效状态。

And contact class respectively并分别联系班级

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

In this case we have same problem, object of Contact class may be in invalid state.在这种情况下,我们遇到了同样的问题,Contact 类的对象可能处于无效状态。 I mean it may have EmailAddress but haven't Name我的意思是它可能有 EmailAddress 但没有 Name

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

Let's fix it and create Contact class with constructor which requires PersonalName, EmailAddress and PostalAddress:让我们修复它并使用需要 PersonalName、EmailAddress 和 PostalAddress 的构造函数创建 Contact 类:

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

But here we have another problem.但是这里我们还有另一个问题。 What if Person have only EmailAdress and haven't PostalAddress?如果 Person 只有 EmailAdress 而没有 PostalAddress 怎么办?

If we think about it there we realize that there are three possibilities of valid state of Contact class object:如果我们考虑一下,我们就会意识到 Contact 类对象的有效状态存在三种可能性:

  1. A contact only has an email address联系人只有一个电子邮件地址
  2. A contact only has a postal address联系人只有邮政地址
  3. A contact has both an email address and a postal address联系人同时具有电子邮件地址和邮政地址

Let's write out domain models.让我们写出领域模型。 For the beginning we will create Contact Info class which state will be corresponding with above cases.首先,我们将创建与上述情况对应的状态的 Contact Info 类。

public class ContactInfo 
{
    public ContactInfo(EmailAddress emailAddress) { ... }
    public ContactInfo(PostalAddress postalAddress) { ... }
    public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}

And Contact class:和联系方式:

public class Contact 
{
    public Contact(
              PersonalName personalName,
              ContactInfo contactInfo
           )
    {
        ...
    }
}

Let's try use it:让我们尝试使用它:

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

Let's add Match method in ContactInfo class让我们在 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)
        {
             ...
        } 
        ...
   }
}

In the match method, we can write this code, because the state of the contact class is controlled with constructors and it may have only one of the possible states.在match方法中,我们可以写这段代码,因为contact类的状态是由构造函数控制的,它可能只有一种可能的状态。

Let's create an auxiliary class, so that each time do not write as many code.让我们创建一个辅助类,这样每次都不会编写尽可能多的代码。

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

We can have such a class in advance for several types, as is done with delegates Func, Action.我们可以预先为几种类型创建一个这样的类,就像委托 Func、Action 一样。 4-6 generic type parameters will be in full for Union class. 4-6 个泛型类型参数将完整用于 Union 类。

Let's rewrite ContactInfo class:让我们重写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.如果我们忘记覆盖其余的构造函数,我们就不能用另一个状态创建 ContactInfo 类的对象。 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()
            )
    );

That's all.就这样。 I hope you enjoyed.我希望你喜欢。

Example taken from the site F# for fun and profit取自F#站点的示例以获取乐趣和利润

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM