C#の差別化された組合


92

[注:この質問の元のタイトルは「C#でのC(ish)スタイルの共用体」でしたが、Jeffのコメントで通知されたように、この構造は明らかに「差別化された共用体」と呼ばれます]

この質問の冗長さをすみません。

すでにSOで採掘している同様の適切な質問がいくつかありますが、それらは組合のメモリ節約の利点または相互運用にそれを使用することに集中しているようです。 ここにそのような質問の例があります

ユニオン型のものを作りたいという私の願望は少し異なります。

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

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

    public object ValueA;
    public object ValueB;
}

かなり複雑なものだと思います。ことはValueA、いくつかの特定のタイプ(たとえばstringintFoo(これはクラスです)にValueBすることができ、別の小さなタイプのセットにすることができます。これらの値をオブジェクトとして扱うのは好きではありません(暖かくて心地よい感じが欲しい)タイプセーフのビットでコーディング)。

そのため、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 charその定義で明らかにそうではないため、ValueAがaであるかどうかを尋ねることは有効ではありません。これはプログラミングエラーであり、コンパイラーがこれを取得できるようにしたいと考えています。[また、これを正しく取得できれば(うまくいけば)インテリセンスも得られるでしょう-これは朗報です。]

これを達成するために、タイプ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;
    } 

私が達成したいことが可能である場合、誰かが何か考えを持っていますか?それとも、このクラスを最初から書いているのは私だけなのでしょうか?

前もって感謝します。


3
Cの共用体は、StructLayout(LayoutKind.Explicit)およびを使用して、値型のC#で実装できますFieldOffset。もちろん、これは参照型では実行できません。あなたがやっていることは、C連合のようなものではありません。
Brian

4
これはしばしば差別された組合と呼ばれます。
ジェフハーディ2010年

ありがとうジェフ-私はこの用語に気づいていませんでしたが、これはまさに私が達成したいことです
Chris Fewtrell

7
おそらくあなたが探しているような応答ではありませんが、F#を検討しましたか?タイプセーフなユニオンとパターンマッチングが言語に合わせて組み込まれており、C#よりもユニオンを表現するのがはるかに簡単です。
ジュリエット2010年

1
識別された共用体の別の名前は、合計タイプです。
cdiggins '09

回答:


113

上記の型チェックと型キャストのソリューションはあまり好きではないので、間違ったデータ型を使用しようとするとコンパイルエラーがスローされる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);
            }
        }
    }
}

3
うん、タイプセーフな区別のある共用​​体が必要な場合はmatch、が必要です。これは、他と同じくらい良い方法です。
Pavel Minaev 2010

20
そして、すべてのボイラープレートコードで問題が解決した場合は、代わりにケースを明示的にタグ付けする、pastebin.com / EEdvVh2Rの実装を試すことができます。ちなみに、このスタイルはF#とOCamlが内部で共用体を表す方法と非常によく似ています。
ジュリエット、

4
ジュリエットの短いコードが好きですが、型が<int、int、string>の場合はどうなりますか?2番目のコンストラクターをどのように呼び出しますか?
Robert Jeppesen

2
どうしてこれが100の賛成票を持っていないのかわかりません。美しさです!
Paolo Falabella 2013

5
@nexusはF#で、このタイプを考慮してくださいtype Result = Success of int | Error of int
AlexFoxGill

33

受け入れられたソリューションの方向性は好きですが、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));
    }
}

+1これはより多くの承認を得るべきです。私は、あらゆる種類のアリティの組合を可能にするのに十分な柔軟性を持たせた方法が好きです。
Paul d'Aoust 2014

+1は、ソリューションの柔軟性と簡潔さを実現します。しかし、私が気になるいくつかの詳細があります。:私は別のコメントとしてそれぞれを投稿します
stakx -もはや貢献

1
1.リフレクションを使用すると、差別化された共用体がその基本的な性質により非常に頻繁に使用される可能性があるため、一部のシナリオではパフォーマンスが大幅に低下する可能性があります。
stakx-2015年

