C#中的歧视联盟

Chr*_*ell 83 c# type-safety discriminated-union

[注意:这个问题的原始标题是" C#中的C(ish)风格联盟 ",但正如杰夫的评论告诉我的那样,显然这种结构被称为"歧视联盟"]

请原谅这个问题的冗长.

我们已经在SO中提出了几个类似的声音问题,但他们似乎专注于联盟的内存节约优势或将其用于互操作. 这是一个这样的问题的例子.

我希望有一个联合类型的东西有点不同.

我现在正在编写一些代码来生成看起来有点像这样的对象

public class ValueWrapper
{
    public DateTime ValueCreationDate;
    // ... other meta data about the value

    public object ValueA;
    public object ValueB;
}
Run Code Online (Sandbox Code Playgroud)

很复杂的东西,我想你会同意的.事情是,ValueA只能是几种特定类型(比方说string,intFoo(这是一个类),ValueB可以是另一小类.我不喜欢将这些值视为对象(我希望温暖舒适的感觉)编码有点类型安全).

所以我想写一个简单的小包装类来表达ValueA逻辑上是对特定类型的引用这一事实.我打电话给班级,Union因为我想要实现的目标让我想起了C语言中的联合概念.

public class Union<A, B, C>
{
    private readonly Type type; 
    public readonly A a;
    public readonly B b;
    public readonly C c;

    public A A{get {return a;}}
    public B B{get {return b;}}
    public C C{get {return c;}}

    public Union(A a)
    {
        type = typeof(A);
        this.a = a;
    }

    public Union(B b)
    {
        type = typeof(B);
        this.b = b;
    }

    public Union(C c)
    {
        type = typeof(C);
        this.c = c;
    }

    /// <summary>
    /// Returns true if the union contains a value of type T
    /// </summary>
    /// <remarks>The type of T must exactly match the type</remarks>
    public bool Is<T>()
    {
        return typeof(T) == type;
    }

    /// <summary>
    /// Returns the union value cast to the given type.
    /// </summary>
    /// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
    public T As<T>()
    {
        if(Is<A>())
        {
            return (T)(object)a;    // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types? 
            //return (T)x;          // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
        }

        if(Is<B>())
        {
            return (T)(object)b; 
        }

        if(Is<C>())
        {
            return (T)(object)c; 
        }

        return default(T);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用此类ValueWrapper现在看起来像这样

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}
Run Code Online (Sandbox Code Playgroud)

这是我想要实现的东西,但我缺少一个相当重要的元素 - 这是在调用Is和As函数时编译器强制类型检查,如下面的代码所示

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }
Run Code Online (Sandbox Code Playgroud)

IMO无论如何询问ValueA是否有效,char因为它的定义清楚地表明它不是 - 这是一个编程错误,我希望编译器能够接受它.[如果我能得到这个正确的话(希望)我也会得到intellisense - 这将是一个福音.]

为了实现这一点,我想告诉编译器该类型T可以是A,B或C之一

    public bool Is<T>() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 
Run Code Online (Sandbox Code Playgroud)

有没有人知道我想要实现的目标是否可行?或者我只是因为首先写这门课而感到愚蠢?

提前致谢.

Jul*_*iet 102

我真的不喜欢上面提供的类型检查和类型转换的解决方案,所以这里的100%类型安全联盟,这将抛出编译错误,如果你尝试使用了错误的数据类型:

using System;

namespace Juliet
{
    class Program
    {
        static void Main(string[] args)
        {
            Union3<int, char, string>[] unions = new Union3<int,char,string>[]
                {
                    new Union3<int, char, string>.Case1(5),
                    new Union3<int, char, string>.Case2('x'),
                    new Union3<int, char, string>.Case3("Juliet")
                };

            foreach (Union3<int, char, string> union in unions)
            {
                string value = union.Match(
                    num => num.ToString(),
                    character => new string(new char[] { character }),
                    word => word);
                Console.WriteLine("Matched union with value '{0}'", value);
            }

            Console.ReadLine();
        }
    }

    public abstract class Union3<A, B, C>
    {
        public abstract T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h);
        // private ctor ensures no external classes can inherit
        private Union3() { } 

        public sealed class Case1 : Union3<A, B, C>
        {
            public readonly A Item;
            public Case1(A item) : base() { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return f(Item);
            }
        }

