การแบ่งแยกสหภาพแรงงานใน C #


93

[หมายเหตุ: คำถามนี้มีชื่อเดิมว่า " C (ish) style union ใน C # " แต่ตามที่ความคิดเห็นของ Jeff แจ้งให้ฉันทราบว่าโครงสร้างนี้เรียกว่า 'การรวมกลุ่มที่เลือกปฏิบัติ']

ขอโทษคำฟุ่มเฟือยของคำถามนี้

มีคำถามที่ทำให้เกิดเสียงคล้าย ๆ กันอยู่สองข้อใน SO แต่ดูเหมือนว่าพวกเขาจะมุ่งเน้นไปที่ประโยชน์ในการประหยัดหน่วยความจำของสหภาพหรือใช้เพื่อการทำงานร่วมกัน นี่คือตัวอย่างของคำถามดังกล่าว

ความปรารถนาของฉันที่จะมีสิ่งที่เป็นสหภาพแตกต่างกันบ้าง

ฉันกำลังเขียนโค้ดบางอย่างในขณะนี้ซึ่งสร้างวัตถุที่มีลักษณะเช่นนี้

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

    public object ValueA;
    public object ValueB;
}

สิ่งที่ค่อนข้างซับซ้อนฉันคิดว่าคุณจะเห็นด้วย เป็นสิ่งที่ValueAสามารถเป็นของบางประเภทไม่กี่ (สมมติว่าstring, intและFoo(ซึ่งเป็นชั้นเรียน) และValueBสามารถเป็นอีกชุดเล็ก ๆ ของประเภท. ฉันไม่ชอบการรักษาค่าเหล่านี้เป็นวัตถุ (ฉันต้องการที่อบอุ่นอย่างอบอุ่นความรู้สึกของ การเข้ารหัสด้วยความปลอดภัยเล็กน้อย)

ดังนั้นฉันจึงคิดเกี่ยวกับการเขียนคลาส wrapper เล็ก ๆ น้อย ๆ เพื่อแสดงความจริงที่ว่า ValueA มีเหตุผลเป็นการอ้างอิงถึงประเภทใดประเภทหนึ่ง ฉันเรียกชั้นเรียนUnionเพราะสิ่งที่ฉันพยายามจะบรรลุทำให้ฉันนึกถึงแนวคิดการรวมกลุ่มในค.

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เพราะคำจำกัดความของมันบอกอย่างชัดเจนว่าไม่ใช่ - นี่เป็นข้อผิดพลาดในการเขียนโปรแกรมและฉันต้องการให้คอมไพเลอร์รับสิ่งนี้ [นอกจากนี้ถ้าฉันสามารถแก้ไขสิ่งนี้ได้ (หวังว่า) ฉันจะได้รับ 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;
    } 

ใครมีความคิดบ้างไหมว่าสิ่งที่ฉันต้องการบรรลุเป็นไปได้ หรือฉันโง่ธรรมดาที่เขียนคลาสนี้ตั้งแต่แรก?

ขอบคุณล่วงหน้า.


3
ยูเนี่ยนใน C สามารถดำเนินการใน C # สำหรับประเภทค่าใช้และStructLayout(LayoutKind.Explicit) FieldOffsetสิ่งนี้ไม่สามารถทำได้กับประเภทอ้างอิงแน่นอน สิ่งที่คุณกำลังทำไม่เหมือน C Union เลย
Brian

5
นี้มักจะถูกเรียกว่าสหภาพเลือกปฏิบัติ
Jeff Hardy

ขอบคุณเจฟฟ์ - ฉันไม่รู้จักคำศัพท์นี้ แต่นี่เป็นสิ่งที่ฉันต้องการบรรลุมากทีเดียว
Chris Fewtrell

7
อาจไม่ใช่คำตอบที่คุณกำลังมองหา แต่คุณได้พิจารณา F # แล้วหรือยัง? มีสหภาพแรงงานประเภทปลอดภัยและการจับคู่รูปแบบที่ถูกอบในภาษาง่ายกว่ามากในการเป็นตัวแทนสหภาพแรงงานมากกว่าภาษา C #
Juliet

1
ชื่ออื่นสำหรับสหภาพที่แบ่งแยกคือประเภทผลรวม
cdiggins

คำตอบ:


114

ฉันไม่ชอบโซลูชันการตรวจสอบประเภทและการหล่อแบบที่ให้ไว้ข้างต้นดังนั้นนี่คือสหภาพที่ปลอดภัย 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

21
และถ้าทุกสิ่งที่รหัสสำเร็จรูปทำให้คุณได้ลงคุณสามารถลองการดำเนินงานนี้อย่างชัดเจนซึ่งแท็กกรณีแทน: pastebin.com/EEdvVh2R อนึ่งลักษณะนี้คล้ายกันมากกับวิธีที่ F # และ OCaml เป็นตัวแทนของสหภาพแรงงานภายใน
Juliet

4
ฉันชอบโค้ดที่สั้นกว่าของ Juliet แต่ถ้าประเภทเป็น <int, int, string> ล่ะ? คุณจะเรียกผู้สร้างที่สองว่าอย่างไร?
Robert Jeppesen

2
ฉันไม่รู้ว่านี่ไม่มี 100 upvotes ได้อย่างไร มันเป็นสิ่งที่สวยงาม!
Paolo Falabella

6
@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

+1 เพื่อความยืดหยุ่นและความกะทัดรัดของโซลูชันของคุณ มีรายละเอียดบางอย่างที่ทำให้ฉันรำคาญ ฉันจะโพสต์แต่ละความคิดเห็นแยกกัน:
stakx - ไม่ร่วมให้ข้อมูลอีกต่อไปใน

1
1.การใช้การสะท้อนกลับอาจทำให้เกิดการลงโทษด้านประสิทธิภาพมากเกินไปในบางสถานการณ์เนื่องจากสหภาพแรงงานที่เลือกปฏิบัติเนื่องจากลักษณะพื้นฐานของพวกเขาอาจถูกนำมาใช้บ่อยมาก
stakx - ไม่ร่วมให้ข้อมูลอีกต่อไปใน

4
2.การใช้dynamic& generics UnionBase<A>และห่วงโซ่การสืบทอดดูเหมือนไม่จำเป็น ทำให้UnionBase<A>ไม่ใช่ทั่วไปฆ่าตัวสร้างสละAและทำให้(ซึ่งมันเป็นอยู่แล้วได้รับประโยชน์มีไม่มีการเพิ่มในประกาศ) จากนั้นได้รับมาในแต่ละระดับได้โดยตรงจาก สิ่งนี้มีข้อได้เปรียบที่จะเปิดเผยเฉพาะวิธีการที่เหมาะสมเท่านั้น (อย่างที่เป็นอยู่ในขณะนี้เช่นแสดงการโอเวอร์โหลดซึ่งรับประกันว่าจะทำให้เกิดข้อยกเว้นหากค่าที่ปิดอยู่ไม่ใช่ค่าที่ไม่ควรเกิดขึ้น)valueobjectdynamicUnion<…>UnionBaseMatch<T>(…)Union<A, B>Match<T>(Func<A, T> fa)A
stakx - ไม่ให้ข้อมูลอีกต่อไปใน

3
คุณอาจพบว่า OneOf ห้องสมุดของฉันมีประโยชน์มันทำสิ่งนี้ได้มากหรือน้อย แต่อยู่ใน Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

ฉันเขียนบล็อกโพสต์เกี่ยวกับเรื่องนี้ซึ่งอาจเป็นประโยชน์:

สมมติว่าคุณมีสถานการณ์รถเข็นช็อปปิ้งที่มี 3 สถานะ ได้แก่ "ว่าง" "ใช้งานอยู่" และ "ชำระเงิน" ซึ่งแต่ละสถานะมีลักษณะการทำงานที่แตกต่างกัน

  • คุณสร้างมีICartStateอินเทอร์เฟซที่ทุกสถานะมีเหมือนกัน (และอาจเป็นเพียงอินเทอร์เฟซเครื่องหมายว่างเปล่า)
  • คุณสร้างคลาสสามคลาสที่ใช้อินเทอร์เฟซนั้น (ชั้นเรียนไม่จำเป็นต้องมีความสัมพันธ์ทางมรดก)
  • อินเทอร์เฟซประกอบด้วยวิธีการ "พับ" โดยที่คุณส่งแลมบ์ดาในแต่ละสถานะหรือกรณีที่คุณต้องจัดการ

คุณสามารถใช้รันไทม์ 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
        );
}    

