Serikat terdiskriminasi di C #


93

[Catatan: Pertanyaan ini memiliki judul asli " C (ish) style union in C # " tetapi seperti yang diberitahukan oleh Jeff, struktur ini disebut 'union yang terdiskriminasi']

Maafkan verbositas pertanyaan ini.

Ada beberapa pertanyaan serupa yang sudah saya miliki di SO tetapi tampaknya berkonsentrasi pada manfaat penyimpanan memori dari union atau menggunakannya untuk interop. Berikut adalah contoh pertanyaan semacam itu .

Keinginan saya untuk memiliki jenis persatuan agak berbeda.

Saya sedang menulis beberapa kode saat ini yang menghasilkan objek yang terlihat seperti ini

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

    public object ValueA;
    public object ValueB;
}

Hal-hal yang cukup rumit, saya pikir Anda akan setuju. Masalahnya adalah itu ValueAhanya bisa dari beberapa jenis tertentu (katakanlah string, intdan Foo(yang merupakan kelas) dan ValueBbisa menjadi kumpulan jenis kecil lainnya. Saya tidak suka memperlakukan nilai-nilai ini sebagai objek (saya ingin perasaan hangat pas coding dengan sedikit keamanan tipe).

Jadi saya berpikir untuk menulis kelas pembungkus kecil yang sepele untuk mengungkapkan fakta bahwa ValueA secara logis adalah referensi ke tipe tertentu. Saya menelepon kelas Unionkarena apa yang saya coba capai mengingatkan saya pada konsep penyatuan di 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);
    }
}

Menggunakan kelas ini ValueWrapper sekarang terlihat seperti ini

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

yang seperti apa yang ingin saya capai tetapi saya kehilangan satu elemen yang cukup penting - yaitu pemeriksaan tipe yang dipaksakan oleh compiler saat memanggil fungsi Is dan As seperti yang ditunjukkan kode berikut

    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 Tidak sah untuk menanyakan ValueA apakah itu a charkarena definisinya dengan jelas mengatakan bukan - ini adalah kesalahan pemrograman dan saya ingin kompilator memahami ini. [Juga jika saya bisa mendapatkan ini dengan benar maka (semoga) saya akan mendapatkan kecerdasan juga - yang akan menjadi keuntungan.]

Untuk mencapai ini, saya ingin memberi tahu kompiler bahwa jenisnya Tbisa salah satu dari A, B atau 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;
    } 

Adakah yang tahu apakah yang ingin saya capai itu mungkin? Atau apakah saya hanya bodoh karena menulis kelas ini sejak awal?

Terima kasih sebelumnya.


3
Serikat di C dapat diimplementasikan di C # untuk tipe nilai menggunakan StructLayout(LayoutKind.Explicit)dan FieldOffset. Ini tidak dapat dilakukan dengan tipe referensi, tentu saja. Apa yang Anda lakukan sama sekali tidak seperti C Union.
Brian

5
Ini sering disebut serikat yang terdiskriminasi .
Jeff Hardy

Terima kasih Jeff - Saya tidak mengetahui istilah ini, tetapi inilah yang ingin saya capai
Chris Fewtrell

7
Mungkin bukan jenis respons yang Anda cari, tetapi apakah Anda sudah mempertimbangkan F #? Ini memiliki serikat pekerja yang aman jenis dan pencocokan pola yang dipanggang langsung dalam bahasa, jauh lebih mudah untuk mewakili serikat pekerja daripada dengan C #.
Juliet

1
Nama lain dari serikat yang terdiskriminasi adalah tipe penjumlahan.
cdiggins

Jawaban:


114

Saya tidak terlalu suka solusi pengecekan tipe dan pengecoran yang disediakan di atas, jadi inilah serikat tipe aman 100% yang akan menimbulkan kesalahan kompilasi jika Anda mencoba menggunakan tipe data yang salah:

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
Yup, jika Anda ingin serikat pekerja terdiskriminasi yang aman, Anda akan membutuhkannya match, dan itu cara yang bagus untuk mendapatkannya.
Pavel Minaev

21
Dan jika semua kode boilerplate membuat Anda kecewa, Anda dapat mencoba penerapan ini yang secara eksplisit memberi tag kasus: pastebin.com/EEdvVh2R . Kebetulan gaya ini sangat mirip dengan cara F # dan OCaml mewakili serikat pekerja secara internal.
Juliet

