C# 中的受歧视联合

发布于 2024-09-08 08:53:16 字数 3465 浏览 3 评论 0原文

[注:这个问题的原标题是“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 只能是几种特定类型(例如 stringintFoo ( 我想要带有一点类型安全性的编码的温暖舒适感)。

这是一个类),而 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(17

渔村楼浪 2024-09-15 08:53:16

我不太喜欢上面提供的类型检查和类型转换解决方案,因此这里是 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 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:

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);
            }
        }
    }
}
反话 2024-09-15 08:53:16

我喜欢所接受的解决方案的方向,但它对于超过三个项目的联合不能很好地扩展(例如,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));
    }
}

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.

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));
    }
}
蓝天 2024-09-15 08:53:16

我在 https://github.com/mcintyre321/OneOf 编写了一个库来执行此操作

安装包OneOf

它具有用于执行 DU 的通用类型,例如 OneOf 一直到
OneOf。其中每个都有一个 .Match 和一个 .Switch 语句,您可以使用它们来实现编译器安全的类型行为,例如:

```

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

```

I have written a library for doing this at https://github.com/mcintyre321/OneOf

Install-Package OneOf

It has the generic types in it for doing DUs e.g. OneOf<T0, T1> all the way to
OneOf<T0, ..., T9>. Each of those has a .Match, and a .Switch statement which you can use for compiler safe typed behaviour, e.g.:

```

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

```

梦言归人 2024-09-15 08:53:16

我写了一些关于这个主题的博客文章,可能有用:

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

  • 您创建了一个所有状态共有的 ICartState 接口(它可能只是一个空标记接口)。
  • 您创建了三个实现该接口的类。 (这些类不必处于继承关系中)
  • 该接口包含一个“fold”方法,您可以通过该方法为需要处理的每个状态或情况传递一个 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 方法扩展 CartStateEmptyCartStateActive,该方法不是< /em> 由 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
        );
}    

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

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)
  • 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.

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:

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.

And also let's say that CartStateActive has a Pay 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:

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.

甚是思念 2024-09-15 08:53:16

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

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

floatOrScalar 联合可以用作 float 或 int,但它们都消耗相同的内存空间。改变一者就会改变另一者。您可以使用 C# 中的结构实现相同的效果:

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

上面的结构总共使用 32 位,而不是 64 位。这只能通过结构体实现。上面的示例是一个类,考虑到 CLR 的性质,不能保证内存效率。如果将 Union 从一种类型更改为另一种类型,则不一定会重用内存...最有可能的是,您正在堆上分配一个新类型并删除一个不同的类型支持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:

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. Changing one changes the other. You can achieve the same thing with a struct in 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. 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 backing object 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.

初懵 2024-09-15 08:53:16
char foo = 'B';

bool bar = foo is int;

这会导致警告,而不是错误。如果您正在寻找与 C# 运算符类似的 IsAs 函数,那么您无论如何都不应该以这种方式限制它们。

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.

少女的英雄梦 2024-09-15 08:53:16

如果允许多种类型,则无法实现类型安全(除非类型相关)。

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

使用带有 T1 ValueAT2 ValueB 的通用 ValueWrapper 会更有意义,...

PS:当谈论类型安全我指的是编译时类型安全。

如果您需要一个代码包装器(对修改执行业务逻辑,您可以使用以下内容:

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

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> with T1 ValueA and T2 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:

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
沧笙踏歌 2024-09-15 08:53:16

这是我的尝试。它使用泛型类型约束对类型进行编译时检查。

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# 计算另一个类型参数?)

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?)

最丧也最甜 2024-09-15 08:53:16

所以我多次遇到同样的问题,我只是想出了一个解决方案来获得我想要的语法(以 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();
        }
    }
}

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.

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();
        }
    }
}
森林很绿却致人迷途 2024-09-15 08:53:16

我尝试使用嵌套联合/任一类型来实现最小但可扩展的解决方案。
此外,在 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);
            }
        }
    }
}

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.

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);
            }
        }
    }
}
我只土不豪 2024-09-15 08:53:16

一旦尝试访问尚未初始化的变量,即如果它是使用 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.

明明#如月 2024-09-15 08:53:16

这是我的解决方案,我相信它涵盖了所有主要用例。它经过完全类型检查,并允许 is、as 和 switch 功能。添加 4+ 可能类型的联合只需要添加另一个子类。

public interface IUnionOf<out T>
{
    bool Is();
    T? As();
}

