سؤال

ملاحظة: كان لهذا السؤال العنوان الأصلي "C (ISH) Union في C#"ولكن كما أبلغني تعليق جيف ، يبدو أن هذا الهيكل يسمى" الاتحاد المتميز "

عذرًا على فعل هذا السؤال.

هناك بضعة أسئلة سبر مماثلة لاستسلامها بالفعل ، لكن يبدو أنهما يركزان على فوائد توفير الذاكرة في الاتحاد أو استخدامه ل interop.إليك مثال على هذا السؤال.

رغبتي في الحصول على نوع الاتحاد مختلف إلى حد ما.

أنا أكتب بعض التعليمات البرمجية في الوقت الحالي الذي يولد كائنات تبدو مثل هذا قليلاً

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

    public object ValueA;
    public object ValueB;
}

أشياء معقدة جدا أعتقد أنك سوف توافق. الشيء هو ذلك ValueA لا يمكن أن يكون سوى عدد قليل من أنواع معينة (دعنا نقول string, int و Foo (وهو فئة) و ValueB يمكن أن تكون مجموعة صغيرة أخرى من الأنواع. لا أحب التعامل مع هذه القيم ككائنات (أريد الشعور الدافئ بالترميز بشكل مريح مع القليل من السلامة من النوع).

لذلك فكرت في كتابة فئة غلاف صغيرة تافهة للتعبير عن حقيقة أن Valuea منطقيا هو إشارة إلى نوع معين. اتصلت بالصف Union لأن ما أحاول تحقيقه يذكرني بمفهوم الاتحاد في C.

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

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

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

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

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

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

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

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

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

        return default(T);
    }
}

يبدو أن استخدام هذا الفئة ValuewRapper الآن هكذا

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}

هذا شيء مثل ما أردت تحقيقه ، لكنني أفتقد عنصرًا مهمًا إلى حد ما - وهو التحقق من النوع المترجم عند الاتصال بـ IS ووظائف كما يوضح الرمز التالي

    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 نظرًا لأن تعريفه يقول بوضوح أنه ليس كذلك - هذا خطأ في البرمجة وأود أن يلتقط المترجم هذا. [أيضًا ، إذا كان بإمكاني الحصول على هذا صحيح ، فأتمنى أن أحصل على 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;
    } 

هل لدى أي شخص أي فكرة إذا كان ما أريد تحقيقه ممكن؟ أو هل أنا فقط غبي لكتابة هذا الفصل في المقام الأول؟

شكرا مقدما.

هل كانت مفيدة؟

المحلول

أنا لا أحب حقًا حلول الفحص والكتابة من النوع الواردة أعلاه ، لذا إليك اتحاد آمن 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);
            }
        }
    }
}

نصائح أخرى

يعجبني اتجاه الحل المقبول ، لكنه لا يتوسع جيدًا بالنسبة للنقابات التي تضم أكثر من ثلاثة عناصر (على سبيل المثال ، يتطلب اتحاد 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));
    }
}

لقد كتبت بعض منشورات المدونة حول هذا الموضوع والتي قد تكون مفيدة:

لنفترض أن لديك سيناريو عربة تسوق مع ثلاث ولايات: "فارغة" و "نشطة" و "مدفوعة" ، ولكل منها كل منها مختلف سلوك.

  • يمكنك إنشاء ICartState الواجهة المشتركة بين جميع الدول (وقد تكون مجرد واجهة علامة فارغة)
  • يمكنك إنشاء ثلاث فئات تنفذ تلك الواجهة. (لا يجب أن تكون الفصول في علاقة الميراث)
  • تحتوي الواجهة على طريقة "طية" ، حيث تمرر Lambda لكل حالة أو حالة تحتاج إلى التعامل معها.

يمكنك استخدام وقت التشغيل F# من C# ولكن كبديل أخف وزنا ، لقد كتبت قالب 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 و CartStateActive مع ال AddItem الطريقة التي هي ليس التي تنفذها 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
        );
}    

لاحظ أن هذا الرمز هو نوع من الأنواع - لا يوجد صب أو شرطية في أي مكان ، وأخطاء البرمجيات إذا حاولت الدفع مقابل عربة فارغة ، على سبيل المثال.

لقد كتبت مكتبة للقيام بذلك في https://github.com/mcintyre321/oneof

تثبيت عبوة واحد

لديها الأنواع العامة في ذلك للقيام بالوزن مثل 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
);

