C # 'da ayrımcı sendika


93

[Not: Bu sorunun orijinal başlığı " C # 'de C (ish) tarzı birleşme " idi, ancak Jeff'in yorumunun bana bildirdiği gibi, görünüşe göre bu yapıya "ayrımcılığa dayalı birleşme" deniyor]

Bu sorunun ayrıntıları için özür dilerim.

SO'da benim için benzer sesli sorular var, ancak sendikanın hafıza tasarrufu yararlarına veya birlikte çalışma için kullanmaya odaklanmış gibi görünüyorlar. İşte böyle bir soruya bir örnek .

Sendika tipi bir şeye sahip olma arzum biraz farklı.

Şu anda biraz buna benzeyen nesneler üreten bir kod yazıyorum

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

    public object ValueA;
    public object ValueB;
}

Oldukça karmaşık konulara katılacağınızı düşünüyorum. Şey yani ValueAsadece (let diyelim birkaç belirli tipte olabilir string, intve Foobir sınıf olan () ve ValueBtürleri başka bir küçük kümesi olabilir. (Ben sıcak sıkı bir şekilde duygu istediğiniz nesneleri olarak bu değerleri tedavi sevmiyorum biraz güvenlikli kodlama).

Bu yüzden, ValueA'nın mantıksal olarak belirli bir türe referans olduğu gerçeğini ifade etmek için önemsiz, küçük bir sarmalayıcı sınıfı yazmayı düşündüm. Sınıfı aradım Unionçünkü başarmaya çalıştığım şey bana C'deki sendika konseptini hatırlattı.

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

Bu sınıf ValueWrapper'ı kullanmak artık buna benziyor

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

bu, elde etmek istediğim şey gibi bir şey ama oldukça önemli bir unsuru kaçırıyorum - bu, aşağıdaki kodun gösterdiği gibi Is ve As işlevlerini çağırırken derleyicinin zorunlu kıldığı tür denetimi

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }

IMO ValueA'ya bir olup olmadığını sormak geçerli değildir charçünkü tanımı açıkça öyle olmadığını söylüyor - bu bir programlama hatası ve derleyicinin bunu almasını istiyorum. [Ayrıca bunu doğru yapabilseydim (umarım) zeka da alırdım - bu bir nimet olurdu.]

Bunu başarmak için derleyiciye türün TA, B veya C'den biri olabileceğini söylemek isterim.

    public bool Is<T>() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 

Başarmak istediğim şeyin mümkün olup olmadığı konusunda herhangi bir fikri olan var mı? Yoksa en başta bu dersi yazdığım için aptal mıyım?

Şimdiden teşekkürler.


3
C'deki birlikler, StructLayout(LayoutKind.Explicit)ve kullanılarak değer türleri için C # 'da uygulanabilir FieldOffset. Bu tabii ki referans türleri ile yapılamaz. Yaptığın şey hiç bir C Birliği gibi değil.
Brian

5
Buna genellikle ayrımcılığa dayalı sendika denir .
Jeff Hardy

Teşekkürler Jeff - Bu terimin farkında değildim ama tam olarak elde etmek istediğim şey buydu
Chris Fewtrell

7
Muhtemelen aradığınız türden bir yanıt değil, ancak F # 'ı düşündünüz mü? Doğrudan dilde pişirilmiş tip güvenli sendikalara ve kalıp eşleştirmesine sahiptir, sendikaları temsil etmek C # ile olduğundan çok daha kolaydır.
Juliet

1
Ayrımcılığa uğramış sendikanın bir başka adı da bir toplam türüdür.
cdiggins

Yanıtlar:


114

Yukarıda sağlanan tür denetleme ve tür atama çözümlerini gerçekten sevmiyorum, bu nedenle, yanlış veri türünü kullanmaya çalışırsanız derleme hataları atacak% 100 tür güvenli birleştirme burada:

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
Evet, güvenli ve ayrımcılığa uğramış sendikalar istiyorsanız, ihtiyacınız olacak matchve bu, onu elde etmenin herhangi bir yolu kadar iyi bir yol.
Pavel Minaev

21
Ve tüm bu standart kod sizi alt üst ederse, bunun yerine vakaları açıkça etiketleyen bu uygulamayı deneyebilirsiniz: pastebin.com/EEdvVh2R . Bu arada, bu tarz, F # ve OCaml'ın sendikaları dahili olarak temsil etme biçimine çok benzer.
Juliet

