C #의 차별적 인 결합


93

[참고 :이 질문의 원래 제목은 " 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;
    } 

내가 이루고 싶은 것이 가능한지 아는 사람 있나요? 아니면 처음에이 클래스를 작성하는 것이 어리석은 것일까 요?

미리 감사드립니다.


3
C의 조합을 사용하여 치형 C #으로 구현 될 수있다 StructLayout(LayoutKind.Explicit)FieldOffset. 물론 참조 유형으로는 수행 할 수 없습니다. 당신이하는 일은 전혀 C 연합과 같지 않습니다.
Brian

4
이를 종종 차별적 노조 라고합니다 .
Jeff Hardy

감사합니다 제프 -이 용어의 인식이었다 그러나 이것은 거의 정확하게 내가 달성하고자하는 것입니다
크리스 퓨 으렐

7
아마도 당신이 찾고있는 종류의 응답은 아니지만 F #을 고려해 보셨습니까? C #보다 유니온을 표현하기가 훨씬 쉬운 언어에서 바로 구워진 형식 안전 공용체 및 패턴 일치가 있습니다.
Juliet

1
구별 된 공용체의 또 다른 이름은 합계 유형입니다.
cdiggins 2011 년

답변:


114

위에 제공된 유형 검사 및 유형 캐스팅 솔루션이별로 마음에 들지 않으므로 잘못된 데이터 유형을 사용하려고하면 컴파일 오류가 발생하는 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

21
그리고 그 모든 상용구 코드가 당신을 실망 시킨다면, 대신 명시 적으로 사례를 태그하는이 구현을 시도해 볼 수 있습니다 : pastebin.com/EEdvVh2R . 참고로이 스타일은 F # 및 OCaml이 내부적으로 공용체를 나타내는 방식과 매우 유사합니다.
Juliet

4
Juliet의 짧은 코드가 마음에 들지만 유형이 <int, int, string>이면 어떨까요? 두 번째 생성자를 어떻게 호출할까요?
Robert Jeppesen

2
이게 어떻게 100 개의 찬성표가 없는지 모르겠습니다. 그것은 아름다움의 것입니다!
Paolo Falabella 2013-08-19

6
@nexus는 F #에서이 유형을 고려합니다.type Result = Success of int | Error of int
AlexFoxGill 2015

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 더 많은 승인을 받아야합니다. 나는 당신이 모든 종류의 arities의 결합을 허용하기에 충분히 유연하게 만든 방식을 좋아합니다.
Paul d' Aoust 2014 년

솔루션의 유연성과 간결성을 위해 +1하십시오. 그러나 나를 괴롭히는 몇 가지 세부 사항이 있습니다. : 나는 별도의 주석으로 각각 게시합니다
stakx - 더 이상 기여

1
1. 반영을 사용하면 근본적인 특성으로 인해 차별적 인 조합이 자주 사용되는 경우 일부 시나리오에서 너무 큰 성능 불이익이 발생할 수 있습니다.
stakx-더 이상

4
2.dynamic & 제네릭 의 사용 UnionBase<A>과 상속 체인은 불필요 해 보입니다. 확인 UnionBase<A>, 제네릭이 아닌을 복용 생성자를 죽이고 A, 그리고 만들 (어쨌든 어떤, 어떤 추가 혜택이 선언에있다 ). 그런 다음 에서 직접 각 클래스를 파생시킵니다 . 이는 적절한 방법 만 노출 된다는 장점 이 있습니다. (현재 상태 그대로, 예를 들어 포함 된 값이 . 이 아닌 경우 예외를 throw하도록 보장 되는 오버로드 를 노출합니다 . 그런 일이 발생해서는 안됩니다.)valueobjectdynamicUnion<…>UnionBaseMatch<T>(…)Union<A, B>Match<T>(Func<A, T> fa)A
stakx-

3
내 라이브러리 OneOf가 유용하다고 생각할 수 있습니다. 다소간이 작업을 수행하지만 Nuget에 있습니다. :) github.com/mcintyre321/OneOf
mcintyre321

20

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

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

  • 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 .

또한의 그 말을하자 CartStateActiveA가 들어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
        );
}    

이 코드는 완전히 형식이 안전합니다. 어디에서든 캐스팅이나 조건문이 없으며 빈 카트를 지불하려고하면 컴파일러 오류가 발생합니다.