4
Saya suka kode Juliet yang lebih pendek, tetapi bagaimana jika tipenya adalah <int, int, string>? Bagaimana Anda memanggil konstruktor kedua?
Robert Jeppesen

2
Saya tidak tahu bagaimana ini tidak memiliki 100 suara positif. Itu adalah keindahan!
Paolo Falabella

6
@ nexus pertimbangkan jenis ini di F #:type Result = Success of int | Error of int
AlexFoxGill

33

Saya suka arah solusi yang diterima tetapi tidak berskala baik untuk gabungan lebih dari tiga item (misalnya gabungan 9 item akan membutuhkan 9 definisi kelas).

Berikut adalah pendekatan lain yang juga 100% aman untuk tipe pada waktu kompilasi, tetapi mudah untuk dikembangkan menjadi serikat pekerja besar.

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 Ini harus mendapatkan lebih banyak persetujuan; Saya suka cara Anda membuatnya cukup fleksibel untuk memungkinkan persatuan dari semua jenis arities.
Paul d'Aoust

1 untuk fleksibilitas dan singkatnya solusi Anda. Namun, ada beberapa detail yang mengganggu saya. Saya akan memposting masing-masing sebagai komentar terpisah:
stakx - tidak lagi berkontribusi

1
1. Penggunaan refleksi mungkin menimbulkan penalti kinerja yang terlalu besar dalam beberapa skenario, mengingat serikat pekerja yang terdiskriminasi, karena sifat dasarnya, mungkin sangat sering digunakan.
stakx - tidak lagi berkontribusi

4
2. Penggunaan dynamic& generik dalam UnionBase<A>dan rantai pewarisan tampaknya tidak perlu. Jadikan UnionBase<A>non-generik, bunuh konstruktor yang mengambil A, dan buat valuean object(yang memang demikian; tidak ada manfaat tambahan dalam mendeklarasikannya dynamic). Kemudian turunkan setiap Union<…>kelas langsung dari UnionBase. Ini memiliki keuntungan bahwa hanya Match<T>(…)metode yang tepat yang akan diekspos. (Seperti sekarang, misalnya Union<A, B>mengekspos kelebihan beban Match<T>(Func<A, T> fa)yang dijamin akan memunculkan pengecualian jika nilai yang disertakan bukan A. Itu seharusnya tidak terjadi.)
stakx - tidak lagi berkontribusi

3
Anda mungkin menemukan perpustakaan saya ONEOF berguna, tidak lebih atau kurang ini, tetapi pada Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

Saya menulis beberapa posting blog tentang hal ini yang mungkin berguna:

Misalkan Anda memiliki skenario keranjang belanja dengan tiga status: "Kosong", "Aktif" dan "Berbayar", masing-masing dengan perilaku yang berbeda .

  • Anda membuat memiliki ICartStateantarmuka yang semua negara bagian memiliki kesamaan (dan itu hanya bisa menjadi antarmuka penanda kosong)
  • Anda membuat tiga kelas yang mengimplementasikan antarmuka itu. (Kelas tidak harus dalam hubungan warisan)
  • Antarmuka berisi metode "lipat", di mana Anda meneruskan lambda untuk setiap status atau kasus yang perlu Anda tangani.

Anda dapat menggunakan runtime F # dari C # tetapi sebagai alternatif yang lebih ringan, saya telah menulis template T4 kecil untuk menghasilkan kode seperti ini.

Berikut antarmukanya:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}

Dan inilah implementasinya:

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

Sekarang katakanlah Anda memperpanjang CartStateEmptydan CartStateActivedengan AddItemmetode yang tidak diimplementasikan oleh CartStatePaid.

Dan juga katakanlah itu CartStateActivememiliki Paymetode yang tidak dimiliki negara bagian lain.

Lalu inilah beberapa kode yang menunjukkan sedang digunakan - menambahkan dua item dan kemudian membayar keranjang:

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

Perhatikan bahwa kode ini sepenuhnya aman untuk mengetik - tidak ada casting atau kondisional di mana pun, dan kesalahan kompilator jika Anda mencoba membayar untuk keranjang kosong, misalnya.