4
Juliet'in daha kısa kodunu seviyorum ama ya türler <int, int, string> ise? İkinci kurucuyu nasıl çağırırsınız?
Robert Jeppesen

2
Bunun nasıl 100 olumlu oyu olmadığını bilmiyorum. Bu güzel bir şey!
Paolo Falabella

6
@nexus bu türü F # olarak düşünün:type Result = Success of int | Error of int
AlexFoxGill

33

Kabul edilen çözümün yönünü seviyorum, ancak üçten fazla maddeden oluşan sendikalar için iyi ölçeklenmiyor (örneğin, 9 maddeden oluşan bir birleşim 9 sınıf tanımı gerektirecektir).

İşte derleme zamanında% 100 tip güvenli olan ancak büyük sendikalara dönüştürülmesi kolay olan başka bir yaklaşım.

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 Bunun daha fazla onay alması gerekiyor; Her tür toprağın birliğine izin verecek kadar esnek hale getirme şeklini seviyorum.
Paul d'Aoust

Çözümünüzün esnekliği ve kısalığı için +1. Yine de beni rahatsız eden bazı detaylar var. Her birini ayrı bir yorum olarak
göndereceğim

1
1. Temel nitelikleri nedeniyle ayrımcılığa uğrayan sendikaların çok sık kullanılabileceği göz önüne alındığında, bazı senaryolarda yansıma kullanımı çok büyük bir performans cezasına neden olabilir.
stakx -

4
2. Kalıtım zincirinde dynamic& jeneriklerin kullanılması UnionBase<A>gereksiz görünüyor. UnionBase<A>Jenerik olmayan yapın , kurucuyu bir alarak öldürün Ave valuebir yapın object(ki bu zaten; bunu bildirmenin ek bir faydası yoktur dynamic). Sonra her Union<…>sınıfı doğrudan türetin UnionBase. Bu, yalnızca uygun Match<T>(…)yöntemin ortaya çıkması avantajına sahiptir . (Şu anda olduğu gibi, ör . Ekteki değer bir değilse bir istisna atması garanti edilen Union<A, B>bir aşırı yükü açığa çıkarır . Bu olmamalıdır.)Match<T>(Func<A, T> fa)A
stakx - artık

3
Kitaplığım OneOf'u yararlı bulabilirsin, aşağı yukarı bunu yapıyor ama Nuget'te :) github.com/mcintyre321/OneOf
mcintyre321

20

Bu konuda faydalı olabilecek bazı blog yazıları yazdım:

Diyelim ki, her biri farklı davranışa sahip üç durumda olan bir alışveriş sepeti senaryonuz var: "Boş", "Etkin" ve "Ödendi" .

  • ICartStateTüm durumların ortak olduğu bir arayüze sahip oluyorsunuz (ve bu sadece boş bir işaret arayüzü olabilir)
  • Bu arabirimi uygulayan üç sınıf oluşturursunuz. (Sınıfların miras ilişkisi içinde olması gerekmez)
  • Arabirim, işlemeniz gereken her durum veya durum için bir lambda girdiğiniz bir "katlama" yöntemi içerir.

C # 'dan F # çalışma zamanını kullanabilirsiniz, ancak daha hafif bir alternatif olarak, bunun gibi bir kod oluşturmak için küçük bir T4 şablonu yazdım.

Arayüz şu şekildedir:

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

Ve işte uygulama:

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

Şimdi senin uzatmak diyelim CartStateEmptyve CartStateActivebir ile AddItemolan yöntemle değil tarafından uygulanan CartStatePaid.

Ve diyelim ki diğer devletlerin sahip olmadığı CartStateActivebir Payyöntemi var.

Ardından, kullanımda olduğunu gösteren bir kod var - iki öğe eklemek ve ardından alışveriş sepeti için ödeme yapmak:

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

Bu kodun tamamen tip güvenli olduğunu unutmayın - hiçbir yerde çevrim veya koşul yoktur ve örneğin boş bir alışveriş sepeti için ödeme yapmaya çalışırsanız derleyici hataları.


