Дизъюнктное объединение в C#

[Примечание: Этот вопрос имел оригинальное название "C (выход) объединение стиля в C#", но поскольку комментарий Jeff сообщил мне, по-видимому, эту структуру называют 'дизъюнктным объединением']

Извините многословие этого вопроса.

Существует несколько подобных звучащих вопросов уже взорвать в ТАК, но они, кажется, концентрируются на преимуществах сохранения памяти объединения или использования его для interop. Вот пример такого вопроса.

Мое требование иметь вещь типа объединения несколько отличается.

Я пишу некоторый код в данный момент, который генерирует объекты, которые немного походят на это

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

    /// 
    /// Returns true if the union contains a value of type T
    /// 
    /// The type of T must exactly match the type
    public bool Is()
    {
        return typeof(T) == type;
    }

    /// 
    /// Returns the union value cast to the given type.
    /// 
    /// If the type of T does not exactly match either X or Y, then the value default(T) is returned.
    public T As()
    {
        if(Is())
        {
            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())
        {
            return (T)(object)b; 
        }

        if(Is())
        {
            return (T)(object)c; 
        }

        return default(T);
    }
}

Используя этот класс ValueWrapper теперь похож на это

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union ValueA;
    public  Union ValueB;
}

который является чем-то как то, чего я хотел достигнуть, но я пропускаю один довольно ключевой элемент - который является компилятором осуществленный тип, проверяющий при вызове и Как функций, как демонстрирует следующий код

    public void DoSomething()
    {
        if(ValueA.Is())
        {
            var s = ValueA.As();
            // .... do somethng
        }

        if(ValueA.Is()) // I would really like this to be a compile error
        {
            char c = ValueA.As();
        }
    }

IMO Это не допустимо для выяснения у ValueA, если это - a char так как в его определении ясно говорится, что это не - это - программная ошибка, и я хотел бы, чтобы компилятор взял на этом. [Также, если бы я мог бы добраться, это исправляет затем (надо надеяться), я получил бы intellisense также - который был бы благом.]

Для достижения этого, я хотел бы сказать компилятор что тип T может быть один из A, B или C

    public bool Is() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 

У кого-либо есть какая-либо идея, если то, чего я хочу достигнуть, возможно? Или я просто глуп для записи этого класса во-первых?

Заранее спасибо.

83
задан Community 23 May 2017 в 11:33
поделиться

8 ответов

Мне не очень нравятся решения по проверке типов и приведению типов, представленные выше, поэтому вот 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);
            }
        }
    }
}
102
ответ дан 24 November 2019 в 08:49
поделиться

Вы можете экспортировать функцию сопоставления псевдошаблонов, как я использую для типа Either в моей библиотеке Sasa . В настоящее время существуют накладные расходы времени выполнения, но в конечном итоге я планирую добавить анализ CIL, чтобы встроить всех делегатов в истинный оператор case.

0
ответ дан 24 November 2019 в 08:49
поделиться

Вы можете генерировать исключения при попытке доступа к переменным, которые не были инициализированы, то есть, если они были созданы с параметром A, а затем произошла попытка доступа к B или C, это могло вызвать, скажем, исключение UnsupportedOperationException. Однако вам понадобится геттер, чтобы он работал.

1
ответ дан 24 November 2019 в 08:49
поделиться

Невозможно использовать именно тот синтаксис, который вы использовали, но с большей детализацией и копированием / вставкой легко заставить разрешение перегрузки сделать эту работу за вас:


// this code is ok
var u = new Union("");
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

// and this one will not compile
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

К настоящему времени должно быть довольно очевидно, как реализовать это:


    public class Union
    {
        private readonly Type type;
        public readonly A a;
        public readonly B b;
        public readonly C c;

        public Union(A a)
        {
            type = typeof(A);
            this.a = a;
        }

        public Union(B b)
        {
            type = typeof(B);
            this.b = b;
        }

        public Union(C c)
        {
            type = typeof(C);
            this.c = c;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(A) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(B) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(C) == type;
        }

        public A Value(GetValueTypeSelector _)
        {
            return a;
        }

        public B Value(GetValueTypeSelector _)
        {
            return b;
        }

        public C Value(GetValueTypeSelector _)
        {
            return c;
        }
    }

    public static class Is
    {
        public static TypeTestSelector OfType()
        {
            return null;
        }
    }

    public class TypeTestSelector
    {
    }

    public static class Get
    {
        public static GetValueTypeSelector ForType()
        {
            return null;
        }
    }

    public class GetValueTypeSelector
    {
    }

Нет никаких проверок для извлечения значения неправильного типа, например:


var u = Union(10);
string s = u.Value(Get.ForType());

Так что вы можете подумать о добавлении необходимых проверок и выбросить исключения в таких случаях.

0
ответ дан 24 November 2019 в 08:49
поделиться
char foo = 'B';

bool bar = foo is int;

Это приводит к предупреждению, а не к ошибке. Если вы ищете, чтобы ваши функции Is и As были аналогами операторов C #, то вам не следует ограничивать их таким образом.

2
ответ дан 24 November 2019 в 08:49
поделиться

Я не уверен, что полностью понимаю вашу цель. В языке C объединение - это структура, которая использует одни и те же области памяти для более чем одного поля. Например:

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

Объединение floatOrScalar может использоваться как float, так и int, но оба они занимают одно и то же пространство памяти. Изменение одного из них приводит к изменению другого. Того же можно добиться с помощью struct в C#:

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

Приведенная выше структура использует 32 бита, а не 64 бита. Это возможно только со структурой. Ваш пример выше - это класс, и, учитывая природу CLR, он не дает никаких гарантий относительно эффективности использования памяти. Если вы изменяете Union от одного типа к другому, вы не обязательно повторно используете память... скорее всего, вы выделяете новый тип на куче и бросаете другой указатель в поле backing object. В отличие от настоящего союза, ваш подход может привести к большему переполнению кучи, чем если бы вы не использовали тип Union.

6
ответ дан 24 November 2019 в 08:49
поделиться

Вот моя попытка. Он выполняет проверку типов во время компиляции с использованием общих ограничений типа.

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
ответ дан 24 November 2019 в 08:49
поделиться

Если вы разрешаете множественные типы, вы не можете достичь безопасности типов (если только типы не связаны).

Вы не можете и не сможете добиться никакой безопасности типов, вы можете добиться только безопасности байт-значений, используя FieldOffset.

Гораздо логичнее иметь общий ValueWrapper с T1 ValueA и T2 ValueB, ...

P.S.: говоря о безопасности типов, я имею в виду безопасность типов во время компиляции.

Если вам нужна обертка кода (выполнение бизнес-логики на модификациях, вы можете использовать что-то вроде:

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
2
ответ дан 24 November 2019 в 08:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: