C#中的有区别的联合


92

[注意:这个问题的原标题为“ C#中的C(ish)样式联合 ”,但正如Jeff的评论所知,这种结构显然称为“歧视联合”。

不好意思,这个问题。

在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是否为a是无效的,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;
    } 

有谁知道我想要实现的目标是否可行?还是我只是因为一开始就写这个课程而变得愚蠢?

提前致谢。


3
对于C中的并集,可以使用StructLayout(LayoutKind.Explicit)和在C#中实现值类型FieldOffset。当然,引用类型不能做到这一点。您所做的根本不像C联盟。
布赖恩2010年

4
这通常被称为歧视工会
Jeff Hardy 2010年


7
可能不是您想要的那种响应,但是您是否考虑过F#?它具有用语言编写的类型安全联合和模式匹配,比使用C#表示联合更容易。
朱丽叶2010年

1
所区分的联合的另一个名称是求和类型。
cdiggins 2011年

Answers:


113

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

3
是的,如果要使用类型安全的区分联合,则需要match,这是获取任何类型的好方法。
Pavel Minaev

20
并且,如果所有这些样板代码都使您失望,则可以尝试使用此实现来显式标记案例的实现:pastebin.com/EEdvVh2R。顺便说一句,这种样式与F#和OCaml在内部表示联合的方式非常相似。
朱丽叶

4
我喜欢朱丽叶(Juliet)的较短代码,但是如果类型为<int,int,string>怎么办?您将如何称呼第二个构造函数?
罗伯特·杰普森

2
我不知道这没有100票赞成票。这是美丽的东西!
Paolo Falabella

5
@nexus在F#中考虑这种类型:type Result = Success of int | Error of int
AlexFoxGill

33

我喜欢接受的解决方案的方向,但是对于超过三个项目的并集(例如,一个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));
    }
}

+1应该获得更多的认可;我喜欢您使它足够灵活以允许各种工会联合的方式。
Paul d'Aoust 2014年

+1为您的解决方案提供了灵活性和简洁性。不过,有些细节困扰着我。我将每个人发表为单独的评论:
stakx-不再贡献

1
1.在某些情况下,使用反射可能会导致太大的性能损失,因为受歧视的工会由于其基本性质而可能会经常使用。
stakx-不再贡献

4
2.在继承链中不使用dynamic&泛型UnionBase<A>。将其设为UnionBase<A>非泛型,杀死带有的构造函数A,然后制作value一个object(无论如何;声明它没有额外的好处dynamic)。然后Union<…>直接从导出每个类UnionBase。这样做的好处是只Match<T>(…)公开适当的方法。(现在,例如,Union<A, B>暴露一个重载Match<T>(Func<A, T> fa),如果所包含的值不是,则可以保证引发异常A。这不应该发生。)
stakx-不再贡献

3
您可能会发现我的OneOne库很有用,它或多或少地做到了这一点,但是在Nuget上:) github.com/mcintyre321/OneOf
mcintyre321 '16

20

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

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

  • 您创建了一个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);
  }
}

现在,假设您使用并未实现的方法来扩展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
        );
}    

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


12

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

安装包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
);

```


7

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

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

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

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

上面的结构总共使用32位,而不是64位。这仅对于结构体是可行的。上面的示例是一个类,并且鉴于CLR的性质,不能保证内存效率。如果将a Union<A, B, C>从一种类型更改为另一种类型,则不一定要重用内存...很可能是在堆上分配新类型,并在后备object字段中放置其他指针。与真正的并集相反,您的方法实际上可能导致比如果不使用并集类型要多的堆颠簸。


正如我在问题中提到的那样,我的动机并不是提高内存效率。我更改了问题标题以更好地反映我的目标是-“ C(ish)union”的原始标题在事后引起误导
Chris Fewtrell 2010年

受歧视的工会对于您要做的事情更有意义。至于它的编译时检查...我将研究.NET 4和代码协定。使用代码合同,可以强制执行编译时合同。Requires可以对.Is <T>运算符强制执行您的要求。
jrista 2010年

我想我仍然必须质疑一般情况下使用工会的问题。即使在C / C ++中,联合也是危险的事情,必须格外小心。我很好奇为什么需要将这样的构造引入C#...您认为从中获得了什么价值?
jrista 2010年

2
char foo = 'B';

bool bar = foo is int;

这将导致警告,而不是错误。如果您希望您的函数IsAs函数成为C#运算符的类似物,则无论如何都不应以此方式限制它们。


2

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

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

ValueWrapper<T1, T2>T1 ValueA和一起使用泛型会更有意义T2 ValueB

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

您提出的让ValueWrapper通用的建议似乎是显而易见的答案,但它使我在执行操作时遇到了问题。本质上,我的代码通过解析一些文本行来创建这些包装对象。所以我有一个类似ValueWrapper MakeValueWrapper(string text)的方法。如果我使包装器成为通用包装,那么我需要将MakeValueWrapper的签名更改为通用包装,这反过来又意味着调用代码需要知道期望的类型,而我在解析文本之前就事先不知道这一点。 ...
克里斯·弗特雷尔

...但是即使我正在写最后一条评论,也感觉好像我错过了一些东西(或弄乱了一些东西),因为我尝试做的事情并不像我做的那样难。我想我会回去花一些时间来研究一个通用的包装器,看看我是否可以适应它周围的解析代码。
克里斯·福特雷尔

我提供的代码应该仅用于业务逻辑。您的方法的问题在于,您永远不知道编译时在Union中存储了什么值。这意味着您每次访问Union对象时都必须使用if或switch语句,因为这些对象没有共享的功能!您将如何在代码中进一步使用包装对象?另外,您可以在运行时构造通用对象(缓慢但可行)。另一个简单的选择是在我编辑的帖子中。
Jaroslav Jandek 2010年

现在,您的代码中基本上没有有意义的编译时类型检查-您也可以尝试动态对象(运行时动态类型检查)。
Jaroslav Jandek'7

2

这是我的尝试。它使用通用类型约束来编译类型的时间检查。

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


2

因此,我已经多次遇到相同的问题,并且我想出了一个解决方案,该解决方案可以获取所需的语法(以牺牲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();
        }
    }
}

2

我尝试使用Union / Either类型的嵌套来实现最小但可扩展的解决方案。同样,在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);
            }
        }
    }
}

1

一旦尝试访问尚未初始化的变量,就可以引发异常,即,如果它是使用A参数创建的,后来又试图访问B或C,则可能引发UnsupportedOperationException。但是,您需要使用吸气剂才能使其正常工作。


是的-我编写的第一个版本确实在As方法中引发了异常-但这虽然肯定会突出代码中的问题,但我还是更希望在编译时而不是在运行时得知此问题。
克里斯·弗特雷尔

0

您可以导出伪模式匹配函数,就像我在Sasa库中为Either类型使用的那样。当前存在运行时开销,但我最终计划添加CIL分析,以将所有委托内联到真正的case语句中。


0

不可能完全使用您所使用的语法,但是更多的冗长性和复制/粘贴功能很容易使重载解析为您完成工作:


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

因此,您可以考虑添加必要的检查并在这种情况下引发异常。


0

我使用自己的联合类型。

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

假设我们有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; }
}

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

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

让我们对其进行修复,并使用需要PersonalName,EmailAddress和PostalAddress的构造函数创建Contact类:

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

但是这里我们还有另一个问题。如果“人”只有电子邮件地址而没有“邮政地址”怎么办?

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

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

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

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

和联系人类:

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

让我们尝试使用它:

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

让我们在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("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()
            )
    );

就这样。希望您喜欢。

取自网站F#的示例,旨在获取乐趣和收益


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.