โปรดทราบว่ารหัสนี้เป็นประเภทที่ปลอดภัยอย่างสมบูรณ์ - ไม่มีการแคสต์หรือเงื่อนไขใด ๆ และข้อผิดพลาดของคอมไพเลอร์หากคุณพยายามจ่ายเงินสำหรับรถเข็นเปล่า


กรณีการใช้งานที่น่าสนใจ สำหรับฉันการใช้สหภาพแรงงานที่เลือกปฏิบัติกับวัตถุนั้นเองก็ค่อนข้างละเอียด นี่คือทางเลือกที่ทำงานแบบที่ใช้สลับการแสดงออกขึ้นอยู่กับรูปแบบของคุณ: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 คุณจะเห็นได้ว่า DU ไม่จำเป็นจริงๆหากมีเส้นทางที่ "มีความสุข" เพียงเส้นทางเดียว แต่จะมีประโยชน์มากเมื่อเมธอดอาจส่งคืนประเภทใดประเภทหนึ่งขึ้นอยู่กับกฎตรรกะทางธุรกิจ
David Cuccia

13

ฉันได้เขียนไลบรารีสำหรับการทำสิ่งนี้ที่https://github.com/mcintyre321/OneOf

ติดตั้งแพ็คเกจ OneOf

มันมีประเภททั่วไปในนั้นสำหรับการทำ DUS เช่นทุกวิธีการ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รวมกันสามารถใช้เป็น float หรือ int ได้ แต่ทั้งสองใช้พื้นที่หน่วยความจำเท่ากัน การเปลี่ยนหนึ่งเปลี่ยนอีกอัน คุณสามารถบรรลุสิ่งเดียวกันได้ด้วยโครงสร้างใน C #:

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