Kasus penggunaan yang menarik. Bagi saya, menerapkan serikat yang terdiskriminasi pada objek itu sendiri cukup bertele-tele. Berikut adalah alternatif bergaya fungsional yang menggunakan ekspresi sakelar, berdasarkan model Anda: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Anda dapat melihat bahwa DU tidak benar-benar diperlukan jika hanya ada satu jalur yang "menyenangkan", tetapi DU menjadi sangat membantu saat metode dapat mengembalikan satu jenis atau lainnya, bergantung pada aturan logika bisnis.
David Cuccia

13

Saya telah menulis perpustakaan untuk melakukan ini di https://github.com/mcintyre321/OneOf

Instal-Paket OneOf

Ini memiliki tipe generik di dalamnya untuk melakukan DUs, misalnya, OneOf<T0, T1>sepanjang jalan OneOf<T0, ..., T9>. Masing-masing memiliki .Match, dan .Switchpernyataan yang dapat Anda gunakan untuk perilaku compiler yang diketik dengan aman, misalnya:

``

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

``


7

Saya tidak yakin saya sepenuhnya memahami tujuan Anda. Di C, gabungan adalah struktur yang menggunakan lokasi memori yang sama untuk lebih dari satu bidang. Sebagai contoh:

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

The floatOrScalarserikat dapat digunakan sebagai pelampung, atau int, tapi mereka berdua mengkonsumsi ruang memori yang sama. Mengubah satu mengubah yang lain. Anda dapat mencapai hal yang sama dengan struct di C #:

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

Struktur di atas menggunakan total 32 bit, bukan 64 bit. Ini hanya mungkin dengan struct. Contoh Anda di atas adalah sebuah kelas, dan mengingat sifat CLR, tidak ada jaminan tentang efisiensi memori. Jika Anda mengubah Union<A, B, C>dari satu jenis ke jenis lainnya, Anda tidak perlu menggunakan kembali memori ... kemungkinan besar, Anda mengalokasikan jenis baru di heap dan melepaskan penunjuk yang berbeda di objectbidang dukungan . Berlawanan dengan serikat nyata , pendekatan Anda sebenarnya dapat menyebabkan lebih banyak tumpukan tumpukan daripada yang akan Anda dapatkan jika Anda tidak menggunakan jenis Serikat Anda.


Seperti yang saya sebutkan dalam pertanyaan saya, motivasi saya bukanlah efisiensi memori yang lebih baik. Saya telah mengubah judul pertanyaan agar lebih mencerminkan apa tujuan saya - judul asli "C (ish) union" di belakangnya menyesatkan
Chris Fewtrell

Persatuan yang terdiskriminasi jauh lebih masuk akal untuk apa yang Anda coba lakukan. Sedangkan untuk membuatnya waktu kompilasi diperiksa ... Saya akan melihat ke .NET 4 dan Kontrak Kode. Dengan Kontrak Kode, dimungkinkan untuk memberlakukan Kontrak waktu kompilasi. Persyaratan yang memberlakukan persyaratan Anda pada operator .Is <T>.
jrista

Saya rasa saya masih harus mempertanyakan penggunaan Union, dalam praktik umum. Bahkan di C / C ++, serikat pekerja adalah hal yang berisiko, dan harus digunakan dengan sangat hati-hati. Saya ingin tahu mengapa Anda perlu membawa konstruksi seperti itu ke dalam C # ... apa nilai yang Anda anggap keluar darinya?
jrista

2
char foo = 'B';

bool bar = foo is int;

Ini menghasilkan peringatan, bukan kesalahan. Jika Anda mencari fungsi Isdan Assebagai analog untuk operator C #, Anda tidak boleh membatasinya dengan cara itu.


2

Jika Anda mengizinkan beberapa tipe, Anda tidak dapat mencapai keamanan tipe (kecuali tipe-tipe tersebut terkait).

Anda tidak dapat dan tidak akan mencapai jenis keamanan apa pun, Anda hanya dapat mencapai keamanan nilai byte menggunakan FieldOffset.

Akan lebih masuk akal untuk memiliki generik ValueWrapper<T1, T2>dengan T1 ValueAdan T2 ValueB, ...