4
2.継承チェーンでのdynamic&ジェネリックの使用はUnionBase<A>不要のようです。確認しUnionBase<A>、非ジェネリック取るコンストラクタを殺しA、そして作る(それはとにかくである。無添加の利点は、それを宣言してあります)。次に、各クラスをから直接派生させます。これには、適切なメソッドのみが公開されるという利点があります。(現在のように、たとえば、囲まれた値がでない場合に例外をスローすることが保証されているオーバーロードを公開します。それは発生しません。)valueobjectdynamicUnion<…>UnionBaseMatch<T>(…)Union<A, B>Match<T>(Func<A, T> fa)A
stakx-

3
私のライブラリOneOfは役に立つかもしれませんが、多かれ少なかれこれを行いますが、Nugetにあります:) github.com/mcintyre321/OneOf
mcintyre321

20

私はこの主題について役立つかもしれないいくつかのブログ投稿を書きました:

「空」、「アクティブ」、「有料」の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);
  }
}

では、CartStateEmptyおよびによって実装されていないメソッドでおよびCartStateActiveを拡張するとします。AddItemCartStatePaid

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

次に、使用中のコードを2つ示します。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
        );
}    

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


興味深いユースケース。私にとって、オブジェクト自体に識別された共用体を実装することはかなり冗長になります。モデルに基づいて、スイッチ式を使用する関数形式の代替案を次に示します:gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866。「幸せな」パスが1つしかない場合、DUは必ずしも必要ではないことがわかりますが、ビジネスロジックルールに応じて、メソッドがあるタイプまたは別のタイプを返す場合に非常に役立ちます。
David Cuccia

12

これを行うためのライブラリをhttps://github.com/mcintyre321/OneOfで作成しました

インストールパッケージOneOf

これは、例えば川下を行うため、その中にジェネリック型持ちOneOf<T0, T1>のすべての方法を OneOf<T0, ..., T9>。これらのそれぞれには、コンパイラの安全な型付き動作に使用できる.Match.Switchステートメントがあります。例:

「」

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

「」


7

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

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

floatOrScalar労働組合は、フロート、またはint型として使用することができますが、それらは両方とも同じメモリ空間を消費します。一方を変更すると、もう一方も変更されます。C#の構造体でも同じことができます。

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

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


私の質問で述べたように、私の動機はメモリ効率の向上ではありませんでした。私の目標がよりよく反映されるように質問のタイトルを変更しました。「C(ish)union」の元のタイトルは誤解を招きやすい
Chris Fewtrell

差別された労働組合は、あなたが何をしようとしているのかについて、ずっとずっと理にかなっています。コンパイル時にチェックされるようにすることについて...私は.NET 4とコード契約を調べます。コードコントラクトを使用すると、.Is <T>演算子に対する要件を適用するコンパイル時のContract.Requiresを適用できる場合があります。
jrista 2010年

私はまだ、一般的には連合の使用について疑問を抱く必要があると思います。C / C ++でさえ、共用体は危険なものであり、細心の注意を払って使用する必要があります。なぜそのような構造をC#に組み込む必要があるのか​​、私は興味があります...
jrista

2
char foo = 'B';

bool bar = foo is int;

これはエラーではなく警告になります。あなたが探している場合IsAsC#の事業者のための類似すべき機能、あなたはとにかくそのようにそれらを制限すべきではありません。


2

複数の型を許可すると、(型が関連付けられていない限り)タイプセーフを実現できません。

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

and 、...でジェネリック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

ValueWrapperをジェネリックにするというあなたの提案は明白な答えのように思えますが、それは私がしていることに問題を引き起こします。基本的に、私のコードは、テキスト行を解析してこれらのラッパーオブジェクトを作成しています。したがって、ValueWrapper MakeValueWrapper(string text)のようなメソッドがあります。ラッパーをジェネリックにした場合、MakeValueWrapperのシグネチャをジェネリックに変更する必要があります。これは、呼び出し側のコードが期待されるタイプを知る必要があることを意味し、テキストを解析する前にこれを事前に知らないだけです。 ...
Chris Fewtrell、2010年

