C# generics: cast generic type to value type
Asked Answered
R

9

18

I have a generic class which saves value for the specified type T. The value can be an int, uint, double or float. Now I want to get the bytes of the value to encode it into an specific protocol. Therefore I want to use the method BitConverter.GetBytes() but unfortunately Bitconverter does not support generic types or undefined objects. That is why I want to cast the value and call the specific overload of GetBytes(). My Question: How can I cast a generic value to int, double or float? This doesn't work:

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

Is there a possibility to cast an generic value? Or is there another way to get the bytes?

Raisaraise answered 11/4, 2013 at 21:4 Comment(1)
If you know the values will be numeric, could you not use the lowest common numeric type instead of a generic one?Render
I
28

First off, this is a really bad code smell. Any time you're doing a type test on a type parameter like this odds are good you're abusing generics.

The C# compiler knows that you are abusing generics in this way and disallows the cast from the value of type T to int, etc. You can turn off the compiler getting in your way by casting the value to object before you cast it to int:

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

Yuck. Again, it would be better to find another way to do this. For example:

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

No generics necessary. Reserve generics for situations that are actually generic. If you've written the code four times one for each kind of type, you haven't gained anything with generics.

Implantation answered 11/4, 2013 at 21:11 Comment(2)
My goal is to write a class to save a 2d matrix (2d array). The class has methods to add rows and columns. I handle the data in an List<List<TYPE>> field so I can easily extend the number of rows and columns whithout always creating new 2d array. Furthermore the class should have an method to export the data to a blow (big byte array where the bytes of each field is saved. Therefore I need the BitConverter. But I don't want to write double code. Why there is not GetBytes<T>() method?Raisaraise
@rittergig: Consider writing an immutable matrix for doing math operations. You don't mutate the number 2 into 5 when you add 3 to it; you create a brand-new number 5 and the 2 still exists. The same can be true of matrices.Implantation
H
12

Late to the party, but just wanted to comment on the comments saying that the original proposal was a "bad design" - in my opinion, the original proposal (though it doesn't work) was not "necessarily" a bad design at all!

Coming from a strong C++ (03/11/14) background with deep understanding of template meta-programming, I created a type generic serialization library in C++11 with minimal code repetition (the goal is to have non-repetitive code, and I believe I have achieved 99% of it). The compile time template meta-programming facilities as provided by C++11, though can become extremely complex, helps achieve true type generic implementation of the serialization library.

However, it is very unfortunate that when I wanted to implement a simpler serialization framework in C#, I was stuck exactly on the problem that the OP had posted. In C++, the template type T can be totally "forwarded" to the site of usage, while C# generics does not forward the actual compile time type to the usage site - any second (or more) level reference to a generic type T makes T becoming a distinct type that is not usable at all at the actual usage site, thus GetBytes(T) cannot determine that it should invoke a specific typed overload - worse, there is even no nice way in C# to say: hey, I know T is int, and if the compiler doesn't know it, does "(int)T" make it an int?

Also, instead of blaming that type based switch has a smell of bad design - this has been a great misnomer that whenever people are doing some advanced type based framework and has to resort to type based switch due to inability of the language environment, without really understanding the constraints of the actual problem at hand, people starts to blatantly say type based switch is a bad design - it is, for most of the traditional OOP usage cases, but there are special cases, most of the time advanced usage case like the problem we are talking here, that this is necessary.

It is also worth mentioning that I would actually blame that the BitConverter class is designed in a traditional and incompetent way to suit generic needs: instead of defining a type specific method for each type with regard to "GetBytes", maybe it would be more generic friendly to define a generic version of GetBytes(T value) - possibly with some constraints, so the user generic type T can be forwarded and work as expected without any type switch at all! The same is true for all the ToBool/ToXxx methods - if the .NET framework provides the facilities as non-generic version, how would one expect a generic framework trying to utilize this foundation framework - type switch or if without type switch, you end up duplicating the code logic for each data type you are trying to serialize - Oh, I miss the day I worked with C++ TMP that I only write the serialization logic once for practically unlimited number of types I can support.

Heldentenor answered 10/7, 2016 at 6:19 Comment(1)
Agreed this is an interesting issue in "no nice way in C# to say: hey, I know T is int...". The solution is Unsafe.As() library method. While it works, it is still "unsafe" for a reason, so needs to be used with discretion.Millepede
C
10

Well, it strikes me that the type really isn't properly generic to start with: it can only be one of a few types, and you can't express that constraint.

Then you want to call a different overload of GetBytes based on the type of T. Generics doesn't work well for that sort of thing. You could use dynamic typing to achieve it, in .NET 4 and above:

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

... but again this doesn't really feel like a nice design.

Cornelie answered 11/4, 2013 at 21:9 Comment(0)
S
10

Pretty late answer, but anyways... there is a way to make it slightly nicer... Make use of generics in a this way: Implement another generic type which converts the types for you. So you don't have to care about unboxing, casting etc of the type to object... it will just work.

Also, in your GenericClass, now you don't have to switch the types, you can just use IValueConverter<T> and also cast it as IValueConverter<T>. This way, generics will do the magic for you to find the correct interface implementation, and in addition, the object will be null if T is something you do not support...

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);
    }
}
Stairhead answered 18/3, 2015 at 22:55 Comment(2)
converter can be static. I would also consider static Action<T,byte[]> convert = ... with a static initialiser that locates the BitConverter.GetBytes method with a matching argument type. Not sure if that would actually reduce runtime overhead though.Mclyman
@Stairhead Your solution is the highest performance solution that I've found so far. Thanks for this :)Quintillion
P
3

You could potentially use Convert.ToInt32(this._value) or (int)((object)this._value). But in general if you find yourself having to check for specific types in a generic method, there's a problem with your design.

In your case, you probably should consider making an abstract base class, and then derived classes for the types you're going to use:

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);
    }
}
Pourboire answered 11/4, 2013 at 21:12 Comment(0)
C
3

If your only goal is to add the GetBytes method to these types, isn't it a much nicer solution to add them as extension methods like so:

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

If you really need you generic class for other purposes, just do the dirty "double typecast" like Eric mentioned where you typecast value to object first.

Coquille answered 11/4, 2013 at 21:20 Comment(0)
M
3

I am on the side that this is an interesting real life problem that has nothing to do with "bad" design but rather standard C# limitations. Casting through object

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

or, in the current language as

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

works but causes a performance hit due to ValueType boxing.

The solution to this problem is Unsafe.As<TFrom,TTo>() from System.Runtime.CompilerServices.Unsafe NuGet package:

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

Results in no explicit or implicit cast to object.

Millepede answered 30/10, 2020 at 8:24 Comment(0)
B
2

What would GenericClass<DateTime> do? Rather, it seems you have a discrete set of classes which know how to get their bytes, so make an abstract base class that does all of the common work, and then make 3 concrete class which override a method to specify the piece that changes between them:

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

This not only provides clean, strongly-typed implementations of your three known types, but leaves the door open for anyone else to subclass Generic<T> and provide an appropriate implementation of GetBytes.

Biles answered 12/4, 2013 at 21:29 Comment(0)
Q
0

Performance test of the methods suggested here along with others found elsewhere:

Note: [MethodImpl(MethodImplOptions.AggressiveInlining)] Provides a very small but measurable performance difference. Small enough to be insignificant but big enough to be measurable.

Looped 100 million times.

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

Surprisingly Marshal provides the slowest performance While the solution suggested by @MichaC provides the best performance.

Here is the benchmark code:

    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;
    }
Quintillion answered 12/4, 2022 at 13:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.