Unia rozłączna w języku C #


93

[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 ValueAmoże to być tylko kilka określonych typów (powiedzmy string, inti Foo(co jest klasą) i ValueBmoż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, Unionponieważ 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, charponieważ 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 Tmoż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ę.


3
Związki w C można zaimplementować w języku C # dla typów wartości przy użyciu StructLayout(LayoutKind.Explicit)i FieldOffset. Oczywiście nie można tego zrobić w przypadku typów referencyjnych. To, co robisz, wcale nie przypomina Unii C.
Brian

5
Nazywa się to często związkiem dyskryminowanym .
Jeff Hardy,

Dzięki Jeff - nie znałem tego terminu, ale to jest dokładnie to, co chcę osiągnąć
Chris Fewtrell

7
Prawdopodobnie nie jest to odpowiedź, której szukasz, ale czy rozważałeś F #? Ma bezpieczne związki i dopasowywanie wzorców wypalone bezpośrednio w języku, znacznie łatwiejsze do reprezentowania związków niż w języku C #.
Juliet

1
Inną nazwą związku dyskryminowanego jest typ sumy.
cdiggins,

Odpowiedzi:


114

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

3
Tak, jeśli chcesz bezpiecznych, dyskryminowanych związków zawodowych, będziesz ich potrzebować match, a to jest równie dobry sposób, jak każdy inny.
Pavel Minaev,

21
A jeśli cały ten standardowy kod cię zawiedzie, możesz wypróbować tę implementację, która zamiast tego wyraźnie oznacza przypadki: pastebin.com/EEdvVh2R . Nawiasem mówiąc, ten styl jest bardzo podobny do sposobu, w jaki F # i OCaml reprezentują związki wewnętrznie.
Julia,

4
Podoba mi się krótszy kod Juliet, ale co, jeśli typy to <int, int, string>? Jak nazwałbyś drugiego konstruktora?
Robert Jeppesen

2
Nie wiem, jak to nie ma 100 głosów za. To rzecz piękna!
Paolo Falabella

6
@nexus rozważ ten typ w F #:type Result = Success of int | Error of int
AlexFoxGill

33

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

+1 Powinno to uzyskać więcej akceptacji; Podoba mi się sposób, w jaki uczyniliście go wystarczająco elastycznym, aby umożliwić związkom wszelkiego rodzaju arcydzieła.
Paul d'Aoust

+1 za elastyczność i zwięzłość rozwiązania. Jednak są pewne szczegóły, które mnie niepokoją. Każdy z nich opublikuję jako osobny komentarz:
stakx - już nie publikuję

1
1. Zastosowanie refleksji może spowodować zbyt dużą utratę wydajności w niektórych scenariuszach, biorąc pod uwagę, że dyskryminowane związki, ze względu na ich podstawowy charakter, mogą być wykorzystywane bardzo często.
stakx - nie publikuje już

4
2. Użycie dynamic& generics w UnionBase<A>łańcuchu dziedziczenia wydaje się niepotrzebne. Uczyń UnionBase<A>nieogólnym, zabij konstruktora pobierającego an Ai wykonaj valuean object(a tak jest; deklarowanie tego nie przynosi żadnych dodatkowych korzyści dynamic). Następnie wyprowadź każdą Union<…>klasę bezpośrednio z UnionBase. Ma to tę zaletę, że Match<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ć.)
stakx - już nie wnosi wkładu

3
Może się okazać moją bibliotekę oneOf przydatna, robi mniej więcej to, ale jest na Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

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.

  • Tworzysz ICartStateinterfejs, który jest wspólny dla wszystkich stanów (i może to być po prostu pusty interfejs znacznika)
  • Tworzysz trzy klasy, które implementują ten interfejs. (Klasy nie muszą być w relacji dziedziczenia)
  • Interfejs zawiera metodę „fold”, w której przekazujesz lambdę dla każdego stanu lub przypadku, który chcesz obsłużyć.

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 CartStateEmptyi CartStateActiveza pomocą AddItemmetody, która nie jest implementowana przez CartStatePaid.

Powiedzmy też, że CartStateActivema Paymetodę, 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.


Ciekawy przypadek użycia. Dla mnie implementowanie dyskryminowanych związków na samych obiektach jest dość rozwlekłe. Oto alternatywa w stylu funkcjonalnym, która używa wyrażeń przełącznika, na podstawie Twojego modelu: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Możesz zobaczyć, że DU nie są naprawdę potrzebni, jeśli istnieje tylko jedna „szczęśliwa” ścieżka, ale stają się bardzo pomocni, gdy metoda może zwrócić jeden lub inny typ, w zależności od reguł logiki biznesowej.
David Cuccia

13

Napisałem do tego bibliotekę pod adresem https://github.com/mcintyre321/OneOf

Zainstaluj pakiet OneOf

Zawiera ogólne typy do wykonywania DU, np. Aż OneOf<T0, T1>do OneOf<T0, ..., T9>. Każdy z nich ma znak .Match, i .Switchinstrukcję, 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
);

