С# generics: тип родового типа для типа значения

У меня есть общий класс, который сохраняет значение для указанного типа T. Значение может быть int, uint, double или float. Теперь я хочу получить байты значения для его кодирования в конкретный протокол. Поэтому я хочу использовать метод BitConverter.GetBytes(), но, к сожалению, Bitconverter не поддерживает общие типы или неопределенные объекты. Вот почему я хочу передать значение и вызвать конкретную перегрузку GetBytes(). Мой вопрос: как я могу использовать общее значение для int, double или float? Это не работает:

public class GenericClass<T>
    where T : struct
{
    T _value;

    public void SetValue(T value)
    {
        this._value = value;
    }

    public byte[] GetBytes()
    {
        //int x = (int)this._value;
        if(typeof(T) == typeof(int))
        {
            return BitConverter.GetBytes((int)this._value);
        }
        else if (typeof(T) == typeof(double))
        {
            return BitConverter.GetBytes((double)this._value);
        }
        else if (typeof(T) == typeof(float))
        {
            return BitConverter.GetBytes((float)this._value);
        }
    }
}

Есть ли возможность использовать общую стоимость? Или есть другой способ получить байты?

Ответ 1

Во-первых, это действительно плохой запах кода. Каждый раз, когда вы выполняете тест типа в параметре типа, подобном этому, хорошо, вы злоупотребляете дженериками.

Компилятор С# знает, что вы злоупотребляете генериками таким образом и запрещаете приведение из значения типа T в int и т.д. Вы можете отключить компилятор на своем пути, выставив значение объекту перед тем, как передать его в int:

return BitConverter.GetBytes((int)(object)this._value);

Тьфу. Опять же, было бы лучше найти другой способ сделать это. Например:

public class NumericValue
{
    double value;
    enum SerializationType { Int, UInt, Double, Float };
    SerializationType serializationType;        

    public void SetValue(int value)
    {
        this.value = value;
        this.serializationType = SerializationType.Int
    }
    ... etc ...