        public sealed class Case2 : Union3<A, B, C>
        {
            public readonly B Item;
            public Case2(B item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return g(Item);
            }
        }

        public sealed class Case3 : Union3<A, B, C>
        {
            public readonly C Item;
            public Case3(C item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return h(Item);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果所有样板代码都让你失望,你可以试试这个明确标记案例的实现:http://pastebin.com/EEdvVh2R.顺便提一下,这种风格与F#和OCaml在内部代表联盟的方式非常相似. (17认同)
  • @nexus在F#中考虑这种类型:`type Result = int |的成功 int`的错误 (5认同)
  • 我喜欢Juliet更短的代码,但是如果类型是<int,int,string>怎么办?你怎么称呼第二个构造函数? (4认同)
  • 是的,如果你想要类型安全的歧视联盟,你需要`匹配',这是一个很好的方式来获得它. (3认同)
  • @RobertJeppesen 你的 &lt;int, int, int&gt; **union** 究竟代表什么?;) (3认同)
  • 我不知道这怎么没有100个赞成票.这是一件美丽的事情! (2认同)

cdi*_*ins 32

我喜欢接受的解决方案的方向,但是对于超过三个项目的联合,它不能很好地扩展(例如,9个项目的联合将需要9个类定义).

这是另一种在编译时也是100%类型安全的方法,但这很容易扩展到大型联合.

public class UnionBase<A>
{
    dynamic value;

    public UnionBase(A a) { value = a; } 
    protected UnionBase(object x) { value = x; }

    protected T InternalMatch<T>(params Delegate[] ds)
    {
        var vt = value.GetType();    
        foreach (var d in ds)
        {
            var mi = d.Method;

            // These are always true if InternalMatch is used correctly.
            Debug.Assert(mi.GetParameters().Length == 1);
            Debug.Assert(typeof(T).IsAssignableFrom(mi.ReturnType));

            var pt = mi.GetParameters()[0].ParameterType;
            if (pt.IsAssignableFrom(vt))
                return (T)mi.Invoke(null, new object[] { value });
        }
        throw new Exception("No appropriate matching function was provided");
    }

    public T Match<T>(Func<A, T> fa) { return InternalMatch<T>(fa); }
}

public class Union<A, B> : UnionBase<A>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb) { return InternalMatch<T>(fa, fb); }
}

public class Union<A, B, C> : Union<A, B>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc) { return InternalMatch<T>(fa, fb, fc); }
}

public class Union<A, B, C, D> : Union<A, B, C>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd) { return InternalMatch<T>(fa, fb, fc, fd); }
}

public class Union<A, B, C, D, E> : Union<A, B, C, D>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    public Union(E e) : base(e) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd, Func<E, T> fe) { return InternalMatch<T>(fa, fb, fc, fd, fe); }
}

public class DiscriminatedUnionTest : IExample
{
    public Union<int, bool, string, int[]> MakeUnion(int n)
    {
        return new Union<int, bool, string, int[]>(n);
    }

    public Union<int, bool, string, int[]> MakeUnion(bool b)
    {
        return new Union<int, bool, string, int[]>(b);
    }

    public Union<int, bool, string, int[]> MakeUnion(string s)
    {
        return new Union<int, bool, string, int[]>(s);
    }

    public Union<int, bool, string, int[]> MakeUnion(params int[] xs)
    {
        return new Union<int, bool, string, int[]>(xs);
    }

    public void Print(Union<int, bool, string, int[]> union)
    {
        var text = union.Match(
            n => "This is an int " + n.ToString(),
            b => "This is a boolean " + b.ToString(),
            s => "This is a string" + s,
            xs => "This is an array of ints " + String.Join(", ", xs));
        Console.WriteLine(text);
    }

    public void Run()
    {
        Print(MakeUnion(1));
        Print(MakeUnion(true));
        Print(MakeUnion("forty-two"));
        Print(MakeUnion(0, 1, 1, 2, 3, 5, 8));
    }
}
Run Code Online (Sandbox Code Playgroud)

  • **2.**在`UnionBase <A>`和继承链中使用`dynamic`&generics似乎是不必要的.使`UnionBase <A>`非泛型,使用`A`杀死构造函数,并使`value`成为`对象`(它无论如何;声明它是`动态的'没有额外的好处).然后直接从`UnionBase`派生每个`Union <...>`类.这样做的好处是只暴露正确的`Match <T>(...)`方法.(就像现在一样,例如`Union <A,B>`暴露了一个重载`Match <T>(Func <A,T> fa)`,如果封闭的值不是'A',它将保证抛出异常.那不应该发生.) (4认同)
  • **1.** 在某些情况下,使用反射可能会导致过大的性能损失,因为受歧视的联合由于其基本性质而可能会经常使用。 (2认同)
  • 您可能会发现我的库OneOf很有用,它或多或少都有,但是在Nuget上:) https://github.com/mcintyre321/OneOf (2认同)
  • 这个继承类是落后的。如果我有一个返回类型为“Union&lt;int, string&gt;”的函数,我可以返回一个“Union&lt;int, string, Table&gt;”类型的对象,这违反了契约并破坏了类型安全。事实上,对于所有 * 来说,应该是 `Union&lt;T1, T2&gt; : Union&lt;T1, T2, *&gt;`,但不幸的是,C# 不支持这一点。 (2认同)

Gru*_*oon 19

虽然这是一个老问题,但我最近写了一篇关于这个主题的博客文章可能有用.

假设您有一个购物车场景,其中包含三种状态:"空","活动"和"付费",每种状态都有不同的行为.

