program story

C #의 차별적 결합

inputbox 2020. 9. 24. 07:49
반응형

C #의 차별적 결합


[참고 :이 질문에는 원래 제목 인 " C #의 C (ish) 스타일 결합 "이 있었지만 Jeff의 의견에 따르면이 구조를 '차별 결합'이라고합니다.]

이 질문의 장황함을 용서하십시오.

이미 SO에서 내 것과 비슷한 몇 가지 질문이 있지만 노조의 메모리 절약 이점이나 interop에 사용하는 데 집중하는 것 같습니다. 다음은 그러한 질문의 예입니다 .

유니온 타입의 것을 갖고 싶다는 욕망은 다소 다릅니다.

지금은 이와 비슷한 개체를 생성하는 코드를 작성 중입니다.

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

    public object ValueA;
    public object ValueB;
}

꽤 복잡한 것들은 당신이 동의 할 것이라고 생각합니다. 문제는 ValueA몇 가지 특정 유형 (예 : string, intFoo(클래스)) 일 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;
}

달성하고 싶었지만 상당히 중요한 요소가 누락되었습니다. 즉, 다음 코드에서 보여 주듯이 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에게 그것이 char정의되어 있지 않다고 분명히 말하고 있기 때문에 ValueA에게 묻는 것은 유효하지 않습니다. 이것은 프로그래밍 오류이며 컴파일러가 이것을 선택하기를 바랍니다. [또한 내가 이것을 맞출 수 있다면 (희망적으로) 나도 지능을 얻게 될 것입니다. 이것은 이익이 될 것입니다.]

이를 달성하기 위해 컴파일러에게 유형 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));
    }
}

이 주제에 대한 유용한 블로그 게시물을 작성했습니다.

"빈", "활성"및 "유료"의 세 가지 상태가 각각 다른 동작을 갖는 장바구니 시나리오가 있다고 가정 해 보겠습니다 .

  • ICartState모든 상태가 공통적으로 갖는 인터페이스를 생성 합니다 (빈 마커 인터페이스 일 수 있음).
  • 해당 인터페이스를 구현하는 세 개의 클래스를 만듭니다. (클래스가 상속 관계에있을 필요는 없습니다)
  • 인터페이스에는 처리해야하는 각 상태 또는 케이스에 대해 람다를 전달하는 "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);
  }
}

이제 당신이 확장 가정 해 봅시다 CartStateEmptyCartStateActiveAddItem되는 방법 하지 에 의해 구현 CartStatePaid.

또한 다른 주 CartStateActive에는없는 Pay방법이 있다고 가정 해 봅시다 .

그런 다음 사용 중임을 보여주는 코드가 있습니다. 두 개의 항목을 추가 한 다음 카트 비용을 지불합니다.

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

그것은 하위 사용자 예를 수행하는 거기에 일반적인 유형이 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
);

```


나는 당신의 목표를 완전히 이해하고 있는지 잘 모르겠습니다. 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 유형을 사용하지 않았을 때 얻을 수있는 것보다 더 많은 힙 스 래싱을 유발할 수 있습니다.


char foo = 'B';

bool bar = foo is int;

이로 인해 오류가 아닌 경고가 발생합니다. 당신 IsAs함수가 C # 연산자의 유사체가 될 것을 찾고 있다면 , 어떻게 든 그런 식으로 제한해서는 안됩니다.


여러 유형을 허용하면 유형 안전을 달성 할 수 없습니다 (유형이 관련되지 않는 한).

어떤 종류의 유형 안전성도 달성 할 수 없으며 달성 할 수 없으며 FieldOffset을 사용하여 바이트 값 안전성 만 달성 할 수 있습니다.

, ... ValueWrapper<T1, T2>와 함께 제네릭을 사용 하는 것이 훨씬 더 합리적입니다 .T1 ValueAT2 ValueB

추신 : 유형 안전성에 대해 이야기 할 때 컴파일 타임 유형 안전성을 의미합니다.

코드 래퍼가 필요한 경우 (수정시 비즈니스 로직을 수행하면 다음과 같은 내용을 사용할 수 있습니다.

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에 대한 형식 매개 변수를 제거하는 방법을 알 수 없었습니다 (하나의 형식 매개 변수를 지정하고 C #이 다른 매개 변수를 파악하도록하는 방법이 없습니까?).


그래서 저는이 같은 문제를 여러 번 겪었고, 제가 원하는 구문을 얻을 수있는 해결책을 찾았습니다 (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;

추가 크레딧을 위해 절대적으로 필요한 것보다 더 많은 공간을 차지하지 않도록합시다.

여기에 두 가지 일반 유형 매개 변수에 대한 구현이 있습니다. 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();
        }
    }
}

그리고 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);
            }
        }
    }
}

초기화되지 않은 변수에 액세스하려는 시도가 있으면 예외를 throw 할 수 있습니다. 예를 들어 A 매개 변수로 만든 후 나중에 B 또는 C에 액세스하려고하면 UnsupportedOperationException이 throw 될 수 있습니다. 그래도 작동하려면 게터가 필요합니다.


Sasa 라이브러리 의 Either 유형에 사용하는 것처럼 의사 패턴 일치 함수를 내보낼 수 있습니다 . 현재 런타임 오버 헤드가 있지만 결국 모든 대리자를 실제 case 문에 인라인하기 위해 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());

따라서 이러한 경우 필요한 검사를 추가하고 예외를 throw하는 것을 고려할 수 있습니다.


나는 Union Type의 자체를 사용합니다.

더 명확하게하기위한 예를 고려하십시오.

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 클래스의 생성자는 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 클래스 객체의 유효한 상태에 대한 세 가지 가능성이 있음을 알 수 있습니다.

  1. 연락처에는 이메일 주소 만 있습니다.
  2. 연락처에는 우편 주소 만 있습니다.
  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 클래스에 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)
        {
             ...
        } 
        ...
   }
}

연락처 클래스의 상태는 생성자로 제어되고 가능한 상태 중 하나만 가질 수 있으므로 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) { }
}

여기서 컴파일러는 하나 이상의 생성자에 대한 재정의를 요청합니다. 나머지 생성자를 재정의하는 것을 잊으면 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-10.md#discriminated-unions-via-closed-types

You can vote for the feature request at https://github.com/dotnet/csharplang/issues/113

참고URL : https://stackoverflow.com/questions/3151702/discriminated-union-in-c-sharp

반응형