C#泛型:将泛型类型转换为值类型

18

我有一个通用类,可以保存指定类型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);
        }
    }
}

有可能转换一个通用值吗? 或者还有其他方法获取字节吗?

如果您知道值将是数字,那么是否可以使用最低公共数字类型而不是通用类型? - Facio Ratio
9个回答

28

首先,这是一种非常糟糕的代码味道。每当您对类型参数进行类型测试时,很有可能滥用泛型。

C#编译器知道您以这种方式滥用泛型,并禁止从类型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 ...

不需要泛型。将泛型保留给实际上是“通用”的情况。如果您已经为每种类型编写了代码四次,那么使用泛型就没有任何好处。


我的目标是编写一个类来保存2D矩阵(2D数组)。该类具有添加行和列的方法。我使用List<List<TYPE>>字段处理数据,以便可以轻松地扩展行数和列数,而不必每次创建新的2D数组。此外,该类应该有一个将数据导出到一个字节数组的方法,其中保存了每个字段的字节。因此,我需要BitConverter。但是我不想编写重复的代码。为什么没有GetBytes<T>()方法呢? - rittergig
3
考虑编写一个不可变矩阵来进行数学运算。当你将3加到数字2上时,你不会改变数字2成为5,而是创建一个全新的数字5,而数字2仍然存在。矩阵也可以如此。 - Eric Lippert

12

虽然来晚了,但我想就评论中说原始提案是“不好的设计”的观点发表一下我的看法 - 在我看来,原始提案(尽管它无法工作)并不是一个“必然”是一个糟糕的设计!

我有着扎实的C ++(03/11/14)背景和深入理解模板元编程,在C ++ 11中创建了一个类型通用的序列化库,代码重复最少(目标是具有非重复性代码,我相信我已经达到了99%)。 C ++ 11提供的编译时模板元编程设施虽然可以变得非常复杂,但有助于实现真正的类型通用序列化库。

但是,非常不幸的是,当我想在C#中实现更简单的序列化框架时,我正好遇到了OP发布的问题。在C ++中,模板类型T可以完全“转发”到使用站点,而C#泛型不会将实际编译时类型转发到使用站点 - 对泛型类型T的第二个(或更多)级引用使T成为不可在实际使用站点上使用的不同类型,因此GetBytes(T)无法确定它应该调用特定的类型化重载 - 更糟糕的是,在C#中甚至没有很好的方法来说:嘿,我知道T是int,如果编译器不知道它,是否“(int) T”使它成为int?

此外,与其责备基于类型的开关具有糟糕设计的味道 - 这是一个巨大的误称,每当人们正在进行一些高级基于类型的框架,并且由于语言环境的无能而不得不求助于基于类型的开关时,而不真正了解实际问题的限制时,人们开始公然说基于类型的开关是一个糟糕的设计 - 对于大多数传统的OOP使用情况,它确实如此,但有特殊情况,大多数时间高级用法案例就像我们在这里讨论的问题一样,这是必要的。

值得一提的是,我实际上会指责BitConverter类以传统和不称职的方式设计以适应通用需求:与其针对“GetBytes”定义每个类型的特定方法,也许更加通用友好的是定义一个泛型版本的GetBytes(T value) - 可能带有一些约束条件,因此用户泛型类型T可以被转发并按预期工作,而无需进行任何类型开关!同样适用于所有ToBool / ToXxx方法 - 如果.NET框架提供了非泛型版本的设施,那么一个尝试利用这个基础框架的通用框架会如何期望 - 类型开关或者如果没有类型开关,您最终会为每个要序列化的数据类型复制代码逻辑 - 哦,我想念我使用C ++ TMP工作的日子,我只需为实际上可以支持的无限数量的类型编写一次序列化逻辑。


1
同意这是一个有趣的问题,即“在C#中没有好的方法来表达:嘿,我知道T是int...”。解决方案是使用Unsafe.As()库方法。虽然它可以工作,但出于某种原因仍然是“不安全”的,因此需要谨慎使用。 - Serge Pavlov