İlginç kullanım durumu. Bana göre, ayrımcılığa tabi sendikaları nesnelerin kendileri üzerinde uygulamak oldukça ayrıntılı oluyor. Modelinize bağlı olarak anahtar ifadeleri kullanan işlevsel tarzda bir alternatif: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Sadece bir "mutlu" yol varsa, DU'ların gerçekten gerekli olmadığını görebilirsiniz, ancak iş mantığı kurallarına bağlı olarak bir yöntem bir tür veya başka bir tür döndürebildiğinde çok yardımcı olurlar.
David Cuccia

13

Bunu yapmak için https://github.com/mcintyre321/OneOf adresinde bir kitaplık yazdım

Kurulum Paketi OneOf

DU'ları yapmak için genel tiplere sahiptir, örneğin OneOf<T0, T1>sonuna kadar OneOf<T0, ..., T9>. Bunların her biri, derleyici güvenli tipte davranış için kullanabileceğiniz .Matchbir ve .Switchifadesine sahiptir, örneğin:

''

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

''


7

Amacınızı tam olarak anladığımdan emin değilim. C'de birleşim, birden fazla alan için aynı bellek konumlarını kullanan bir yapıdır. Örneğin:

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

floatOrScalarSendika bir yüzen veya bir int olarak kullanılabilir, ancak her ikisi de aynı bellek alanını tüketirler. Birini değiştirmek diğerini değiştirir. C # 'da bir yapı ile aynı şeyi elde edebilirsiniz:

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

Yukarıdaki yapı 64 bit yerine toplam 32 bit kullanır. Bu sadece bir yapı ile mümkündür. Yukarıdaki örneğiniz bir sınıftır ve CLR'nin doğası göz önüne alındığında, bellek verimliliği konusunda hiçbir garanti vermez. Bir Union<A, B, C>türü bir türden diğerine değiştirirseniz, belleği yeniden kullanmanıza gerek kalmaz ... büyük olasılıkla, yığın üzerine yeni bir tür atıyor ve destek objectalanına farklı bir işaretçi bırakıyorsunuzdur . Gerçek bir birleşmenin aksine , yaklaşımınız aslında Birlik türünüzü kullanmadıysanız, aksi takdirde elde edeceğinizden daha fazla yığın atılmasına neden olabilir.


Sorumda da belirttiğim gibi, motivasyonum daha iyi hafıza verimliliği değildi. Soru başlığını,
amacımın

Ayrımcı bir sendika, yapmaya çalıştığınız şey için çok daha mantıklı. Derleme zamanı kontrol edilmesine gelince ... .NET 4 ve Kod Sözleşmelerine bakardım. Kod Sözleşmeleri ile, bir derleme zamanı Sözleşmesi uygulamak mümkün olabilir. .Is <T> operatörü üzerinde gereksinimlerinizi uygulayan gereklidir.
jrista

Sanırım genel uygulamada hala bir Birliğin kullanımını sorgulamam gerekiyor. C / C ++ 'da bile sendikalar riskli bir şeydir ve çok dikkatli kullanılmalıdır. Neden böyle bir yapıyı C # 'a getirmeniz gerektiğini merak ediyorum ... ondan çıkmanın nasıl bir değer olduğunu düşünüyorsunuz?
jrista

2
char foo = 'B';

bool bar = foo is int;

Bu, bir hata değil uyarı ile sonuçlanır. Sizin Isve Asişlevlerinizin C # operatörleri için analog olmasını istiyorsanız, onları bu şekilde kısıtlamamalısınız.


2

Birden çok türe izin verirseniz, tür güvenliğini elde edemezsiniz (türler ilişkili değilse).

Herhangi bir tür güvenliği elde edemezsiniz ve elde edemezsiniz, yalnızca FieldOffset'i kullanarak bayt değeri güvenliğini elde edebilirsiniz.

Bir jeneriğe ValueWrapper<T1, T2>sahip olmak çok daha mantıklı olurdu T1 ValueAve T2 ValueB...

Not: Tip güvenliği hakkında konuşurken, derleme zamanı tip güvenliğini kastediyorum.

