108

[注: この質問の元のタイトルは「C# の C (ish) スタイル ユニオン」でしたが、Jeff のコメントからわかるように、この構造は「差別化されたユニオン」と呼ばれているようです]

この質問の冗長さを許してください。

すでに SO に似たような質問がいくつかありますが、それらはユニオンのメモリ節約の利点または相互運用のためにそれを使用することに集中しているようです。 そのような質問の例を次に示します。

ユニオンタイプのものを持ちたいという私の願望は、多少異なります。

現在、このようなオブジェクトを生成するコードを書いています。

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

    public object ValueA;
    public object ValueB;
}

かなり複雑なことですが、同意していただけると思います。問題は、それはいくつかの特定の型 (例えば, and (クラス) としValueAましょう) だけであり、別の小さな型のセットになる可能性があるということです。私はこれらの値をオブジェクトとして扱うのは好きではありません (私は、型安全性を考慮したコーディング)。 stringintFooValueB

そこで、ValueA が論理的に特定の型への参照であるという事実を表現するために、単純な小さなラッパー クラスを作成することを考えました。Union私が達成しようとしていることが 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);
    }
}

このクラスを使用すると ValueWrapper は次のようになります

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

これは私が達成したかったことのようなものですが、かなり重要な要素が 1 つ欠けています。それは、次のコードが示すように、Is および As 関数を呼び出すときにコンパイラが強制する型チェックです。

    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 が a であるかどうかを尋ねるのは有効ではありません。charその定義が明確にそうではないことを示しているためです。これはプログラミング エラーであり、コンパイラにこれを取り上げてもらいたいと思います。[また、これを正しく理解できれば、(願わくば) インテリセンスも取得できます。これは恩恵です。]

Tこれを実現するために、型が A、B、または 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;
    } 

私が達成したいことが可能かどうか、誰にも分かりますか? それとも、そもそもこのクラスを書いた私は単純に愚かでしょうか?

前もって感謝します。

4

15 に答える 15

124

上記の型チェックと型キャストのソリューションはあまり好きではないので、間違ったデータ型を使用しようとするとコンパイルエラーがスローされる100%タイプセーフなユニオンを次に示します。

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);
            }
        }
    }
}
于 2010-07-07T22:39:13.997 に答える
35

受け入れられたソリューションの方向性は気に入っていますが、3つを超えるアイテムのユニオンでは適切に拡張できません(たとえば、9つのアイテムのユニオンには9つのクラス定義が必要です)。

これは、コンパイル時に100%タイプセーフであるが、大規模なユニオンに成長するのが簡単な別のアプローチです。

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));
    }
}
于 2011-09-05T00:23:40.130 に答える
21

このテーマについて、役立つかもしれないいくつかのブログ投稿を書きました。

「空」、「アクティブ」、「支払い済み」の 3 つの状態があり、それぞれ動作が異なるショッピング カートのシナリオがあるとします。

  • ICartStateすべての状態に共通するインターフェースを作成します(そして、それは空のマーカーインターフェースである可能性があります)
  • そのインターフェイスを実装する 3 つのクラスを作成します。(クラスは継承関係にある必要はありません)
  • インターフェイスには「fold」メソッドが含まれています。これにより、処理する必要がある状態またはケースごとにラムダを渡します。

C# から F# ランタイムを使用することもできますが、より軽量な代替手段として、このようなコードを生成するための小さな T4 テンプレートを作成しました。

インターフェースは次のとおりです。

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

そして、実装は次のとおりです。

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

で実装されていないメソッドでand を拡張するCartStateEmptyCartStateActiveします。AddItemCartStatePaid

また、他の州にはない方法があるCartStateActiveとしましょう。Pay

次に、使用中のコードを示します。2 つのアイテムを追加して、カートの料金を支払います。

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

このコードは完全にタイプセーフであることに注意してください。たとえば、空のカートに支払いをしようとすると、どこにもキャストや条件がなく、コンパイラ エラーが発生します。

于 2012-11-02T18:12:51.073 に答える
7