public abstract class Union<T1> : IUnionOf<T1>
{
    protected readonly object? value;

    protected Union(object? value)
    {
        this.value = value;
    }

    bool IUnionOf<T1>.Is() => value is T1;
    T1? IUnionOf<T1>.As() => value is T1 v2 ? v2 : default;
}

public class Union<T1, T2> : Union<T1>, IUnionOf<T2>
{
    protected Union(object? value) : base(value)
    {

    }
    
    bool IUnionOf<T2>.Is() => value is T2;
    T2? IUnionOf<T2>.As() => value is T2 v2 ? v2 : default;


    public void Switch(Action<T1> t1, Action<T2> t2)
    {
        switch (value)
        {
            case T1 v1 : t1(v1); break;
            case T2 v2 : t2(v2); break;
        }
    }

    public static implicit operator Union<T1, T2>(T1 value) => new(value);
    public static implicit operator Union<T1, T2>(T2 value) => new(value);
}

public class Union<T1, T2, T3> : Union<T1, T2>, IUnionOf<T3>
{
    protected Union(object? value) : base(value)
    {
    }

    bool IUnionOf<T3>.Is() => value is T3;
    T3? IUnionOf<T3>.As() => value is T3 v3 ? v3 : default;

    public void Switch(Action<T1> t1, Action<T2> t2, Action<T3> t3)
    {
        switch (value)
        {
            case T1 v1: t1(v1); break;
            case T2 v2: t2(v2); break;
            case T3 v3: t3(v3); break;
        }
    }

    public static implicit operator Union<T1, T2, T3>(T1 value) => new(value);
    public static implicit operator Union<T1, T2, T3>(T2 value) => new(value);
    public static implicit operator Union<T1, T2, T3>(T3 value) => new(value);
}

public static class UnionExtensions
{
    public static bool Is<T>(this IUnionOf<T> union) => union.Is();
    public static T? As<T>(this IUnionOf<T> union) => union.As();
}

隐式转换允许生产者非常自然地使用:

public Union<string, int> GetStringOrInt(bool getString)
{
    if (getString)
    {
        return "Hello";
    }
    else
    {
        return 39;
    }
}

并且扩展方法允许消费者自然使用:

var result = GetStringOrInt(true);

if (result.Is<string>())
{
    // It's a string!
}

var stringValue = result.As<string>();

// In this case, intValue is 0 because As() returns default if the type doesn't match
var intValue = result.As<intValue>();

result.Switch(
    stringValue => 
    {
        // Do something with stringValue
    },
    intValue =>
    {
        // Do something with intValue
    }
);

// This does not compile
if (result.Is<double>()) 
{
}

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.

public interface IUnionOf<out T>
{
    bool Is();
    T? As();
}

public abstract class Union<T1> : IUnionOf<T1>
{
    protected readonly object? value;

    protected Union(object? value)
    {
        this.value = value;
    }

    bool IUnionOf<T1>.Is() => value is T1;
    T1? IUnionOf<T1>.As() => value is T1 v2 ? v2 : default;
}

public class Union<T1, T2> : Union<T1>, IUnionOf<T2>
{
    protected Union(object? value) : base(value)
    {

    }
    
    bool IUnionOf<T2>.Is() => value is T2;
    T2? IUnionOf<T2>.As() => value is T2 v2 ? v2 : default;


    public void Switch(Action<T1> t1, Action<T2> t2)
    {
        switch (value)
        {
            case T1 v1 : t1(v1); break;
            case T2 v2 : t2(v2); break;
        }
    }

    public static implicit operator Union<T1, T2>(T1 value) => new(value);
    public static implicit operator Union<T1, T2>(T2 value) => new(value);
}

public class Union<T1, T2, T3> : Union<T1, T2>, IUnionOf<T3>
{
    protected Union(object? value) : base(value)
    {
    }

    bool IUnionOf<T3>.Is() => value is T3;
    T3? IUnionOf<T3>.As() => value is T3 v3 ? v3 : default;

    public void Switch(Action<T1> t1, Action<T2> t2, Action<T3> t3)
    {
        switch (value)
        {
            case T1 v1: t1(v1); break;
            case T2 v2: t2(v2); break;
            case T3 v3: t3(v3); break;
        }
    }

    public static implicit operator Union<T1, T2, T3>(T1 value) => new(value);
    public static implicit operator Union<T1, T2, T3>(T2 value) => new(value);
    public static implicit operator Union<T1, T2, T3>(T3 value) => new(value);
}