Bir kod sarmalayıcısına ihtiyacınız varsa (değişikliklerde iş mantığı yapmak, şu satırlar boyunca bir şeyler kullanabilirsiniz:

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

Kolay bir çıkış yolu olarak kullanabilirsiniz (performans sorunları vardır, ancak çok basittir):

public class Wrapper
{
    private object v1;
    private object v2;

    public T GetValue1<T>() { if (v1.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v1; }
    public void SetValue1<T>(T value) { v1 = value; }

    public T GetValue2<T>() { if (v2.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v2; }
    public void SetValue2<T>(T value) { v2 = value; }
}

//usage:
Wrapper wrapper = new Wrapper();
wrapper.SetValue1("aaaa");
wrapper.SetValue2(456);

string s = wrapper.GetValue1<string>();
DateTime dt = wrapper.GetValue1<DateTime>();//InvalidCastException

ValueWrapper'ı jenerik yapma öneriniz apaçık bir cevap gibi görünüyor, ancak yaptığım işte bana sorunlara neden oluyor. Esasen, kodum bu sarmalayıcı nesnelerini bazı metin satırlarını ayrıştırarak oluşturmaktır. Bu yüzden ValueWrapper MakeValueWrapper (string text) gibi bir metodum var. Sarmalayıcıyı jenerik yaparsam, MakeValueWrapper imzasını jenerik olacak şekilde değiştirmem gerekir ve sonra bu, arayan kodun hangi türlerin beklendiğini bilmesi gerektiği ve metni ayrıştırmadan önce bunu önceden bilmediğim anlamına gelir. ...
Chris Fewtrell

... ama son yorumu yazarken bile, belki bir şeyi kaçırmışım (ya da bir şeyi mahvetmişim) hissettim çünkü yapmaya çalıştığım şey, yapmakta olduğum kadar zor olması gerektiği kadar hissetmiyor. Sanırım geri dönüp birkaç dakika genel doğrulanmış bir sarmalayıcı üzerinde çalışacağım ve ayrıştırma kodunu etrafına adapte edip edemeyeceğime bakacağım.
Chris Fewtrell

Sağladığım kodun sadece iş mantığı için olması gerekiyordu. Yaklaşımınızla ilgili sorun, derleme sırasında Birlik'te hangi değerin depolandığını asla bilmemenizdir. Bu, Union nesnesine her eriştiğinizde if veya geçiş ifadelerini kullanmanız gerektiği anlamına gelir, çünkü bu nesneler ortak bir işlevi paylaşmaz! Sarmalayıcı nesneleri kodunuzda daha fazla nasıl kullanacaksınız? Ayrıca çalışma zamanında genel nesneler de oluşturabilirsiniz (yavaş, ancak mümkün). İle başka bir kolay seçenek, düzenlenmiş yazımda.
Jaroslav Jandek

Şu anda kodunuzda temelde anlamlı bir derleme zamanı tip kontrolüne sahip değilsiniz - dinamik nesneleri de deneyebilirsiniz (çalışma zamanında dinamik tip kontrolü).
Jaroslav Jandek

2

İşte benim girişimim. Genel tür kısıtlamalarını kullanarak türlerin zaman denetimini derler.

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

Biraz güzelleştirmeye ihtiyacı olabilir. Özellikle, tip parametrelerinden As / Is / Set'e nasıl kurtulacağımı bulamadım (bir tip parametresini belirtip diğerini C # belirlemenin bir yolu yok mu?)


2

Bu yüzden aynı soruna birçok kez çarptım ve istediğim sözdizimini elde eden bir çözüm buldum (Birlik türünün uygulanmasındaki bazı çirkinlikler pahasına).

Özetlemek gerekirse: çağrı sitesinde bu tür bir kullanım istiyoruz.

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

Bununla birlikte, aşağıdaki örneklerin derlenememesini istiyoruz, böylece bir nebze güvenlik türü elde ederiz.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Ekstra kredi için, kesinlikle gerekenden daha fazla yer kaplamayalım.

Tüm söylenenlerle birlikte, işte benim iki genel tür parametresi için uygulamam. Üç, dört ve benzeri tür parametrelerinin uygulanması basittir.

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

Ve Union / Her iki türünün yuvalanmasını kullanarak minimal ancak genişletilebilir bir çözüm bulma girişimim . Ayrıca Match yönteminde varsayılan parametrelerin kullanılması doğal olarak "X Veya Varsayılan" senaryosunu etkinleştirir.

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

Bir kez başlatılmamış değişkenlere erişme girişimi olduğunda istisnalar atabilirsiniz, yani bir A parametresiyle oluşturulmuşsa ve daha sonra B veya C'ye erişme girişimi varsa, örneğin UnsupportedOperationException oluşturabilir. Yine de çalışmasını sağlamak için bir alıcıya ihtiyacınız olacak.


Evet - yazdığım ilk sürüm, As yönteminde bir istisna yarattı - ancak bu kesinlikle koddaki sorunu vurgulamakla birlikte, bunun çalışma zamanında olduğundan daha derleme zamanında anlatılmasını tercih ederim.
Chris Fewtrell


0

Sasa kitaplığımda Her İki tip için kullandığım gibi sözde desen eşleştirme işlevini dışa aktarabilirsiniz . Şu anda çalışma zamanı ek yükü var, ancak sonunda tüm delegeleri gerçek bir vaka bildirimine dahil etmek için bir CIL analizi eklemeyi planlıyorum.


0

Tam olarak kullandığınız sözdizimi ile yapmak mümkün değildir, ancak biraz daha fazla ayrıntı ve kopyalama / yapıştırma ile aşırı yük çözümlemesinin sizin için işi yapması kolaydır:


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

Şimdiye kadar nasıl uygulanacağı oldukça açık olmalı:


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

Yanlış türün değerini çıkarmak için herhangi bir kontrol yoktur, örneğin:


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

Bu nedenle, bu tür durumlarda gerekli kontrolleri eklemeyi ve istisnalar atmayı düşünebilirsiniz.


-1

Sendika Türü'nün sahibini kullanıyorum.

Daha açık hale getirmek için bir örnek düşünün.

İletişim sınıfımız olduğunu hayal edin:

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

Bunların hepsi basit dizeler olarak tanımlanıyor, ancak gerçekten sadece dizeler mi? Tabii ki değil. Ad, Ad ve Soyad'dan oluşabilir. Veya bir E-posta sadece bir dizi semboldür? En azından @ içermesi gerektiğini biliyorum ve zorunlu olarak.

Bize etki alanı modelini geliştirelim

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

Bu sınıflarda, oluşturma sırasında doğrulamalar yapılacak ve sonunda geçerli modellere sahip olacağız. PersonaName sınıfındaki Consturctor, FirstName ve LastName'i aynı anda gerektirir. Bu, yaratıldıktan sonra geçersiz duruma sahip olamayacağı anlamına gelir.

Ve sırasıyla iletişim sınıfı

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

Bu durumda aynı problemi yaşıyoruz, Contact sınıfının nesnesi geçersiz durumda olabilir. Yani EmailAddress olabilir ama Name yok

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

Düzeltelim ve PersonalName, EmailAddress ve PostalAddress gerektiren yapıcı ile Contact sınıfı oluşturalım:

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

Ama burada başka bir sorunumuz var. Kişi yalnızca EmailAdress'e sahipse ve PostalAddress yoksa ne olur?

Orada düşünürsek, Temas sınıfı nesnesinin geçerli durumu için üç olasılık olduğunu anlarız:

  1. Bir kişinin yalnızca bir e-posta adresi vardır
  2. Bir kişinin yalnızca posta adresi vardır
  3. Bir kişinin hem e-posta adresi hem de posta adresi vardır

Alan modellerini yazalım. Başlangıç ​​için, yukarıdaki durumlara karşılık gelecek olan İletişim Bilgileri sınıfını oluşturacağız.

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

Ve İletişim sınıfı:

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

Kullanmayı deneyelim:

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

ContactInfo sınıfına Match metodu ekleyelim

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

Match yönteminde bu kodu yazabiliriz çünkü kontak sınıfının durumu kurucularla kontrol edilir ve olası durumlardan sadece birine sahip olabilir.

Yardımcı bir sınıf oluşturalım ki her seferinde o kadar çok kod yazmasın.

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

Delegeler Func, Action'da olduğu gibi, birkaç tür için önceden böyle bir sınıfa sahip olabiliriz. 4-6 jenerik tür parametreleri Union sınıfı için tam olacaktır.

ContactInfoSınıfı yeniden yazalım:

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

Burada derleyici en az bir kurucu için geçersiz kılmayı isteyecektir. Yapıcıların geri kalanını geçersiz kılmayı unutursak, başka bir durumla ContactInfo sınıfının nesnesini oluşturamayız. Bu, Eşleştirme sırasında bizi çalışma zamanı istisnalarından koruyacaktır.

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

Bu kadar. Umarım eğlenmişsindir.

Eğlence ve kar için F # sitesinden alınan örnek

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.