흥미로운 사용 사례. 저에게있어 객체 ​​자체에 차별적 인 결합을 구현하는 것은 꽤 장황합니다. 다음은 모델을 기반으로 스위치 표현식을 사용하는 기능적 스타일 대안입니다. gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . "행복한"경로가 하나만있는 경우 DU가 실제로 필요하지 않다는 것을 알 수 있지만, 비즈니스 논리 규칙에 따라 메서드가 한 유형 또는 다른 유형을 반환 할 때 매우 유용합니다.
David Cuccia

13

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 (틱)"의 원래 제목을 돌이켜 보면 오해의 소지가있는 - 나는 더 나은 나의 목표는 무엇을 반영하기 위해 질문 제목을 변경 한
크리스 퓨 으렐

차별적 인 노조는 당신이하려는 일에 대해 훨씬 더 의미가 있습니다. 컴파일 시간을 확인하려면 .NET 4 및 코드 계약을 살펴 보겠습니다. 코드 계약을 사용하면 컴파일 타임 계약을 적용 할 수 있습니다. .Is <T> 연산자에 대한 요구 사항을 적용하는 요구 사항입니다.
jrista 2010 년

나는 여전히 일반적인 관행에서 연합의 사용에 의문을 제기해야한다고 생각합니다. C / C ++에서도 공용체는 위험하므로 매우주의해서 사용해야합니다. 왜 그런 구조를 C #으로 가져와야하는지 궁금합니다 ... 이로부터 얻는 가치는 무엇입니까?
jrista 2010 년

2
char foo = 'B';

bool bar = foo is int;

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


2

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

어떤 종류의 유형 안전성도 달성 할 수 없으며 달성 할 수 없으며 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

ValueWrapper를 제네릭으로 만드는 제안은 분명한 대답처럼 보이지만 내가하는 일에 문제가 발생합니다. 기본적으로 내 코드는 일부 텍스트 줄을 구문 분석하여 이러한 래퍼 개체를 만듭니다. 그래서 ValueWrapper MakeValueWrapper (string text)와 같은 메서드가 있습니다. 래퍼를 제네릭으로 만들면 MakeValueWrapper의 시그니처를 제네릭으로 변경해야합니다. 그러면 호출 코드가 예상되는 유형을 알아야하며 텍스트를 구문 분석하기 전에 미리 알지 못함을 의미합니다. ...
Chris Fewtrell

...하지만 마지막 댓글을 쓸 때도 내가하려는 것이 내가 만드는 것만 큼 어렵게 느껴지지 않기 때문에 내가 뭔가를 놓친 것 (또는 엉망이 된 것) 같은 느낌이 들었습니다. 다시 돌아가서 생성 된 래퍼에 대해 작업하는 데 몇 분을 소비하고 그 주위에 구문 분석 코드를 적용 할 수 있는지 확인합니다.
Chris Fewtrell

내가 제공 한 코드는 비즈니스 로직을위한 것입니다. 접근 방식의 문제는 컴파일 타임에 Union에 어떤 값이 저장되는지 알 수 없다는 것입니다. 즉, Union 개체에 액세스 할 때마다 if 또는 switch 문을 사용해야합니다. 이러한 개체는 공통 기능을 공유하지 않기 때문입니다! 코드에서 래퍼 객체를 어떻게 더 많이 사용 하시겠습니까? 또한 런타임에 일반 객체를 생성 할 수 있습니다 (느리지 만 가능함). 또 다른 쉬운 옵션은 편집 된 게시물에 있습니다.
Jaroslav Jandek

기본적으로 현재 코드에서 의미있는 컴파일 타임 유형 검사가 없습니다. 동적 개체 (런타임시 동적 유형 검사)를 시도 할 수도 있습니다.
Jaroslav Jandek

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


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;

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

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

초기화되지 않은 변수에 액세스하려는 시도가있는 경우 예외를 throw 할 수 있습니다. 즉, A 매개 변수로 생성 된 후 나중에 B 또는 C에 액세스하려고 시도하면 UnsupportedOperationException이 발생할 수 있습니다. 그래도 작동하려면 게터가 필요합니다.


예-제가 작성한 첫 번째 버전은 As 메서드에서 예외를 발생 시켰습니다. 그러나 이것은 확실히 코드의 문제를 강조하지만 런타임보다 컴파일 타임에 이에 대해 이야기하는 것을 선호합니다.
Chris Fewtrell


0

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

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


0

나는 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 # 에서 가져온 예

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.