[Uwaga: to pytanie miało oryginalny tytuł „ Unia w stylu C (ish) w C # ”, ale jak poinformował mnie komentarz Jeffa, najwyraźniej ta struktura jest nazywana „unią dyskryminowaną”]
Przepraszam za gadatliwość tego pytania.
Jest kilka podobnie brzmiących pytań, które mogę zadać już w SO, ale wydają się one koncentrować na korzyściach wynikających z oszczędzania pamięci przez związek lub używanie go do współpracy. Oto przykład takiego pytania .
Moje pragnienie posiadania czegoś w rodzaju związku jest nieco inne.
W tej chwili piszę kod, który generuje obiekty wyglądające trochę tak
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Dość skomplikowane rzeczy, myślę, że się zgodzisz. Chodzi o to, że ValueA
może to być tylko kilka określonych typów (powiedzmy string
, int
i Foo
(co jest klasą) i ValueB
może to być kolejny mały zestaw typów. Nie lubię traktować tych wartości jako obiektów (chcę, aby ciepłe, przytulne uczucie kodowanie z odrobiną bezpieczeństwa typu).
Pomyślałem więc o napisaniu trywialnej małej klasy opakowującej, aby wyrazić fakt, że ValueA jest logicznie odwołaniem do określonego typu. Zadzwoniłem do klasy, Union
ponieważ to, co próbuję osiągnąć, przypomniało mi koncepcję związku w 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);
}
}
Używanie tej klasy ValueWrapper wygląda teraz tak
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
co jest czymś podobnym do tego, co chciałem osiągnąć, ale brakuje mi jednego dość kluczowego elementu - czyli sprawdzania typu wymuszonego przez kompilator podczas wywoływania funkcji Is i As, jak pokazuje poniższy kod
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 Nie można pytać ValueA, czy jest a, char
ponieważ jego definicja wyraźnie mówi, że tak nie jest - jest to błąd programowania i chciałbym, aby kompilator to wychwycił. [Również gdybym mógł to zrobić poprawnie, wtedy (miejmy nadzieję) dostałbym również intelisense - co byłoby dobrodziejstwem.]
Aby to osiągnąć, chciałbym powiedzieć kompilatorowi, że T
może to być typ A, B lub 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;
}
Czy ktoś ma pojęcie, czy to, co chcę osiągnąć, jest możliwe? A może po prostu jestem po prostu głupi, że piszę te zajęcia w pierwszej kolejności?
Z góry dziękuję.
źródło
StructLayout(LayoutKind.Explicit)
iFieldOffset
. Oczywiście nie można tego zrobić w przypadku typów referencyjnych. To, co robisz, wcale nie przypomina Unii C.Odpowiedzi:
Nie podoba mi się powyższe rozwiązania do sprawdzania i rzutowania typów, więc oto w 100% bezpieczny związek, który spowoduje błędy kompilacji, jeśli spróbujesz użyć niewłaściwego typu danych:
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); } } } }
źródło
match
, a to jest równie dobry sposób, jak każdy inny.type Result = Success of int | Error of int
Podoba mi się kierunek przyjętego rozwiązania, ale nie skaluje się on dobrze dla związków złożonych z więcej niż trzech pozycji (np. Suma 9 elementów wymagałaby 9 definicji klas).
Oto inne podejście, które jest również w 100% bezpieczne dla typów w czasie kompilacji, ale można je łatwo rozwinąć do dużych związków.
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)); } }
źródło
dynamic
& generics wUnionBase<A>
łańcuchu dziedziczenia wydaje się niepotrzebne. UczyńUnionBase<A>
nieogólnym, zabij konstruktora pobierającego anA
i wykonajvalue
anobject
(a tak jest; deklarowanie tego nie przynosi żadnych dodatkowych korzyścidynamic
). Następnie wyprowadź każdąUnion<…>
klasę bezpośrednio zUnionBase
. Ma to tę zaletę, żeMatch<T>(…)
zostanie ujawniona tylko właściwa metoda. (Tak jak teraz, np.Union<A, B>
Ujawnia przeciążenie,Match<T>(Func<A, T> fa)
które gwarantuje, że wyrzuci wyjątek, jeśli zamknięta wartość nie jest wartościąA
. To nie powinno się zdarzyć.)Napisałem kilka postów na blogu na ten temat, które mogą być przydatne:
Załóżmy, że masz scenariusz koszyka na zakupy z trzema stanami: „Pusty”, „Aktywny” i „Zapłacony”, z których każdy ma inne zachowanie.
ICartState
interfejs, który jest wspólny dla wszystkich stanów (i może to być po prostu pusty interfejs znacznika)Możesz użyć środowiska uruchomieniowego F # z C #, ale jako lżejszą alternatywę napisałem mały szablon T4 do generowania kodu w ten sposób.
Oto interfejs:
partial interface ICartState { ICartState Transition( Func<CartStateEmpty, ICartState> cartStateEmpty, Func<CartStateActive, ICartState> cartStateActive, Func<CartStatePaid, ICartState> cartStatePaid ); }
A oto realizacja:
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); } }
Teraz powiedzmy, że rozszerzasz
CartStateEmpty
iCartStateActive
za pomocąAddItem
metody, która nie jest implementowana przezCartStatePaid
.Powiedzmy też, że
CartStateActive
maPay
metodę, której nie mają inne stany.Następnie jest kod, który pokazuje go w użyciu - dodanie dwóch przedmiotów, a następnie opłacenie koszyka:
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 ); }
Zwróć uwagę, że ten kod jest całkowicie bezpieczny dla typów - nie ma nigdzie rzutowania ani warunków warunkowych oraz błędów kompilatora, jeśli próbujesz zapłacić za pusty koszyk, powiedzmy.
źródło
Napisałem do tego bibliotekę pod adresem https://github.com/mcintyre321/OneOf
Zawiera ogólne typy do wykonywania DU, np. Aż
OneOf<T0, T1>
doOneOf<T0, ..., T9>
. Każdy z nich ma znak.Match
, i.Switch
instrukcję, której możesz użyć do bezpiecznego wpisywania zachowania kompilatora, np .:`` ''
OneOf<string, ColorName, Color> backgroundColor = getBackground(); Color c = backgroundColor.Match( str => CssHelper.GetColorFromString(str), name => new Color(name), col => col );
`` ''
źródło
Nie jestem pewien, czy w pełni rozumiem Twój cel. W języku C unia to struktura wykorzystująca te same lokalizacje pamięci dla więcej niż jednego pola. Na przykład:
typedef union { float real; int scalar; } floatOrScalar;
floatOrScalar
Związek może być stosowany jako pływak lub int, ale obie zużywa tyle samo miejsca w pamięci. Zmiana jednej zmienia drugą. Możesz osiągnąć to samo za pomocą struktury w C #:[StructLayout(LayoutKind.Explicit)] struct FloatOrScalar { [FieldOffset(0)] public float Real; [FieldOffset(0)] public int Scalar; }
Powyższa struktura wykorzystuje łącznie 32 bity zamiast 64 bitów. Jest to możliwe tylko w przypadku struktury. Twój przykład powyżej jest klasą i biorąc pod uwagę naturę CLR, nie gwarantuje wydajności pamięci. Jeśli zmienisz
Union<A, B, C>
jeden typ na inny, niekoniecznie będziesz ponownie używać pamięci ... najprawdopodobniej alokujesz nowy typ na stercie i upuszczasz inny wskaźnik wobject
polu zapasowym . W przeciwieństwie do prawdziwego związku , twoje podejście może w rzeczywistości spowodować więcej potrząsania stertą niż w przeciwnym razie, gdybyś nie używał swojego typu Union.źródło
char foo = 'B'; bool bar = foo is int;
Powoduje to ostrzeżenie, a nie błąd. Jeśli chcesz, aby twoje
Is
iAs
funkcje były analogami dla operatorów C #, to i tak nie powinieneś ich ograniczać w ten sposób.źródło
Jeśli zezwolisz na wiele typów, nie możesz osiągnąć bezpieczeństwa typów (chyba że typy są powiązane).
Nie możesz i nie osiągniesz żadnego rodzaju bezpieczeństwa, możesz osiągnąć bezpieczeństwo wartości bajtów tylko za pomocą FieldOffset.
Znacznie bardziej sensowne byłoby posiadanie produktu generycznego
ValueWrapper<T1, T2>
zT1 ValueA
iT2 ValueB
...PS: mówiąc o bezpieczeństwie typów mam na myśli bezpieczeństwo typów w czasie kompilacji.
Jeśli potrzebujesz otoki kodu (wykonującej logikę biznesową na modyfikacjach, możesz użyć czegoś takiego jak:
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); } }
Dla łatwego wyjścia możesz użyć (ma problemy z wydajnością, ale jest bardzo proste):
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
źródło
Oto moja próba. Sprawdza czas kompilacji typów przy użyciu ogólnych ograniczeń typu.
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>() } }
Przydałoby się trochę upiększenia. W szczególności nie mogłem dowiedzieć się, jak pozbyć się parametrów typu do As / Is / Set (czy nie ma sposobu, aby określić jeden parametr typu i pozwolić C # zilustrować drugi?)
źródło
Więc napotkałem ten sam problem wiele razy i właśnie wymyśliłem rozwiązanie, które ma taką składnię, jaką chcę (kosztem jakiejś brzydoty w implementacji typu Union).
Podsumowując: chcemy tego rodzaju wykorzystania w witrynie połączeń.
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";
Chcemy jednak, aby poniższe przykłady nie skompilowały się, abyśmy otrzymali odrobinę bezpieczeństwa typów.
Aby uzyskać dodatkowy kredyt, nie zajmujmy też więcej miejsca niż jest to absolutnie konieczne.
Biorąc to wszystko pod uwagę, oto moja implementacja dla dwóch parametrów typu ogólnego. Implementacja parametrów typu trzy, cztery i tak dalej jest prosta.
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(); } } }
źródło
I moja próba stworzenia minimalnego, ale rozszerzalnego rozwiązania wykorzystującego zagnieżdżanie typu Union / Either . Również użycie parametrów domyślnych w metodzie Match naturalnie włącza scenariusz „Albo X albo domyślny”.
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); } } } }
źródło
Możesz zgłosić wyjątki, gdy pojawi się próba dostępu do zmiennych, które nie zostały zainicjowane, tj. Jeśli zostanie utworzony z parametrem A, a później nastąpi próba uzyskania dostępu do B lub C, może zgłosić, powiedzmy, UnsupportedOperationException. Potrzebowałbyś jednak gettera, aby to zadziałało.
źródło
Zespół ds. Projektowania języka C # omówił związki dyskryminowane w styczniu 2017 r. Https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Możesz głosować na prośbę o funkcję na https://github.com/dotnet/csharplang/issues/113
źródło
Możesz wyeksportować funkcję dopasowywania pseudo-wzorca, tak jak ja używam dla typu Either w mojej bibliotece Sasa . Obecnie istnieje narzut czasu wykonywania, ale ostatecznie planuję dodać analizę CIL, aby umieścić wszystkich delegatów w prawdziwej instrukcji przypadku.
źródło
Nie jest możliwe, aby zrobić dokładnie taką składnię, której użyłeś, ale przy nieco większej szczegółowości i kopiowaniu / wklejaniu łatwo jest sprawić, że rozwiązanie przeciążenia zrobi to za Ciebie:
// 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()); }
Do tej pory powinno być całkiem oczywiste, jak to zaimplementować:
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 { }
Nie ma kontroli wyodrębniania wartości niewłaściwego typu, np .:
var u = Union(10); string s = u.Value(Get.ForType());
Możesz więc rozważyć dodanie niezbędnych kontroli i zgłosić wyjątki w takich przypadkach.
źródło
Używam własnego typu Union.
Rozważ przykład, aby było jaśniej.
Wyobraź sobie, że mamy klasę Contact:
public class Contact { public string Name { get; set; } public string EmailAddress { get; set; } public string PostalAdrress { get; set; } }
Są to wszystkie zdefiniowane jako proste ciągi, ale czy naprawdę są to tylko ciągi? Oczywiście nie. Imię może składać się z imienia i nazwiska. A może e-mail to tylko zestaw symboli? Wiem, że przynajmniej powinien zawierać @ i koniecznie jest.
Poprawmy model domeny
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) { ... } }
W tych klasach będą walidacje podczas tworzenia i ostatecznie będziemy mieć prawidłowe modele. Consturctor w klasie PersonaName wymaga jednocześnie FirstName i LastName. Oznacza to, że po utworzeniu nie może mieć nieprawidłowego stanu.
I odpowiednio klasa kontaktu
public class Contact { public PersonalName Name { get; set; } public EmailAdress EmailAddress { get; set; } public PostalAddress PostalAddress { get; set; } }
W tym przypadku mamy ten sam problem, obiekt klasy Contact może być w nieprawidłowym stanie. Mam na myśli to, że może mieć adres e-mail, ale nie ma nazwy
var contact = new Contact { EmailAddress = new EmailAddress("[email protected]") };
Naprawmy to i stwórzmy klasę Contact z konstruktorem, która wymaga PersonalName, EmailAddress i PostalAddress:
public class Contact { public Contact( PersonalName personalName, EmailAddress emailAddress, PostalAddress postalAddress ) { ... } }
Ale tutaj mamy inny problem. Co się stanie, jeśli osoba ma tylko adres e-mail i nie ma adresu pocztowego?
Jeśli się nad tym zastanowimy, zdamy sobie sprawę, że istnieją trzy możliwości prawidłowego stanu obiektu klasy Contact:
Wypiszmy modele domen. Na początek stworzymy klasę Contact Info, której stan będzie odpowiadał powyższym przypadkom.
public class ContactInfo { public ContactInfo(EmailAddress emailAddress) { ... } public ContactInfo(PostalAddress postalAddress) { ... } public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... } }
I klasa kontaktu:
public class Contact { public Contact( PersonalName personalName, ContactInfo contactInfo ) { ... } }
Spróbujmy tego użyć:
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
Dodajmy metodę Match w klasie 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) { ... } ... } }
Stwórzmy klasę pomocniczą, aby za każdym razem nie pisać jak największej ilości kodu.
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"); } }
Przepiszmy
ContactInfo
klasę: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() ) );
To wszystko. Mam nadzieję, że ci się podobało.
Przykład zaczerpnięty z serwisu F # dla zabawy i zysku
źródło