โครงสร้างข้างต้นใช้ทั้งหมด 32 บิตมากกว่า 64 บิต สิ่งนี้ทำได้เฉพาะกับโครงสร้างเท่านั้น ตัวอย่างของคุณด้านบนเป็นคลาสและเนื่องจากลักษณะของ CLR ทำให้ไม่มีการรับประกันเกี่ยวกับประสิทธิภาพหน่วยความจำ หากคุณเปลี่ยนUnion<A, B, C>จากประเภทหนึ่งไปเป็นอีกประเภทหนึ่งคุณไม่จำเป็นต้องใช้หน่วยความจำซ้ำ ... เป็นไปได้มากว่าคุณกำลังจัดสรรประเภทใหม่บนฮีปและวางตัวชี้ที่แตกต่างกันในobjectฟิลด์สำรอง ตรงกันข้ามกับสหภาพที่แท้จริงวิธีการของคุณอาจทำให้เกิดการกระแทกแบบฮีปมากกว่าที่คุณจะได้รับหากคุณไม่ได้ใช้ประเภทสหภาพของคุณ


ดังที่ฉันได้กล่าวไว้ในคำถามของฉันแรงจูงใจของฉันไม่ใช่ประสิทธิภาพของหน่วยความจำที่ดีขึ้น ฉันได้เปลี่ยนชื่อคำถามเพื่อให้สะท้อนถึงเป้าหมายของฉันได้ดีขึ้น - ชื่อเดิมของ "C (ish) union" ทำให้เข้าใจผิดใน
อดีต

สหภาพแรงงานที่เลือกปฏิบัติจะมีความหมายมากขึ้นสำหรับสิ่งที่คุณกำลังพยายามทำ สำหรับการตรวจสอบเวลาคอมไพล์ ... ฉันจะดู. NET 4 และ Code Contracts ด้วย Code Contracts อาจเป็นไปได้ที่จะบังคับใช้สัญญาเวลาคอมไพล์ข้อกำหนดที่บังคับใช้ข้อกำหนดของคุณกับตัวดำเนินการ. Is <T>
jrista

