Search code examples
c#.netgenericshalf-precision-float

Convert generic type to Half value allocation-free


In an application that can write numeric values to a file using BinaryWriter I have a class that is typed to the number type that should be used for the file. It looks like this:

class ValueCollection<T> where T : struct, IConvertible
{
    private BinaryWriter writer;
}

The constructor will make sure that only supported types are used as type arguments. (Code not shown here, it's just typeof(T) comparisons.)

It has a method that should write such a value to the file writer, that looks like this:

public void Write(T value)
{
    if (typeof(T) == typeof(int)) writer.Write(value.ToInt32(NumberFormatInfo.InvariantInfo));
    if (typeof(T) == typeof(long)) writer.Write(value.ToInt64(NumberFormatInfo.InvariantInfo));
    if (typeof(T) == typeof(double)) writer.Write(value.ToDouble(NumberFormatInfo.InvariantInfo));
    // (There are more...)
}

That's all fine. But how can I support the Half type with this in an allocation-free and non-dynamic manner? This doesn't work:

    if (typeof(T) == typeof(Half)) writer.Write((Half)value);
    // Cannot convert T to System.Half

IConvertible and the Convert class don't know about Half at all. Am I lost here with this shiny new data type being unusable when all other number types work just fine?

I'm targeting .NET 8 and can use all the features there are. But it needs to work in an AOT publishing environment, so no reflection and probably also no dynamics.

PS: The same will come for a read method that uses BinaryReader.ReadHalf and should convert it back to T to return a value.


Solution

  • You cannot immediately use System.Half as type value T, as Half does not implement the IConvertible interface that you request in the constraint.

    Since you state that you check the number type in the constructor anyway, you could as a workaround drop that type constraint, and instead implement a custom handler function for every type. You then choose a suitable one during initialization:

    class ValueCollection<T> where T : struct
    {
        private readonly BinaryWriter _writer;
    
        private readonly Action<T, BinaryWriter> _writeValue;
        
        public ValueCollection(BinaryWriter writer)
        {
            _writer = writer;
            _writeValue = typeof(T) switch
            {
                Type t when t == typeof(int) => (Action<T, BinaryWriter>)(object)WriteInt32,
                // ...
                Type t when t == typeof(Half) => (Action<T, BinaryWriter>)(object)WriteHalf,
                _ => throw new NotSupportedException()
            };
        }
        
        public void Write(T value) => _writeValue(value, _writer);
        
        private static void WriteInt32(int value, BinaryWriter writer) => writer.Write(value);
        // ...
        private static void WriteHalf(Half value, BinaryWriter writer) => writer.Write(value);
    }