[c#] 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 .

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

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


답변

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 과 는 달리 , 접근 방식은 실제로 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