C#の識別された組合
-
01-10-2019 - |
質問
注:この質問には元のタイトルがありました」c(ish)スタイルの組合c#「しかし、ジェフのコメントが私に知らせたように、どうやらこの構造は「差別された連合」と呼ばれているようです。
この質問の冗長性を許してください。
すでに私のSOに同様の響きの質問がいくつかありますが、彼らは組合の記憶保存の利点に集中しているように見えます。このような質問の例は次のとおりです.
組合タイプのものを持ちたいという私の欲求は、やや異なっています。
私はこのように見えるオブジェクトを生成する瞬間にいくつかのコードを書いています
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
かなり複雑なものはあなたが同意すると思います。事はそれです ValueA
いくつかの特定の種類しかありません(たとえば string
, int
と Foo
(これはクラスです)と 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つ見逃しています。
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
その定義は明らかにそうではないと言っているので - これはプログラミングエラーであり、コンパイラにこれを取り上げてほしい。 [もし私がこれを正しくすることができれば(うまくいけば)私もIntellisenseを得るだろう - これは恩恵になるだろう。
これを達成するために、私はコンパイラにタイプを伝えたいと思います 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;
}
私が達成したいことが可能かどうか、誰かが考えていますか?それとも、そもそもこのクラスを書くのはただ愚かですか?
前もって感謝します。
解決
上記のタイプチェックおよびタイプキャスティングソリューションはあまり気に入らないので、間違ったデータタイプを使用しようとするとコンピレーションエラーを投げる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つ以上の項目の組合には十分に拡張されません(たとえば、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));
}
}
私はこのテーマに関するいくつかのブログ投稿を書きました。
3つの州のショッピングカートシナリオがあるとしましょう:「空」、「アクティブ」、「有料」、それぞれが 違う 行動。
- あなたはaを作成します
ICartState
すべての状態が共通しているインターフェース(そして、それはただの空のマーカーインターフェイスである可能性があります) - そのインターフェイスを実装する3つのクラスを作成します。 (クラスは相続関係にある必要はありません)
- インターフェイスには「フォールド」メソッドが含まれており、それにより、各状態または処理する必要がある場合にラムダを渡します。
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
で AddItem
方法です いいえ によって実装されています CartStatePaid
.
そして、それを言ってみましょう 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
);
}
このコードは完全にタイプセーフであることに注意してください - キャストや条件はどこにもありません。また、空のカートの支払いを試みた場合はコンパイラエラーがあります。
これを行うためのライブラリを書いています https://github.com/mcintyre321/oneof
インストールパッケージOneof
それはdusを行うための一般的なタイプを持っています OneOf<T0, T1>
ずっとOneOf<T0, ..., T9>
. 。それらのそれぞれは .Match
, 、a .Switch
コンパイラセーフタイプの動作に使用できる声明、例:
```
OneOf<string, ColorName, Color> backgroundColor = getBackground();
Color c = backgroundColor.Match(
str => CssHelper.GetColorFromString(str),
name => new Color(name),
col => col
);
```
あなたの目標を完全に理解しているかどうかはわかりません。 Cでは、組合は複数のフィールドに同じメモリ位置を使用する構造です。例えば:
typedef union
{
float real;
int scalar;
} floatOrScalar;
floatOrScalar
ユニオンはフロートまたはINTとして使用できますが、どちらも同じメモリスペースを消費します。 1つを変更すると、他方が変更されます。 c#で構造体で同じことを達成できます。
[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
[FieldOffset(0)]
public float Real;
[FieldOffset(0)]
public int Scalar;
}
上記の構造は、64ビットではなく合計32ビットを使用します。これは、構造体でのみ可能です。上記の例はクラスであり、CLRの性質を考えると、メモリ効率については保証されません。変更した場合 Union<A, B, C>
あるタイプから別のタイプまで、あなたは必ずしもメモリを再利用しているわけではありません...おそらく、あなたはヒープに新しいタイプを割り当て、バッキングに別のポインターをドロップしています object
分野。に反対 本当の組合, 、あなたのアプローチは、あなたがあなたの組合タイプを使用しなかった場合、あなたがそうでなければ得られるよりも多くのヒープスラッシングを実際に引き起こす可能性があります。
char foo = 'B';
bool bar = foo is int;
これにより、エラーではなく警告が発生します。あなたがあなたを探しているなら Is
と As
C#オペレーターのアナログに機能するように機能すると、とにかくそのように制限されるべきではありません。
複数のタイプを許可する場合、タイプの安全を達成することはできません(タイプが関連していない限り)。
どんな種類のタイプの安全性を達成することもできません。フィールドオフセットを使用してByte-Value-Safetyのみを達成することができます。
ジェネリックを持っている方がはるかに理にかなっています ValueWrapper<T1, T2>
と T1 ValueA
と T2 ValueB
, ...
PS:タイプセーフティについて話すとき、私はコンパイル時間タイプセーフティを意味します。
コードラッパーが必要な場合(変更時にBussiness Logicを実行すると、次の行に沿って何かを使用できます。
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
これが私の試みです。一般的なタイプの制約を使用して、タイプの時間チェックをコンパイルします。
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つに把握させる方法はありませんか?)
それで、私はこの同じ問題に何度もヒットしましたが、私が望む構文を取得するソリューションを思いつきました(組合タイプの実装にある程度のugさを犠牲にして)。
要約するには、この種の使用がコールサイトで使用されたいと考えています。
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();
}
}
}
そして、最小限の拡張可能なソリューションを使用した私の試み 組合のネスト/いずれかのタイプ。また、一致方法でのデフォルトパラメーターの使用は、自然に「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);
}
}
}
}
初期化されていない変数にアクセスしようとする試みがあると、例外をスローすることができます。つまり、Aパラメーターで作成された場合、後でBまたはCにアクセスする試みがある場合、たとえばUnsupportedoperationExceptionを投げることができます。ただし、それを機能させるにはゲッターが必要です。
私のどちらのタイプにも使用しているように、擬似パターンマッチング関数をエクスポートできます SASAライブラリ. 。現在、ランタイムのオーバーヘッドがありますが、最終的にはすべての代表者を真のケースステートメントにインライン化するためにCIL分析を追加する予定です。
使用した構文を正確に行うことはできませんが、もう少し冗長性とコピー/ペーストで、過負荷解像度を簡単に行うことができます。
// 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());
そのため、そのような場合には、必要なチェックを追加し、例外をスローすることを検討する場合があります。
私はユニオンタイプの所有者を使用しています。
それを明確にするために例を考えてください。
連絡先クラスがあると想像してください:
public class Contact
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public string PostalAdrress { get; set; }
}
これらはすべて単純な文字列として定義されていますが、実際には文字列ですか?もちろん違います。名前は、名と姓で構成できます。それとも、電子メールはシンボルのセットですか?少なくとも @を含めるべきであり、必然的であることを知っています。
USドメインモデルを改善しましょう
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) { ... }
}
このクラスでは、作成中の検証となり、最終的には有効なモデルがあります。ペルソナームクラスのConsturctorは、同時にFirstNameとLastNameが必要です。これは、作成後、無効な状態を持つことができないことを意味します。
それぞれクラスに連絡します
public class Contact
{
public PersonalName Name { get; set; }
public EmailAdress EmailAddress { get; set; }
public PostalAddress PostalAddress { get; set; }
}
この場合、同じ問題があり、連絡先クラスのオブジェクトが無効な状態にある可能性があります。 emailAddressがあるかもしれませんが、名前はありません
var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };
それを修正して、PersonalName、EmailAddress、PostalAddressを必要とするコンストラクターを使用して連絡先クラスを作成しましょう。
public class Contact
{
public Contact(
PersonalName personalName,
EmailAddress emailAddress,
PostalAddress postalAddress
)
{
...
}
}
しかし、ここに別の問題があります。人がemailAdressしかなく、郵便路dressを持っていない場合はどうなりますか?
そこでそれについて考えると、有効な連絡先クラスオブジェクトの3つの可能性があることに気付きます。
- 連絡先にはメールアドレスのみがあります
- 連絡先には郵便住所のみがあります
- 連絡先にはメールアドレスと郵便アドレスの両方があります
ドメインモデルを書きましょう。最初に、上記のケースに対応する状態が連絡先情報クラスを作成します。
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クラスにマッチメソッドを追加しましょう
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つしかない可能性があるため、このコードを書くことができます。
補助クラスを作成して、毎回それほど多くのコードを書かないようにしましょう。
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");
}
}
Delegates FUNC、Actionで行われるように、いくつかのタイプでこのようなクラスを事前に使用できます。 4-6ユニオンクラスでは、4-6ジェネリック型パラメーターが完全になります。
書き直しましょう 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#楽しみと利益のため
C#言語デザインチームは、2017年1月に差別化された組合について議論しました https://github.com/dotnet/csharplang/blob/master/meetings/2017/ldm-2017-01-0.md#discriminated-unions-via-closed-types