开发者

Generic that takes only numeric types (int double etc)?

开发者 https://www.devze.com 2023-02-03 03:12 出处:网络
In a program I\'m working on, I need to write a function to take any numeric type (int, short, long etc) and shove it in to a byte array at a specific offset.

In a program I'm working on, I need to write a function to take any numeric type (int, short, long etc) and shove it in to a byte array at a specific offset.

There exists a Bitconverter.GetBytes() method that takes the numeric type and returns it as a byte array, and this method only takes numeric types.

So far I have:

    private void AddToByteArray<T>(byte[] destination, int offset, T toAdd) where T : struct
    {
        Buffer.BlockCopy(BitConverter.GetBytes(toAdd), 0, destination, offset, sizeof(toAdd));
    }

So basically my goal is that, for example, a call to AddToByteArray(array, 3, (short)10) would take 10 and store it in the 4th slot of array. The explicit cast exists because I know exactly how many bytes I want it to take up. There are cases where I would want a number that is small enough to be a short to really take up 4 bytes. On the flip side, there are times when I want an int to be crunched down to just a single byte. I'm doing this to create a custom network packet, if that makes any ideas pop in to your heads.

If the where clause of a generic support开发者_StackOverflow社区ed something like "where T : int || long || etc" I would be ok. (And no need to explain why they don't support that, the reason is fairly obvious)

Any help would be greatly appreciated!

Edit: I realize that I could just do a bunch of overloads, one for each type I want to support... but I'm asking this question because I want to avoid precisely that :)


I disagree that this can't be done; it's just that the design I'd propose to do it is a little weird (and involved).

Here's the idea.

The Idea

Define an interface IBytesProvider<T>, with one method:

public interface IBytesProvider<T>
{
    byte[] GetBytes(T value);
}

Then implement this in a BytesProvider<T> class with a static Default property.

If this sounds familiar, it's because it's exactly how the EqualityComparer<T> and Comparer<T> classes work (heavily used in plenty of LINQ extension methods).

The Implementation

Here's how I'd propose you set it up.

public class BytesProvider<T> : IBytesProvider<T>
{
    public static BytesProvider<T> Default
    {
        get { return DefaultBytesProviders.GetDefaultProvider<T>(); }
    }

    Func<T, byte[]> _conversion;

    internal BytesProvider(Func<T, byte[]> conversion)
    {
        _conversion = conversion;
    }

    public byte[] GetBytes(T value)
    {
        return _conversion(value);
    }
}

static class DefaultBytesProviders
{
    static Dictionary<Type, object> _providers;

    static DefaultBytesProviders()
    {
        // Here are a couple for illustration. Yes, I am suggesting that
        // in reality you would add a BytesProvider<T> for each T
        // supported by the BitConverter class.
        _providers = new Dictionary<Type, object>
        {
            { typeof(int), new BytesProvider<int>(BitConverter.GetBytes) },
            { typeof(long), new BytesProvider<long>(BitConverter.GetBytes) }
        };
    }

    public static BytesProvider<T> GetDefaultProvider<T>()
    {
        return (BytesProvider<T>)_providers[typeof(T)];
    }
}

The Payoff

Now, finally, once you'd done all this, what you'd do is simply call:

byte[] bytes = BytesProvider<T>.Default.GetBytes(toAdd);

No overloads needed.


You can do this by first seperating the method into two parts, one to turn the value into an array of bytes, and another to insert them. Then just use overloads:

        public static void AddToByteArray(byte[] destination, int offset, long value)
        { InsertBytes(destination, offset, BitConverter.GetBytes(value)); }

        public static void AddToByteArray(byte[] destination, int offset, int value)
        { InsertBytes(destination, offset, BitConverter.GetBytes(value)); }

        public static void AddToByteArray(byte[] destination, int offset, short value)
        { InsertBytes(destination, offset, BitConverter.GetBytes(value)); }

        private static void InsertBytes(byte[] destination, int offset, byte[] bytes)
        {
            Buffer.BlockCopy(bytes, 0, destination, offset, bytes.Length);
        }


This would not work anyway, because which overload of BitConverter.GetBytes() to use is resolved at compile time and not at runtime, so the generic argument passed as T will not be used to help determine the GetBytes() overload. Since there is no overload that accepts object, this approach could not work even if you could constrain T around some specific set of types. So you are doubly-screwed here.

Your only real option here is to overload your AddToByteArray method for each numeric type you want to accept. I know you don't want to do that, but there's little else you can do. (You could accept an argument of object and use reflection to invoke the specific overload of GetBytes() based on the argument type, but that would be dog slow due to reflection and boxing overhead...)


As a simplification of Dan Tao's solution in combination with SLaks' suggestion, here's a complete generic BitConverter:

public class BitConverter<T>
{
    public static readonly Func<T, byte[]> GetBytes = x => new byte[] { };

    static BitConverter()
    {
        BitConverter<byte>.GetBytes = x => new byte[] { x };
        BitConverter<bool>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<char>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<double>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<Int16>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<Int32>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<Int64>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<Single>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<UInt16>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<UInt32>.GetBytes = x => BitConverter.GetBytes(x);
        BitConverter<UInt64>.GetBytes = x => BitConverter.GetBytes(x);
    }
}

Your example would then look like this:

private void AddToByteArray<T>(byte[] destination, int offset, T toAdd) where T : struct
{
    Buffer.BlockCopy(BitConverter<T>.GetBytes(toAdd), 0, destination, offset, sizeof(toAdd));
}

You would simply have to provide a GetBytes() method for each expected type to the static constructor. Which, by the way, is not limited to struct types, for example:

BitConverter<MemoryStream>.GetBytes = x => x.ToArray();
BitConverter<string>.GetBytes = x => Encoding.Default.GetBytes(x);

If there's no GetBytes() method for the generic type, it will return an empty array (but you might want to change the code to throw an Exception instead!).


This is a real problem in C#, there is no common interface that all numeric types implement, you can restrict to struct and new(), but that still will allow structs with a parameterless constructor. If you really want to restrict it, you sadly have to use defined overloads for all numeric types.


If you only care about running on little-endian systems (like Windows), you could add a constraint on IConvertible (which I believe all the numeric types support), use that to convert the value to a 64-bit integer, get the bytes for that, and then throw away the bytes you know you don't need. Something like this:

private byte[] NumberToBytes<T>(T value)
    where T : new(), struct, IConvertible
{
    var longValue = value.ToUInt64();
    var bytes = BitConverter.GetBytes(longValue);
    Array.Resize(ref bytes, sizeof(T));
    return bytes;
}

Of course, this assumes that you're only working with integers -- it wouldn't work for float, double, or decimal. And as noted above, it would only work on little-endian systems; for big-endian, you would need to keep the last sizeof(T) elements from the array, not the first.

0

精彩评论

暂无评论...
验证码 换一张
取 消