```

لست متأكدًا من أنني أفهم هدفك تمامًا. في 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;
}

يستخدم الهيكل أعلاه إجمالي 32Bits ، بدلاً من 64 بايت. هذا ممكن فقط مع بنية. مثالك أعلاه هو فئة ، وبالنظر إلى طبيعة CLR ، لا يضمن أي ضمان حول كفاءة الذاكرة. إذا قمت بتغيير أ Union<A, B, C> من نوع إلى آخر ، لا تعيد استخدام الذاكرة بالضرورة ... على الأرجح ، تقوم بتخصيص نوع جديد على الكومة وإسقاط مؤشر مختلف في الدعم object مجال. على عكس أ اتحاد حقيقي, ، قد يتسبب نهجك في الواقع في سحق كومة أكثر مما ستحصل عليه إذا لم تستخدم نوع الاتحاد الخاص بك.

char foo = 'B';

bool bar = foo is int;

هذا يؤدي إلى تحذير ، وليس خطأ. إذا كنت تبحث عن Is و As وظائف لتكون نظائر لمشغلي C# ، ثم يجب ألا تقيدها بهذه الطريقة على أي حال.

إذا سمحت بأنواع متعددة ، فلا يمكنك تحقيق سلامة الكتابة (ما لم تكن الأنواع ذات صلة).

لا يمكنك ولن تحقق أي نوع من السلامة من النوع ، يمكنك فقط تحقيق سلامة القيمة البايت باستخدام FieldOffset.

سيكون من المنطقي أن يكون لديك عام ValueWrapper<T1, T2> مع T1 ValueA و T2 ValueB, ...

ملاحظة: عند الحديث عن سلامة النوع ، أعني سلامة نوع الترجمة.

إذا كنت بحاجة إلى غلاف رمز (أداء منطق الحافلات على التعديلات ، يمكنك استخدام شيء على غرار:

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

ها هي محاولتي. يقوم بتجميع وقت التحقق من الأنواع ، باستخدام قيود النوع العام.

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# بتحديد المعلمة الأخرى؟)

لذا فقد أصبت بهذه المشكلة نفسها عدة مرات ، وقد توصلت للتو إلى حل يحصل على بناء الجملة الذي أريده (على حساب بعض القبح في تنفيذ نوع الاتحاد.)

لتلخيص: نريد هذا النوع من الاستخدام في موقع الاتصال.

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

ومحاولتي على الحد الأدنى من الحل القابل للتوسعة باستخدام تعشيش الاتحاد/أي نوع. كما يتيح استخدام المعلمات الافتراضية في طريقة المطابقة بشكل طبيعي سيناريو "إما 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);
            }
        }
    }
}

يمكنك رمي الاستثناءات بمجرد أن تكون هناك محاولة للوصول إلى المتغيرات التي لم تتم تهيئتها ، أي إذا تم إنشاؤها بمعلمة A وبعد ذلك هناك محاولة للوصول إلى B أو C ، يمكن أن ترمي ، على سبيل المثال ، غير مدعوم. ستحتاج إلى getter لجعلها تعمل رغم ذلك.

يمكنك تصدير وظيفة مطابقة النمط الزائفة ، مثل استخدام أي من النوعين في بلدي مكتبة SASA. يوجد حاليًا وقت التشغيل ، لكنني أخطط في النهاية لإضافة تحليل CIL لتضمين جميع المندوبين في بيان حالة حقيقي.

ليس من الممكن أن تفعل مع بناء الجملة بالضبط الذي استخدمته ، ولكن مع المزيد من الأناقة ونسخ/لصق ، من السهل جعل دقة التحميل الزائد القيام بالمهمة لك:


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

لذلك قد تفكر في إضافة الشيكات اللازمة ورمي الاستثناءات في مثل هذه الحالات.

أستخدم نوع الاتحاد.

النظر في مثال لجعله أكثر وضوحا.

تخيل أن لدينا فئة الاتصال:

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

في هذه الفئات ، سيتم التحقق من صحة أثناء الإنشاء وسيكون لدينا نماذج صالحة في النهاية. يتطلب Consturctor في فئة Personaname اسم FirstName و LastName في نفس الوقت. هذا يعني أنه بعد الخلق ، لا يمكن أن يكون لها حالة غير صالحة.

و Contact Class على التوالي

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

في هذه الحالة ، لدينا نفس المشكلة ، قد يكون كائن فئة الاتصال في حالة غير صالحة. أعني أنه قد يكون لديه eMailAddress ولكن ليس اسمه

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

دعونا نصلحه وإنشاء فئة اتصال مع مُنشئ يتطلب اسم PersonalName و EmailAddress و postaladdress:

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

ولكن هنا لدينا مشكلة أخرى. ماذا لو كان لدى الشخص البريد الإلكتروني فقط ولم يكن postaladdress؟

إذا فكرنا في الأمر ، فسندرك أن هناك ثلاثة إمكانيات لحالة فئة الاتصال الصالحة:

  1. جهة اتصال فقط تحتوي على عنوان بريد إلكتروني
  2. جهة اتصال فقط لها عنوان بريدي
  3. جهة اتصال يحتوي على عنوان بريد إلكتروني وعنوان بريدي

دعنا نكتب نماذج المجال. للبدء ، سنقوم بإنشاء فئة معلومات الاتصال التي ستتوافق مع الحالات المذكورة أعلاه.

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

و Contact Class:

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

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

في طريقة المطابقة ، يمكننا كتابة هذا الرمز ، لأن حالة فئة الاتصال يتم التحكم فيها مع المُنشئين وقد تحتوي على واحدة فقط من الحالات الممكنة.

دعنا ننشئ فئة مساعدة ، بحيث لا تكتب في كل مرة أكبر عدد من التعليمات البرمجية.

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 ، العمل. 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# للمتعة والربح

ناقش فريق تصميم اللغة C# النقابات المتميزة في يناير 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/ldm-2017-01-10.md#discriminated-unions-via- comosed-types

يمكنك التصويت لطلب الميزة في https://github.com/dotnet/csharplang/issues/113

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top