...しかし、最後のコメントを書いているときでも、何かを見逃している(または何かをめちゃくちゃにしている)ように感じました。戻って、数分かけて汎用ラッパーの作業を行い、その周りの解析コードを適応できるかどうかを確認します。
Chris Fewtrell、2010年

私が提供したコードは、ビジネスロジックのためだけのものであることになっています。アプローチの問題は、コンパイル時にどの値がUnionに格納されるかを決して知らないことです。つまり、Unionオブジェクトにアクセスするときは、ifまたはswitchステートメントを使用する必要があります。これらのオブジェクトは共通の機能を共有していないためです。コードでラッパーオブジェクトをどのように使用しますか?また、実行時に汎用オブジェクトを構築することもできます(低速ですが、可能です)。別の簡単なオプションは、編集した投稿にあります。
Jaroslav Jandek 2010年

現時点では、コード内に意味のあるコンパイル時の型チェックはありません。動的オブジェクト(実行時の動的型チェック)を試すこともできます。
Jaroslav Jandek 2010

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つの型パラメーターを計算させる方法はありませんか?)


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

2

そして、Union / Either型の入れ子を使用した最小限の拡張可能なソリューションへの私の試み。また、Matchメソッドでデフォルトパラメータを使用すると、「Xまたはデフォルトのどちらか」のシナリオが自然に有効になります。

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

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


はい-私が書いた最初のバージョンはAsメソッドで例外を発生させました-しかし、これは確かにコードの問題を浮き彫りにしますが、実行時よりもコンパイル時にこれについて通知されることを好みます。
Chris Fewtrell、2010年

0

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


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

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


0

ユニオンタイプを使用しています。

わかりやすくするために例を考えます。

Contactクラスがあるとします。

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

これらはすべて単純な文字列として定義されていますが、実際には単なる文字列ですか?もちろん違います。名前は、名と姓で構成できます。または、メールは単なる記号のセットですか?少なくとも@を含める必要があることは知っています。

ドメインモデルを改善しましょう

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

このクラスには、作成中の検証が含まれ、最終的には有効なモデルが作成されます。PersonaNameクラスのConsturctorには、FirstNameとLastNameが同時に必要です。つまり、作成後は無効な状態になることはありません。

それぞれ連絡先クラス

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

この場合、同じ問題があります。Contactクラスのオブジェクトが無効な状態である可能性があります。つまり、EmailAddressはあるが名前は付いていない可能性があります

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

それを修正して、PersonalName、EmailAddress、PostalAddressを必要とするコンストラクターでContactクラスを作成しましょう。

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

しかし、ここには別の問題があります。PersonにEmailAdressのみがあり、PostalAddressがない場合はどうなりますか?

そこで考えると、Contactクラスオブジェクトの有効な状態には3つの可能性があることがわかります。

  1. 連絡先にはメールアドレスしかありません
  2. 連絡先には住所のみがある
  3. 連絡先にメールアドレスと住所の両方がある

ドメインモデルを書きましょう。まず、上記のケースに対応する状態のContact Infoクラスを作成します。

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

そして、連絡先クラス:

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

それを使ってみましょう:

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クラスにMatchメソッドを追加しましょう

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

連絡方法クラスの状態はコンストラクターで制御され、可能な状態の1つしか持たない可能性があるため、matchメソッドでこのコードを記述できます。

毎回それほど多くのコードを書かないように、補助クラスを作成しましょう。

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

デリゲートFunc、Actionで行われるように、このようなクラスをいくつかのタイプに対して事前に持つことができます。4-6ジェネリック型パラメーターは、Unionクラスで完全になります。

ContactInfoクラスを書き換えましょう:

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

ここで、コンパイラーは少なくとも1つのコンストラクターのオーバーライドを要求します。残りのコンストラクターをオーバーライドするのを忘れた場合、別の状態でContactInfoクラスのオブジェクトを作成できません。これにより、マッチング中のランタイム例外から保護されます。

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

それで全部です。楽しんでいただけましたか。

楽しさと利益のためにサイトF#から取得した例


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.