ฉันเดาว่าฉันยังคงต้องตั้งคำถามเกี่ยวกับการใช้สหภาพในทางปฏิบัติทั่วไป แม้แต่ในภาษา C / C ++ สหภาพแรงงานก็เป็นสิ่งที่มีความเสี่ยงและต้องใช้ความระมัดระวังเป็นอย่างยิ่ง ฉันอยากรู้ว่าทำไมคุณถึงต้องนำโครงสร้างดังกล่าวมาไว้ใน C # ... คุณรับรู้คุณค่าอะไรที่จะได้รับจากมัน?
jrista

2
char foo = 'B';

bool bar = foo is int;

ผลลัพธ์นี้เป็นการเตือนไม่ใช่ข้อผิดพลาด หากคุณกำลังมองหาIsและAsฟังก์ชั่นของคุณให้เป็นแอนะล็อกสำหรับตัวดำเนินการ C # คุณไม่ควร จำกัด ด้วยวิธีนี้


2

หากคุณอนุญาตหลายประเภทคุณจะไม่สามารถบรรลุประเภทความปลอดภัยได้ (เว้นแต่ประเภทจะเกี่ยวข้องกัน)

คุณทำไม่ได้และไม่ได้รับความปลอดภัยประเภทใด ๆ คุณสามารถบรรลุความปลอดภัยแบบไบต์โดยใช้ FieldOffset เท่านั้น

มันจะสมเหตุสมผลมากกว่าที่จะมีของทั่วไปที่ValueWrapper<T1, T2>มีT1 ValueAและT2 ValueB...

PS: เมื่อพูดถึงประเภทความปลอดภัยฉันหมายถึงประเภทความปลอดภัยในการรวบรวมเวลา