  • 你创建了一个ICartState所有状态都有共同的接口(它可能只是一个空的标记接口)
  • 您创建了三个实现该接口的类.(这些类不必处于继承关系中)
  • 该接口包含一个"fold"方法,您可以为需要处理的每个状态或大小写传递lambda.

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

这是界面:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}
Run Code Online (Sandbox Code Playgroud)

这是实施:

class CartStateEmpty : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the empty state, so invoke cartStateEmpty 
      return cartStateEmpty(this);
  }
}

class CartStateActive : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the active state, so invoke cartStateActive
      return cartStateActive(this);
  }
}

class CartStatePaid : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the paid state, so invoke cartStatePaid
      return cartStatePaid(this);
  }
}
Run Code Online (Sandbox Code Playgroud)

现在让我们假设您使用实现的方法扩展CartStateEmpty和.CartStateActiveAddItemCartStatePaid

而且,让我们说CartStateActive有一种Pay方法,其他国家没有.

然后这里有一些代码显示它正在使用 - 添加两个项目然后支付购物车:

public ICartState AddProduct(ICartState currentState, Product product)
{
    return currentState.Transition(
        cartStateEmpty => cartStateEmpty.AddItem(product),
        cartStateActive => cartStateActive.AddItem(product),
        cartStatePaid => cartStatePaid // not allowed in this case
        );

}

public void Example()
{
    var currentState = new CartStateEmpty() as ICartState;

    //add some products 
    currentState = AddProduct(currentState, Product.ProductX);
    currentState = AddProduct(currentState, Product.ProductY);

    //pay 
    const decimal paidAmount = 12.34m;
    currentState = currentState.Transition(
        cartStateEmpty => cartStateEmpty,  // not allowed in this case
        cartStateActive => cartStateActive.Pay(paidAmount),
        cartStatePaid => cartStatePaid     // not allowed in this case
        );
}    
Run Code Online (Sandbox Code Playgroud)

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


mci*_*321 9

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

Install-Package OneOf

它具有用于执行DU的通用类型,例如OneOf<T0, T1>一直到 OneOf<T0, ..., T9>.其中每个都有一个.Match和一个.Switch可用于编译器安全类型行为的语句,例如:

```

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

```


jri*_*sta 6

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

typedef union
{
    float real;
    int scalar;
} floatOrScalar;
Run Code Online (Sandbox Code Playgroud)

floatOrScalar联合可以用作浮子,或int,但它们都消耗相同的内存空间.改变一个会改变另一个.您可以使用C#中的结构实现相同的功能:

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

上述结构总共使用32位,而不是64位.这只适用于结构.上面的示例是一个类,并且鉴于CLR的性质,不保证内存效率.如果将a Union<A, B, C>从一种类型更改为另一种类型,则不必重用内存...很可能是,您在堆上分配新类型并在备份object字段中删除不同的指针.与真正的联合相反,如果你没有使用你的联盟类型,你的方法实际上可能导致更多的堆颠簸.