10
很晚回答,但是有一个方法可以让它变得更好一些...利用泛型来实现:实现另一个泛型类型来为你转换类型。所以你不需要关心将类型拆箱、转型等操作转换为对象...它会自然而然地工作。
此外,在你的 GenericClass 中,现在你不需要切换类型,你只需要使用 IValueConverter<T> 并将其强制转换为 as IValueConverter<T>。这样,泛型就可以为您找到正确的接口实现,并且如果 T 是您不支持的内容,则该对象将为 null...
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);
    }
}

“converter” 可以是静态的。我也会考虑使用 static Action<T,byte[]> convert = ...,并使用静态初始化器来定位具有匹配参数类型的 BitConverter.GetBytes 方法。不确定这是否会真正减少运行时开销。 - Jeremy Lakeman
@MichaC,你的解决方案是我迄今为止找到的最高性能解决方案。谢谢你 :) - tcwicks

10

我认为这种类型并不是真正的通用,因为它只能是几种类型之一,而你无法表达这种约束。

然后,你想根据T的类型调用GetBytes的不同重载。泛型对于这种事情并不奏效。你可以在.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);
    }
}

3
如果你的唯一目标是将 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) ;
    }
}

如果您确实需要将通用类用于其他目的,只需像Eric提到的那样进行"双重类型转换"即可,其中首先将值强制转换为对象。

3

我认为这是一个有趣的现实问题,与“糟糕”的设计无关,而是标准C#的限制。通过object转换类型。

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

或者,用当前的语言说

if (this._value is int intValue)
{
    return BitConverter.GetBytes(intValue);
} 

这段代码在类型转换时使用了boxing,导致性能受到了影响。

解决这个问题的方法是使用来自于System.Runtime.CompilerServices.Unsafe NuGet包的Unsafe.As<TFrom,TTo>()函数:

if(typeof(T) == typeof(int))
{
     return BitConverter.GetBytes(Unsafe.As<T, int>(ref this._value));
}

不会有对 object 的显式或隐式转换的结果。


2

“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 实现的机会。


0

对这里提出的方法以及其他地方发现的方法进行性能测试:

注意:[MethodImpl(MethodImplOptions.AggressiveInlining)] 提供了非常小但可测量的性能差异。足够小以至于可以忽略不计,但足够大以至于可以测量。

循环执行1亿次。

Baseline Time   : 00:00:00.5938502
Baseline +Method: 00:00:00.8098170
Marshal         : 00:00:10.5734336
Boxed Conversion: 00:00:01.9270779
GenericConv<T>  : 00:00:01.3276721
UnsafeConv<t>   : 00:00:02.4099777
(dynamic)Conv<T>: 00:00:02.8901075

令人惊讶的是,Marshal 提供了最慢的性能,而 @MichaC 建议的解决方案提供了最佳性能。

