[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 ValueA
hanya bisa dari beberapa jenis tertentu (katakanlah string
, int
dan Foo
(yang merupakan kelas) dan ValueB
bisa 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 Union
karena 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 char
karena 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 T
bisa 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.
sumber
StructLayout(LayoutKind.Explicit)
danFieldOffset
. Ini tidak dapat dilakukan dengan tipe referensi, tentu saja. Apa yang Anda lakukan sama sekali tidak seperti C Union.Jawaban:
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); } } } }
sumber
match
, dan itu cara yang bagus untuk mendapatkannya.type Result = Success of int | Error of int
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)); } }
sumber
dynamic
& generik dalamUnionBase<A>
dan rantai pewarisan tampaknya tidak perlu. JadikanUnionBase<A>
non-generik, bunuh konstruktor yang mengambilA
, dan buatvalue
anobject
(yang memang demikian; tidak ada manfaat tambahan dalam mendeklarasikannyadynamic
). Kemudian turunkan setiapUnion<…>
kelas langsung dariUnionBase
. Ini memiliki keuntungan bahwa hanyaMatch<T>(…)
metode yang tepat yang akan diekspos. (Seperti sekarang, misalnyaUnion<A, B>
mengekspos kelebihan bebanMatch<T>(Func<A, T> fa)
yang dijamin akan memunculkan pengecualian jika nilai yang disertakan bukanA
. Itu seharusnya tidak terjadi.)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 .
ICartState
antarmuka yang semua negara bagian memiliki kesamaan (dan itu hanya bisa menjadi antarmuka penanda kosong)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
CartStateEmpty
danCartStateActive
denganAddItem
metode yang tidak diimplementasikan olehCartStatePaid
.Dan juga katakanlah itu
CartStateActive
memilikiPay
metode 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.
sumber
Saya telah menulis perpustakaan untuk melakukan ini di https://github.com/mcintyre321/OneOf
Ini memiliki tipe generik di dalamnya untuk melakukan DUs, misalnya,
OneOf<T0, T1>
sepanjang jalanOneOf<T0, ..., T9>
. Masing-masing memiliki.Match
, dan.Switch
pernyataan 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 );
``
sumber
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
floatOrScalar
serikat 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 diobject
bidang 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.sumber
char foo = 'B'; bool bar = foo is int;
Ini menghasilkan peringatan, bukan kesalahan. Jika Anda mencari fungsi
Is
danAs
sebagai analog untuk operator C #, Anda tidak boleh membatasinya dengan cara itu.sumber
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>
denganT1 ValueA
danT2 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
sumber
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?)
sumber
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.
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(); } } }
sumber
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); } } } }
sumber
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.
sumber
Tim Desain Bahasa C # membahas serikat yang terdiskriminasi pada Januari 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Anda dapat memilih permintaan fitur di https://github.com/dotnet/csharplang/issues/113
sumber
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.
sumber
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.
sumber
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("[email protected]") };
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:
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("[email protected]") ) ); 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) { ... } ... } }
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"); } }
Ayo tulis ulang
ContactInfo
kelas: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) { } }
var contact = new Contact( new PersonalName("James", "Bond"), new ContactInfo( new EmailAddress("[email protected]") ) ); 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
sumber