Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid excessive type-checking in generic methods?

Tags:

c#

generics

My question concerns type-checking in a chain of generic methods. Let's say I have an extension method that attempts to convert a byte array to an int, decimal, string, or DateTime.

public static T Read<T>(this ByteContainer ba, int size, string format) where T : struct, IConvertible
{
    var s = string.Concat(ba.Bytes.Select(b => b.ToString(format)).ToArray());            
    var magic = FromString<T>(s);
    return (T)Convert.ChangeType(magic, typeof(T));
}

This calls a method called FromString that translates the concatenated string to a specific type. Unfortunately, the business logic is completely dependent on the type T. So I end up with a megalithic if-else block:

private static T FromString<T>(string s) where T : struct
{
    if (typeof(T).Equals(typeof(decimal)))
    {
        var x = (decimal)System.Convert.ToInt32(s) / 100;
        return (T)Convert.ChangeType(x, typeof(T));
    }
    if (typeof(T).Equals(typeof(int)))
    {
        var x = System.Convert.ToInt32(s);
        return (T)Convert.ChangeType(x, typeof(T));
    }
    if (typeof(T).Equals(typeof(DateTime)))
        ... etc ...
 }

At this point, I would prefer multiple methods with the same name and different return types, something along the lines of this:

// <WishfulThinking>
private static decimal FromString<T>(string s)
{
    return (decimal)System.Convert.ToInt32(s) / 100;
}    
private static int FromString<T>(string s)
{
    return System.Convert.ToInt32(s);
}
// </WishfulThinking>

... but I realize this is not valid, as T can't be constrained to a specific type, and without it, all of the methods will have the same conflicting signature.

Is there a feasible way to implement FromString without the excessive type-checking? Or might there be a better way to approach this problem altogether?

like image 523
MadHenchbot Avatar asked Aug 09 '13 20:08

MadHenchbot


2 Answers

Or might there be a better way to approach this problem altogether?

Sure, there is one: you can make each converter into a lambda, make a dictionary of them, and use them for the conversion, like this:

private static IDictionary<Type,Func<string,object>> Converters = new Dictionary<Type,Func<string,object>> {
    {typeof(int), s => Convert.ChangeType(System.Convert.ToInt32(s), typeof(int))}
,   {typeof(decimal), s => Convert.ChangeType((decimal)System.Convert.ToInt32(s) / 100, typeof(decimal))}
,   ... // And so on
};
public static T Read<T>(this ByteContainer ba, int size, string format) where T : struct, IConvertible {
    Func<string,object> converter;
    if (!Converters.TryGetValue(typeof(T), out converter)) {
        throw new ArgumentException("Unsupported type: "+typeof(T));
    }
    var s = string.Concat(ba.Bytes.Select(b => b.ToString(format)).ToArray());
    return (T)converter(s);
}
like image 55
Sergey Kalinichenko Avatar answered Sep 20 '22 03:09

Sergey Kalinichenko


In general, if you have to write logic that must always check the type of a generic type parameter, you are not actually writing code that benefits from being generic. That being the case, and assuming the actual problem you're trying to solve is the need to convert a byte array into some predictable built-in type that it represents, I recommend you abandon this approach and use the methods in the BitConverter class.

At the point where you could determine the value of T, simply call the appropriate method on the BitConverter class.

Update: If a generic solution is necessary, I'd suggest something similar to dasblinkenlight's answer, though I would let the caller inject the converter (after all, the caller knows the requisite result type), which avoids the problem of maintaining a list of conversion functions alongside the generic method:

public static T Read<T>(this ByteContainer ba, int size, string format, 
                        Func<string, T> converter) where T : struct, IConvertible 
{
    var s = string.Concat(ba.Bytes.Select(b => b.ToString(format)).ToArray());
    return converter(s);
}
like image 33
Dan J Avatar answered Sep 20 '22 03:09

Dan J