以下是基准测试代码:

    static void Main(string[] args)
    {
        int NumReps;
        NumReps = 100000000;
        Stopwatch SW;
        SW = new Stopwatch();
        byte[] buffer = new byte[4];

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = BitConverter.GetBytes(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"Baseline Time   : {0}", SW.Elapsed.ToString());

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = BitConversion(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"Baseline +Method: {0}", SW.Elapsed.ToString());

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = MarshalConv<int>(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"Marshal         : {0}", SW.Elapsed.ToString());

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = BoxedConversion<int>(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"Boxed Conversion: {0}", SW.Elapsed.ToString());

        GenericClass<int> GenericConverter = new GenericClass<int>();
        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = GenericConverter.GetBytes(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"GenericConv<T>  : {0}", SW.Elapsed.ToString());

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = UnsafeConversion<int>(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"UnsafeConv<t>   : {0}", SW.Elapsed.ToString());

        for (int N = 0; N < 2; N++)
        {
            SW.Restart();
            for (int I = 0; I < NumReps; I++)
            {
                buffer = DynamicConv<int>(I);
            }
            SW.Stop();
        }
        Console.WriteLine(@"(dynamic)Conv<T>: {0}", SW.Elapsed.ToString());
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static byte[] BitConversion(int input)
    {
        return BitConverter.GetBytes(input);
    }

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

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

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

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

    public class GenericClass<T> where T : struct
    {
        IValueConverter<T> converter = new ValueConverter() as IValueConverter<T>;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte[] GetBytes(T value)
        {
            if (converter == null)
            {
                throw new InvalidOperationException("Unsuported type");
            }

            return converter.FromValue(value);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static byte[] BoxedConversion<T>(T input)
        where T : struct, IConvertible
    {
        switch (Type.GetTypeCode(typeof(T)))
        {
            case TypeCode.Boolean:
                return BitConverter.GetBytes((bool)(object)input);
            case TypeCode.Byte:
                return BitConverter.GetBytes((byte)(object)input);
            case TypeCode.Char:
                return BitConverter.GetBytes((char)(object)input);
            case TypeCode.Double:
                return BitConverter.GetBytes((double)(object)input);
            case TypeCode.Int16:
                return BitConverter.GetBytes((short)(object)input);
            case TypeCode.Int32:
                return BitConverter.GetBytes((int)(object)input);
            case TypeCode.Int64:
                return BitConverter.GetBytes((long)(object)input);
            case TypeCode.SByte:
                return BitConverter.GetBytes((sbyte)(object)input);
            case TypeCode.Single:
                return BitConverter.GetBytes((float)(object)input);
            case TypeCode.UInt16:
                return BitConverter.GetBytes((ushort)(object)input);
            case TypeCode.UInt32:
                return BitConverter.GetBytes((uint)(object)input);
            case TypeCode.UInt64:
                return BitConverter.GetBytes((ulong)(object)input);
            default:
                throw new NotSupportedException(@"Unsupported privitive type. Example null reference or datetime.");
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static byte[] UnsafeConversion<T>(T input)
        where T : struct, IConvertible
    {
        switch (Type.GetTypeCode(typeof(T)))
        {
            case TypeCode.Boolean:
                return BitConverter.GetBytes(Unsafe.As < T, bool>(ref input));
            case TypeCode.Byte:
                return BitConverter.GetBytes(Unsafe.As < T, byte>(ref input));
            case TypeCode.Char:
                return BitConverter.GetBytes(Unsafe.As < T, char>(ref input));
            case TypeCode.Double:
                return BitConverter.GetBytes(Unsafe.As < T, double>(ref input));
            case TypeCode.Int16:
                return BitConverter.GetBytes(Unsafe.As < T, short>(ref input));
            case TypeCode.Int32:
                return BitConverter.GetBytes(Unsafe.As < T, int>(ref input));
            case TypeCode.Int64:
                return BitConverter.GetBytes(Unsafe.As < T, long>(ref input));
            case TypeCode.SByte:
                return BitConverter.GetBytes(Unsafe.As < T, sbyte>(ref input));
            case TypeCode.Single:
                return BitConverter.GetBytes(Unsafe.As < T, float>(ref input));
            case TypeCode.UInt16:
                return BitConverter.GetBytes(Unsafe.As < T, ushort>(ref input));
            case TypeCode.UInt32:
                return BitConverter.GetBytes(Unsafe.As < T, uint>(ref input));
            case TypeCode.UInt64:
                return BitConverter.GetBytes(Unsafe.As < T, ulong>(ref input));
            default:
                throw new NotSupportedException(@"Unsupported privitive type. Example null reference or datetime.");
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static byte[] DynamicConv<T>(T input)
        where T : struct, IConvertible
    {
        return BitConverter.GetBytes((dynamic)input);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] MarshalConv<T>(T input) where T : struct
    {
        int size = Marshal.SizeOf(typeof(T));
        var result = new byte[size];
        var gcHandle = GCHandle.Alloc(input, GCHandleType.Pinned);
        Marshal.Copy(gcHandle.AddrOfPinnedObject(), result, 0, size);
        gcHandle.Free();
        return result;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接