`` ''


7

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;

floatOrScalarZwią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 w objectpolu 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.


Jak wspomniałem w swoim pytaniu, moją motywacją nie była lepsza wydajność pamięci. Zmieniłem tytuł pytania, aby lepiej odzwierciedlał mój cel - oryginalny tytuł „C (ish) union” jest z perspektywy czasu mylący
Chris Fewtrell

Związek dyskryminowany ma o wiele więcej sensu dla tego, co próbujesz zrobić. Jeśli chodzi o sprawdzanie czasu kompilacji ... przyjrzałbym się .NET 4 i kontraktom kodu. W przypadku kontraktów kodu może być możliwe wymuszenie kontraktu w czasie kompilacji. Wymagania, które wymuszają wymagania dotyczące operatora .Is <T>.
jrista

Myślę, że nadal muszę kwestionować stosowanie Unii w ogólnej praktyce. Nawet w C / C ++ związki są ryzykowne i muszą być używane z najwyższą ostrożnością. Jestem ciekawy, dlaczego musisz wprowadzić taki konstrukt do C # ... jaką wartość dostrzegasz, wydostając się z niego?
jrista

2
char foo = 'B';

bool bar = foo is int;

Powoduje to ostrzeżenie, a nie błąd. Jeśli chcesz, aby twoje Isi Asfunkcje były analogami dla operatorów C #, to i tak nie powinieneś ich ograniczać w ten sposób.


2

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>z T1 ValueAi T2 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

Twoja sugestia, aby ValueWrapper była generyczna, wydaje się oczywistą odpowiedzią, ale powoduje to problemy w tym, co robię. Zasadniczo mój kod tworzy te obiekty opakowujące, analizując niektóre wiersze tekstu. Mam więc metodę taką jak ValueWrapper MakeValueWrapper (tekst ciągu). Jeśli zrobię opakowanie ogólne, muszę zmienić podpis MakeValueWrapper na rodzajowy, a to z kolei oznacza, że ​​kod wywołujący musi wiedzieć, jakie typy są oczekiwane, a ja po prostu nie wiem tego z góry, zanim przeanalizuję tekst ...
Chris Fewtrell

... ale nawet gdy pisałem ostatni komentarz, wydawało mi się, że być może coś przeoczyłem (lub coś zepsułem), ponieważ to, co próbuję zrobić, nie wydaje mi się tak trudne, jak to robię. Myślę, że wrócę i spędzę kilka minut pracując nad uogólnionym opakowaniem i zobaczę, czy mogę dostosować otaczający go kod parsujący.
Chris Fewtrell

Kod, który podałem, ma służyć tylko logice biznesowej. Problem z twoim podejściem polega na tym, że nigdy nie wiesz, jaka wartość jest przechowywana w Unii w czasie kompilacji. Oznacza to, że będziesz musiał użyć instrukcji if lub switch za każdym razem, gdy uzyskasz dostęp do obiektu Union, ponieważ te obiekty nie mają wspólnych funkcji! Jak zamierzasz dalej używać obiektów opakowania w kodzie? Możesz także konstruować ogólne obiekty w czasie wykonywania (powolne, ale możliwe). Inną łatwą opcją jest w moim edytowanym poście.
Jaroslav Jandek

Obecnie w kodzie nie ma w zasadzie żadnego sensownego sprawdzania typu w czasie kompilacji - możesz również wypróbować obiekty dynamiczne (dynamiczne sprawdzanie typów w czasie wykonywania).
Jaroslav Jandek

2

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


2

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.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

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

2

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

1

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.


Tak - pierwsza wersja, którą napisałem, zgłosiła wyjątek w metodzie As - ale chociaż to z pewnością podkreśla problem w kodzie, o wiele wolę, aby o tym mówiono w czasie kompilacji niż w czasie wykonywania.
Chris Fewtrell


0

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.


0

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.


0

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("foo@bar.com") };

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:

  1. Kontakt ma tylko adres e-mail
  2. Kontakt ma tylko adres pocztowy
  3. Kontakt zawiera zarówno adres e-mail, jak i adres pocztowy

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

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

W metodzie match możemy taki kod napisać, ponieważ stan klasy contact jest kontrolowany przez konstruktory i może mieć tylko jeden z możliwych stanów.

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

Możemy mieć taką klasę z góry dla kilku typów, jak to się dzieje z delegatami Func, Action. 4-6 ogólnych parametrów typu będzie pełnych dla klasy Union.

Przepiszmy ContactInfoklasę:

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

W tym przypadku kompilator poprosi o zastąpienie dla co najmniej jednego konstruktora. Jeśli zapomnimy o nadpisaniu pozostałych konstruktorów, nie możemy stworzyć obiektu klasy ContactInfo z innym stanem. To ochroni nas przed wyjątkami w czasie wykonywania podczas dopasowywania.

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

To wszystko. Mam nadzieję, że ci się podobało.

Przykład zaczerpnięty z serwisu F # dla zabawy i zysku

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.