あなたの目標を完全に理解しているかどうかはわかりません。C では、共用体は複数のフィールドに対して同じメモリ位置を使用する構造体です。例えば:

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

共用体はfloatOrScalarfloat または int として使用できますが、どちらも同じメモリ空間を消費します。一方を変更すると、他方が変更されます。C# の構造体を使用して同じことを実現できます。

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

上記の構造は、64 ビットではなく、合計 32 ビットを使用します。これは構造体でのみ可能です。上記の例はクラスであり、CLR の性質を考えると、メモリ効率については保証されません。Union<A, B, C>aをある型から別の型に変更する場合、必ずしもメモリを再利用しているわけではありません...ほとんどの場合、ヒープに新しい型を割り当て、バッキングobjectフィールドに別のポインターをドロップしています。実際の unionとは対照的に、あなたのアプローチは実際には Union 型を使用しなかった場合よりも多くのヒープ スラッシングを引き起こす可能性があります。

于 2010-06-30T17:45:02.337 に答える
2

複数のタイプを許可すると、タイプの安全性を実現できません(タイプが関連している場合を除く)。

いかなる種類の型安全性も達成できず、達成することもできません。FieldOffsetを使用してのみバイト値の安全性を達成できます。

と、..を使用したジェネリックValueWrapper<T1, T2>を使用する方がはるかに理にかなっています。T1 ValueAT2 ValueB

PS:型安全性について話すとき、私はコンパイル時の型安全性を意味します。

コードラッパーが必要な場合(変更時にビジネスロジックを実行するには、次の行に沿って何かを使用できます:

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

簡単な方法として使用できます(パフォーマンスの問題がありますが、非常に簡単です)。

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
于 2010-06-30T17:52:36.547 に答える
2

だから私はこれと同じ問題に何度も遭遇しました、そして私はちょうど私が望む構文を得る解決策を思いつきました(Union型の実装におけるいくらかの醜さを犠牲にして)。

要約すると、コールサイトでこの種の使用法が必要です。

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

ただし、次の例はコンパイルに失敗するため、型安全性が少し高くなります。

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

追加のクレジットとして、絶対に必要なスペースを超えないようにしましょう。

とはいえ、これが2つのジェネリック型パラメーターの実装です。3、4などの型パラメーターの実装は簡単です。

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();
        }
    }
}
于 2011-04-30T10:22:23.933 に答える
2

これが私の試みです。ジェネリック型制約を使用して、型のコンパイル時チェックを行います。

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

それはいくつかのきれいにすることができます。特に、As/Is/Set の型パラメーターを削除する方法がわかりませんでした (1 つの型パラメーターを指定して、C# にもう 1 つの型パラメーターを認識させる方法はありませんか?)。

于 2010-07-01T09:25:10.753 に答える
2
char foo = 'B';

bool bar = foo is int;

これにより、エラーではなく警告が発生します。Is関数と関数を C# 演算子の類似物にしたい場合Asは、とにかくそのように制限するべきではありません。

于 2010-06-30T17:39:30.637 に答える
1

初期化されていない変数にアクセスしようとすると、例外がスローされる可能性があります。つまり、変数が A パラメータで作成され、後で B または C にアクセスしようとすると、たとえば UnsupportedOperationException がスローされる可能性があります。ただし、それを機能させるにはゲッターが必要です。

于 2010-06-30T17:38:07.330 に答える
0

使用した構文を正確に使用することはできませんが、もう少し冗長にしてコピー/貼り付けを行うと、オーバーロードの解決を簡単に行うことができます。


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

今では、それを実装する方法はかなり明白なはずです:


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

間違った型の値を抽出するためのチェックはありません。


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

そのため、必要なチェックを追加して、そのような場合に例外をスローすることを検討してください。

于 2010-07-01T07:59:31.020 に答える
0

私がSasaライブラリのEitherタイプに使用するように、疑似パターンマッチング関数をエクスポートできます。現在、実行時のオーバーヘッドがありますが、最終的には CIL 分析を追加して、すべてのデリゲートを真の case ステートメントにインライン化する予定です。

于 2010-06-30T18:42:16.273 に答える