PS: ketika berbicara tentang keamanan tipe yang saya maksud adalah keamanan tipe waktu kompilasi.

Jika Anda memerlukan kode pembungkus (menjalankan logika bisnis pada modifikasi, Anda dapat menggunakan sesuatu di sepanjang baris:

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

Untuk jalan keluar yang mudah, Anda dapat menggunakan (ini memiliki masalah kinerja, tetapi sangat sederhana):

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

Saran Anda untuk membuat ValueWrapper generik sepertinya merupakan jawaban yang jelas tetapi menyebabkan masalah bagi saya dalam apa yang saya lakukan. Pada dasarnya, kode saya membuat objek pembungkus ini dengan mengurai beberapa baris teks. Jadi saya memiliki metode seperti ValueWrapper MakeValueWrapper (teks string). Jika saya membuat pembungkusnya generik maka saya perlu mengubah tanda tangan MakeValueWrapper menjadi generik dan kemudian ini berarti bahwa kode pemanggil perlu mengetahui jenis apa yang diharapkan dan saya tidak tahu ini sebelumnya sebelum saya mengurai teks ...
Chris Fewtrell

... tetapi bahkan ketika saya menulis komentar terakhir, rasanya saya mungkin telah melewatkan sesuatu (atau mengacaukan sesuatu) karena apa yang saya coba lakukan tidak terasa sesulit yang saya buat. Saya pikir saya akan kembali dan menghabiskan beberapa menit mengerjakan pembungkus yang dibuat dan melihat apakah saya dapat menyesuaikan kode parsing di sekitarnya.
Chris Fewtrell

Kode yang saya berikan seharusnya hanya untuk logika bisnis. Masalah dengan pendekatan Anda adalah Anda tidak pernah tahu nilai apa yang disimpan di Union pada waktu kompilasi. Ini berarti Anda harus menggunakan pernyataan if atau switch setiap kali Anda mengakses objek Union, karena objek tersebut tidak berbagi fungsionalitas yang sama! Bagaimana Anda akan menggunakan objek pembungkus lebih jauh dalam kode Anda? Anda juga dapat membuat objek umum saat runtime (lambat, tapi mungkin). Opsi mudah lainnya adalah di posting saya yang diedit.
Jaroslav Jandek

Anda pada dasarnya tidak memiliki pemeriksaan jenis waktu kompilasi yang berarti dalam kode Anda sekarang - Anda juga dapat mencoba objek dinamis (pemeriksaan jenis dinamis saat runtime).
Jaroslav Jandek

2

Ini usahaku. Itu mengkompilasi pemeriksaan tipe waktu, menggunakan batasan tipe generik.

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

Ini bisa menggunakan beberapa penyempurnaan. Terutama, saya tidak tahu bagaimana cara menyingkirkan parameter tipe ke As / Is / Set (bukankah ada cara untuk menentukan satu parameter tipe dan membiarkan C # mencari yang lain?)


2

Jadi saya telah mengalami masalah yang sama berkali-kali, dan saya baru saja menemukan solusi yang mendapatkan sintaks yang saya inginkan (dengan mengorbankan beberapa keburukan dalam penerapan tipe Union.)

Singkatnya: kami ingin penggunaan semacam ini di situs panggilan.

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

Namun, kami ingin contoh berikut gagal dikompilasi, sehingga kami mendapatkan sedikit keamanan tipe.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Untuk kredit ekstra, jangan gunakan lebih banyak ruang daripada yang benar-benar dibutuhkan.

Dengan semua yang dikatakan, inilah implementasi saya untuk dua parameter tipe generik. Implementasi untuk tiga, empat, dan seterusnya parameter tipe adalah langsung.

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

Dan upaya saya pada solusi minimal namun dapat diperluas menggunakan tipe bersarang dari Union / Either . Juga penggunaan parameter default dalam metode Match secara alami mengaktifkan skenario "Either X or Default".

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

Anda bisa melempar pengecualian setelah ada upaya untuk mengakses variabel yang belum diinisialisasi, yaitu jika dibuat dengan parameter A dan kemudian ada upaya untuk mengakses B atau C, itu bisa melempar, katakanlah, UnsupportedOperationException. Anda membutuhkan getter untuk membuatnya berhasil.


Ya - versi pertama yang saya tulis memang meningkatkan pengecualian dalam metode As - tetapi sementara ini jelas menyoroti masalah dalam kode, saya lebih suka diberitahu tentang ini pada waktu kompilasi daripada saat runtime.
Chris Fewtrell


0

Anda dapat mengekspor fungsi pencocokan pola semu, seperti yang saya gunakan untuk tipe Either di pustaka Sasa saya . Saat ini ada overhead runtime, tetapi saya akhirnya berencana untuk menambahkan analisis CIL untuk memasukkan semua delegasi ke dalam pernyataan kasus yang sebenarnya.


0

Ini tidak mungkin dilakukan dengan sintaks yang Anda gunakan tetapi dengan sedikit lebih banyak verbositas dan salin / tempel, mudah untuk membuat resolusi yang berlebihan melakukan pekerjaan untuk Anda:


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

Sekarang sudah cukup jelas bagaimana menerapkannya:


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

Tidak ada pemeriksaan untuk mengekstrak nilai dari jenis yang salah, misalnya:


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

Jadi, Anda mungkin mempertimbangkan untuk menambahkan pemeriksaan yang diperlukan dan melempar pengecualian dalam kasus seperti itu.


0

Saya menggunakan Union Type sendiri.

Pertimbangkan contoh untuk membuatnya lebih jelas.

Bayangkan kita memiliki kelas Kontak:

public class Contact 
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string PostalAdrress { get; set; }
}

Ini semua didefinisikan sebagai string sederhana, tetapi apakah sebenarnya itu hanya string? Tentu saja tidak. Nama dapat terdiri dari Nama Depan dan Nama Belakang. Atau apakah email hanyalah sekumpulan simbol? Saya tahu bahwa setidaknya itu harus berisi @ dan itu harus.

Mari tingkatkan model domain kami

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

Di kelas ini akan ada validasi selama pembuatan dan pada akhirnya kita akan memiliki model yang valid. Consturctor di kelas PersonaName membutuhkan FirstName dan LastName secara bersamaan. Artinya setelah dibuat, tidak boleh ada status tidak valid.

Dan kelas kontak masing-masing

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

Dalam hal ini kami memiliki masalah yang sama, objek kelas Kontak mungkin dalam keadaan tidak valid. Maksud saya itu mungkin memiliki EmailAddress tetapi belum Nama

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

Mari perbaiki dan buat kelas Kontak dengan konstruktor yang membutuhkan PersonalName, EmailAddress dan PostalAddress:

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

Tapi di sini kita punya masalah lain. Bagaimana jika Person hanya memiliki EmailAdress dan belum memiliki PostalAddress?

Jika kita memikirkannya di sana kita menyadari bahwa ada tiga kemungkinan keadaan valid dari objek kelas Kontak:

  1. Kontak hanya memiliki alamat email
  2. Kontak hanya memiliki alamat pos
  3. Kontak memiliki alamat email dan alamat pos

Mari kita tulis model domain. Untuk permulaan kita akan membuat kelas Info Kontak yang statusnya akan sesuai dengan kasus di atas.

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

Dan kelas Kontak:

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

Ayo coba gunakan:

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

Mari tambahkan metode Match di kelas 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)
        {
             ...
        } 
        ...
   }
}

Dalam metode pencocokan, kita dapat menulis kode ini, karena status kelas kontak dikontrol dengan konstruktor dan mungkin hanya memiliki satu status yang memungkinkan.

Mari buat kelas tambahan, sehingga setiap kali tidak menulis banyak kode.

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

Kita dapat memiliki kelas seperti itu sebelumnya untuk beberapa jenis, seperti yang dilakukan dengan delegasi Func, Action. 4-6 parameter tipe generik akan lengkap untuk kelas Union.

Ayo tulis ulang ContactInfokelas:

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

Di sini kompilator akan meminta penggantian untuk setidaknya satu konstruktor. Jika kita lupa untuk mengganti konstruktor lainnya, kita tidak dapat membuat objek kelas ContactInfo dengan status lain. Ini akan melindungi kita dari pengecualian waktu proses selama Pencocokan.

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

Itu saja. Saya berharap kamu menikmatinya.

Contoh diambil dari situs F # untuk kesenangan dan keuntungan

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.