    public byte[] GetBytes()
    {
        switch(this.serializationType)
        {
            case SerializationType.Int:
                return BitConverter.GetBytes((int)this.value);
            ... etc ...

Никаких дженериков не требуется. Резервные дженерики для ситуаций, которые на самом деле являются общими. Если вы написали код четыре раза по одному для каждого типа, вы ничего не получили с дженериками.

Ответ 2

Ну, мне кажется, что тип действительно не является должным образом общим для начала: он может быть только одним из нескольких типов, и вы не можете выразить это ограничение.

Затем вы хотите вызвать другую перегрузку GetBytes на основе типа T Дженерики не работают хорошо для такого рода вещей. Вы можете использовать динамическую типизацию для достижения этой цели в.NET 4 и выше:

public byte[] GetBytes()
{
    return BitConverter.GetBytes((dynamic) _value);
}

... но опять же это не похоже на приятный дизайн.

Ответ 3

Вы могли бы использовать Convert.ToInt32(this._value) или (int)((object)this._value). Но в целом, если вам приходится проверять определенные типы в общем методе, возникает проблема с вашим дизайном.

В вашем случае вам, вероятно, следует рассмотреть возможность создания абстрактного базового класса, а затем производных классов для типов, которые вы собираетесь использовать:

public abstract class GenericClass<T>
where T : struct
{
    protected T _value;

    public void SetValue(T value)
    {
        this._value = value;
    }

    public abstract byte[] GetBytes();
}

public class IntGenericClass: GenericClass<int>
{
    public override byte[] GetBytes()
    {
        return BitConverter.GetBytes(this._value);
    }
}

Ответ 4

Если ваша единственная цель - добавить метод GetBytes к этим типам, не намного ли лучше добавить их в качестве методов расширения, например:

public static class MyExtensions {
    public static byte[] GetBytes(this int value) {
        return BitConverter.GetBytes(value) ;
    }
    public static byte[] GetBytes(this uint value) {
        return BitConverter.GetBytes(value) ;
    }
    public static byte[] GetBytes(this double value) {
        return BitConverter.GetBytes(value) ;
    }
    public static byte[] GetBytes(this float value) {
        return BitConverter.GetBytes(value) ;
    }
}

Если вам действительно нужен универсальный класс для других целей, просто выполните грязный "двойной тип", например, Эрик упомянул, где вы сначала указываете значение объекта.

Ответ 5

Что сделал GenericClass<DateTime>? Скорее, кажется, у вас есть дискретный набор классов, которые знают, как получить свои байты, поэтому создайте абстрактный базовый класс, который выполняет всю общую работу, а затем создайте 3 конкретных класса, которые переопределяют метод для указания фрагмента, который изменяется между их:

public abstract class GenericClass<T>
{
    private T _value;

    public void SetValue(T value)
    {
        _value = value;
    }

    public byte[] GetBytes()
    {
        return GetBytesInternal(_value);
    }

    protected abstract byte[] GetBytesInternal(T value);
}

public class IntClass : GenericClass<int>
{
    protected override byte[] GetBytesInternal(int value)
    {
        return BitConverter.GetBytes(value);
    }
}

public class DoubleClass : GenericClass<double>
{
    protected override byte[] GetBytesInternal(double value)
    {
        return BitConverter.GetBytes(value);
    }
}

public class FloatClass : GenericClass<float>
{
    protected override byte[] GetBytesInternal(float value)
    {
        return BitConverter.GetBytes(value);
    }
}

Это не только обеспечивает чистые, строго типизированные реализации трех известных типов, но оставляет дверь открытой для кого-либо еще для подкласса Generic<T> и обеспечивает соответствующую реализацию GetBytes.

Ответ 6

Довольно поздний ответ, но в любом случае... есть способ сделать его немного приятнее... Использовать дженерики таким образом: Внедрить еще один универсальный тип, который преобразует типы для вас. Поэтому вам не нужно заботиться о распаковке, кастинге и т.д. Типа для объекта... он будет работать.

Кроме того, в вашем GenericClass теперь вам не нужно переключать типы, вы можете просто использовать IValueConverter<T> а также использовать его as IValueConverter<T>. Таким образом, generics будут делать магию для вас, чтобы найти правильную реализацию интерфейса, и, кроме того, объект будет null, если T - это то, что вы не поддерживаете...

interface IValueConverter<T> where T : struct
{
    byte[] FromValue(T value);
}

class ValueConverter:
    IValueConverter<int>,
    IValueConverter<double>,
    IValueConverter<float>
{
    byte[] IValueConverter<int>.FromValue(int value)
    {
        return BitConverter.GetBytes(value);
    }

    byte[] IValueConverter<double>.FromValue(double value)
    {
        return BitConverter.GetBytes(value);
    }

    byte[] IValueConverter<float>.FromValue(float value)
    {
        return BitConverter.GetBytes(value);
    }
}

public class GenericClass<T> where T : struct
{
    T _value;

    IValueConverter<T> converter = new ValueConverter() as IValueConverter<T>;

    public void SetValue(T value)
    {
        this._value = value;
    }

    public byte[] GetBytes()
    {
        if (converter == null)
        {
            throw new InvalidOperationException("Unsuported type");
        }

        return converter.FromValue(this._value);
    }
}

Ответ 7

Поздно к партии, но просто хотел прокомментировать комментарии, сказав, что первоначальное предложение было "плохим дизайном" - на мой взгляд, оригинальное предложение (хотя оно не работает) не было "обязательно" плохим дизайном вообще !

Исходя из сильного фона C++ (03/11/14) с глубоким пониманием метапрограмм шаблона, я создал типовую библиотеку сериализации типа в C++ 11 с минимальным повторением кода (цель состоит в том, чтобы иметь неповторяющийся код, и я считаю, что я достиг 99% этого). Компонент метаобработки шаблона времени компиляции, предоставленный C++ 11, хотя может стать чрезвычайно сложным, помогает достичь типичной реализации библиотеки сериализации.

Тем не менее, очень жаль, что, когда я хотел реализовать более простую структуру сериализации в С#, я застрял точно на проблеме, которую опубликовал OP. В C++ тип шаблона T может быть полностью "перенаправлен" на сайт использования, а генераторы С# не перенаправляют фактический тип времени компиляции на сайт использования - любую вторую (или более) ссылку на общий тип T делает T отличным типом, который вообще не используется на фактическом сайте использования, поэтому GetBytes (T) не может определить, что он должен ссылаться на определенную типизированную перегрузку - хуже, в С# даже нет хорошего способа сказать: hey, я знаете, что Т - это int, и если компилятор этого не знает, значит ли он (int) T "сделать int?

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

Стоит также отметить, что я действительно виноват в том, что класс BitConverter разработан традиционным и некомпетентным способом удовлетворения общих потребностей: вместо определения типа конкретного метода для каждого типа в отношении "GetBytes", возможно, это будет более общий чтобы определить общую версию GetBytes (значение T) - возможно, с некоторыми ограничениями, поэтому пользовательский тип T может быть перенаправлен и работать как ожидалось без переключения любого типа! То же самое верно для всех методов ToBool/ToXxx - если инфраструктура.NET предоставляет средства как не общие версии, как можно ожидать, что универсальная структура пытается использовать этот базовый коммутатор типа основы или если без переключателя типа вы заканчиваете дублируя логику кода для каждого типа данных, который вы пытаетесь сериализовать. О, я пропустил день, когда работал с C++ TMP, что я только пишу логику сериализации один раз для практически неограниченного количества типов, которые я могу поддерживать.