หากคุณต้องการ code wrapper (การใช้ bussiness logic ในการปรับเปลี่ยนคุณสามารถใช้บางสิ่งตามบรรทัด:

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 (ข้อความสตริง) ถ้าฉันสร้างกระดาษห่อหุ้มแบบทั่วไปฉันต้องเปลี่ยนลายเซ็นของ MakeValueWrapper เป็นแบบทั่วไปจากนั้นก็หมายความว่ารหัสการโทรจำเป็นต้องรู้ว่าคาดว่าจะเป็นประเภทใดและฉันไม่รู้ล่วงหน้าก่อนที่จะแยกวิเคราะห์ข้อความ ...
Chris Fewtrell

... แต่ในขณะที่ฉันเขียนความคิดเห็นสุดท้ายมันรู้สึกเหมือนว่าฉันอาจจะพลาดอะไรบางอย่างไป (หรือทำอะไรบางอย่างจนยุ่งเหยิง) เพราะสิ่งที่ฉันพยายามทำนั้นไม่รู้สึกว่ามันควรจะยากเท่าที่ฉันกำลังทำ ฉันคิดว่าฉันจะกลับไปใช้เวลาสักสองสามนาทีในการทำงานกับกระดาษห่อหุ้มทั่วไปและดูว่าฉันสามารถปรับรหัสการแยกวิเคราะห์รอบ ๆ มันได้หรือไม่
Chris Fewtrell

รหัสที่ฉันให้ไว้นั้นควรจะใช้เพื่อตรรกะทางธุรกิจเท่านั้น ปัญหาในแนวทางของคุณคือคุณไม่มีทางรู้ว่าค่าใดถูกเก็บไว้ในสหภาพในเวลาคอมไพล์ หมายความว่าคุณจะต้องใช้คำสั่ง if หรือสลับเมื่อใดก็ตามที่คุณเข้าถึงวัตถุ Union เนื่องจากวัตถุเหล่านั้นไม่ได้ใช้ฟังก์ชันร่วมกัน! คุณจะใช้วัตถุ Wrapper เพิ่มเติมในโค้ดของคุณอย่างไร นอกจากนี้คุณสามารถสร้างวัตถุทั่วไปที่รันไทม์ (ช้า แต่เป็นไปได้) อีกหนึ่งตัวเลือกง่ายๆในโพสต์ที่แก้ไขของฉัน
Jaroslav Jandek

โดยพื้นฐานแล้วคุณไม่มีการตรวจสอบประเภทเวลาคอมไพล์ที่มีความหมายในโค้ดของคุณในตอนนี้คุณยังสามารถลองใช้วัตถุแบบไดนามิก (การตรวจสอบประเภทไดนามิกที่รันไทม์)
Jaroslav Jandek

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

และความพยายามของฉันในการแก้ปัญหาน้อยที่สุดยังขยายโดยใช้การทำรังของยูเนี่ยน / ทั้งสองประเภท นอกจากนี้การใช้พารามิเตอร์เริ่มต้นในวิธีการจับคู่จะเปิดใช้สถานการณ์ "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 - แต่ในขณะที่สิ่งนี้เน้นย้ำถึงปัญหาในโค้ด แต่ฉันชอบที่จะได้รับแจ้งเกี่ยวกับสิ่งนี้ในเวลาคอมไพล์มากกว่ารันไทม์
Chris Fewtrell

1

ทีมออกแบบภาษา C # ได้หารือเกี่ยวกับสหภาพแรงงานที่เลือกปฏิบัติในเดือนมกราคม 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types

คุณสามารถโหวตคำขอคุณสมบัติได้ที่https://github.com/dotnet/csharplang/issues/113


0

คุณสามารถส่งออกฟังก์ชั่นการจับคู่หลอกรูปแบบเหมือนที่ผมใช้สำหรับชนิดทั้งในของฉันห้องสมุด Sasa ขณะนี้มีค่าใช้จ่ายรันไทม์อยู่ แต่ในที่สุดฉันก็วางแผนที่จะเพิ่มการวิเคราะห์ CIL เพื่อรวมผู้รับมอบสิทธิ์ทั้งหมดลงในคำสั่งกรณีจริง


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

ดังนั้นคุณอาจพิจารณาเพิ่มการตรวจสอบที่จำเป็นและข้อยกเว้นในกรณีเช่นนี้


-1

ฉันใช้ Union Type ของตัวเอง

พิจารณาตัวอย่างเพื่อให้ชัดเจนขึ้น

ลองนึกภาพว่าเรามีชั้นเรียนติดต่อ:

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 ในเวลาเดียวกัน ซึ่งหมายความว่าหลังจากสร้างแล้วจะไม่มีสถานะที่ไม่ถูกต้อง

และติดต่อชั้นเรียนตามลำดับ

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

ในกรณีนี้เรามีปัญหาเดียวกัน object ของคลาส Contact อาจอยู่ในสถานะไม่ถูกต้อง ฉันหมายความว่าอาจมี 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
           ) 
    { 
         ... 
    }
}

แต่ที่นี่เรามีปัญหาอื่น จะเกิดอะไรขึ้นถ้าบุคคลนั้นมีเพียง EmailAdress และไม่มีที่อยู่ไปรษณีย์?

หากเราคิดเกี่ยวกับเรื่องนี้เราตระหนักดีว่ามีความเป็นไปได้สามประการของสถานะที่ถูกต้องของวัตถุคลาสติดต่อ

  1. ผู้ติดต่อมีที่อยู่อีเมลเท่านั้น
  2. ผู้ติดต่อมีที่อยู่ทางไปรษณีย์เท่านั้น
  3. ผู้ติดต่อมีทั้งที่อยู่อีเมลและที่อยู่ไปรษณีย์

มาเขียนโมเดลโดเมนกัน ในการเริ่มต้นเราจะสร้างคลาสข้อมูลการติดต่อซึ่งสถานะจะสอดคล้องกับกรณีข้างต้น

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

มาเพิ่ม Match method ในคลาส 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, Action พารามิเตอร์ประเภททั่วไป 4-6 ตัวจะเต็มสำหรับคลาส Union

มาเขียน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 # เพื่อความสนุกสนานและผลกำไร

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.