public static class UnionExtensions
{
    public static bool Is<T>(this IUnionOf<T> union) => union.Is();
    public static T? As<T>(this IUnionOf<T> union) => union.As();
}

The implicit conversions allow for very natural usage for producers:

public Union<string, int> GetStringOrInt(bool getString)
{
    if (getString)
    {
        return "Hello";
    }
    else
    {
        return 39;
    }
}

And the extension methods allow for natural usage for consumers:

var result = GetStringOrInt(true);

if (result.Is<string>())
{
    // It's a string!
}

var stringValue = result.As<string>();

// In this case, intValue is 0 because As() returns default if the type doesn't match
var intValue = result.As<intValue>();

result.Switch(
    stringValue => 
    {
        // Do something with stringValue
    },
    intValue =>
    {
        // Do something with intValue
    }
);

// This does not compile
if (result.Is<double>()) 
{
}
这样的小城市 2024-09-15 08:53:16

您可以导出伪模式匹配函数,就像我在 莎莎图书馆。目前存在运行时开销,但我最终计划添加 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.

疏忽 2024-09-15 08:53:16

不可能完全使用您所使用的语法,但通过更详细的操作和复制/粘贴,很容易使重载解析为您完成工作:


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

因此您可能会考虑添加必要的检查并在这种情况下抛出异常。

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, e.g.:


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

So you might consider adding necessary checks and throw exceptions in such cases.

假扮的天使 2024-09-15 08:53:16

我目前正在尝试在 .NET 中创建 Julia 运行时。 Julia 有像 Union{Int, String}... 等类型。我目前正在尝试模拟这个 .NET(不做奇怪的 IL,无法从 c# 调用)。

这是结构体联合的编译时实现。我将为对象联合、跨对象和结构联合创建更多联合(这将是最复杂的情​​况)。


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

您可以像下面这样使用上面的内容:

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

我还将为交叉对象和结构联合创建动态联合生成器或对齐联合。

看一下:生成的结构体联合输出 查看我正在使用的当前编译时联合。

如果您想创建任何大小的联合,请查看 结构联合生成器

如果有人对上述内容有任何改进,请告诉我!将 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).


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.

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!

烟织青萝梦 2024-09-15 08:53:16

我使用自己的联合类型。

考虑一个例子以使其更清楚。

想象一下我们有 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; }
}

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

var contact = new Contact { EmailAddress = new EmailAddress("[email protected]") };

让我们修复它并使用需要 PersonalName、EmailAddress 和 PostalAddress 的构造函数创建 Contact 类:

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

但这里我们还有另一个问题。如果 Person 只有 EmailAdress 而没有 PostalAddress 怎么办?

如果我们考虑一下,我们就会意识到 Contact 类对象的有效状态存在三种可能:

  1. 联系人仅具有电子邮件地址
  2. 联系人仅具有邮政地址
  3. 联系人同时具有电子邮件地址和邮政地址

让我们写出域模型。首先,我们将创建 Contact Info 类,其状态将与上述情况相对应。

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

和 Contact 类:

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

让我们尝试使用它:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("[email protected]")
                  )
               );
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 所做的那样。 Union 类将包含 4-6 个泛型类型参数。

让我们重写 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("[email protected]")
                  )
               );
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 andprofit

I use own of Union Type.

Consider an example to make it clearer.

Imagine we have Contact class:

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. 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. I mean it may have EmailAddress but haven't Name

var contact = new Contact { EmailAddress = new EmailAddress("[email protected]") };

Let's fix it and create Contact class with constructor which requires PersonalName, EmailAddress and PostalAddress:

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?

If we think about it there we realize that there are three possibilities of valid state of Contact class object:

  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.

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("[email protected]")
                  )
               );
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

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.

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. 4-6 generic type parameters will be in full for Union class.

Let's rewrite ContactInfo class:

public sealed class ContactInfo : Union<
                                     EmailAddress,
                                     PostalAddress,
                                     Tuple<EmaiAddress,PostalAddress>
                                  >
{
    public Contact(EmailAddress emailAddress) : base(emailAddress) { }
    public Contact(PostalAddress postalAddress) : base(postalAddress) { }
    public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}

Here the compiler will ask override for at least one constructor. If we forget to override the rest of the constructors we can't create object of ContactInfo class with another state. This will protect us from runtime exceptions during Matching.

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("[email protected]")
                  )
               );
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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文