Matthew Layton
Matthew Layton

Reputation: 42340

Intelligent type conversion in .NET

I have been playing around with converting a string to a value type in .NET, where the resulting value type is unknown. The problem I have encountered in my code is that I need a method which accepts a string, and uses a "best fit" approach to populate the resulting value type. Should the mechanism not find a suitable match, the string is returned.

This is what I have come up with:

public static dynamic ConvertToType(string value)
{
    Type[] types = new Type[]
    {
        typeof(System.SByte),
        typeof(System.Byte), 
        typeof(System.Int16), 
        typeof(System.UInt16), 
        typeof(System.Int32), 
        typeof(System.UInt32), 
        typeof(System.Int64), 
        typeof(System.UInt64), 
        typeof(System.Single), 
        typeof(System.Double), 
        typeof(System.Decimal),
        typeof(System.DateTime),
        typeof(System.Guid)
    };
    foreach (Type type in types)
    {
         try
         {
               return Convert.ChangeType(value, type);
         }
         catch (Exception)
         {
             continue;
         }
    }
    return value;
}

I feel that this approach is probably not best practice because it can only match against the predefined types.

Usually I have found that .NET accommodates this functionality in a better way than my implementation, so my question is: are there any better approaches to this problem and/or is this functionality implemented better in .NET?

EDIT: Note that the ordering of types in the array is so that the "best fit" occurs as accurately as possible for the given types.

EDIT: as per miniBill's request, this I how the method might be used (simple example!):

JsonDictionary["myKey"] = ConvertToType("255"); // 255 is a stringified json value, which should be assigned to myKey as a byte.

Upvotes: 1

Views: 842

Answers (4)

Daniel Imms
Daniel Imms

Reputation: 50229

Your method isn't ideal as its going to cause a series of exceptions if value is not a SByte.

Seeing as all of these types share a common method .TryParse(string, out T) we can use reflection extract the method and call it for each type. I made the method an extension method on string and also factored out the Type[] array into its own lazy loaded property for faster use.

public static class StringExtensions
{
    public static dynamic ConvertToType(this string value)
    {
        foreach (Type type in ConvertibleTypes)
        {
            var obj = Activator.CreateInstance(type);
            var methodParameterTypes = new Type[] { typeof(string), type.MakeByRefType() };
            var method = type.GetMethod("TryParse", methodParameterTypes);
            var methodParameters = new object[] { value, obj };

            bool success = (bool)method.Invoke(null, methodParameters);

            if (success)
            {
                return methodParameters[1];
            }
        }
        return value;
    }

    private static Type[] _convertibleTypes = null;

    private static Type[] ConvertibleTypes
    {
        get
        {
            if (_convertibleTypes == null)
            {
                _convertibleTypes = new Type[]
                {
                    typeof(System.SByte),
                    typeof(System.Byte), 
                    typeof(System.Int16), 
                    typeof(System.UInt16), 
                    typeof(System.Int32), 
                    typeof(System.UInt32), 
                    typeof(System.Int64), 
                    typeof(System.UInt64), 
                    typeof(System.Single), 
                    typeof(System.Double), 
                    typeof(System.Decimal),
                    typeof(System.DateTime),
                    typeof(System.Guid)
                };
            }
            return _convertibleTypes;
        }
    }
}

Usage:

string value = "2391203921";
dynamic converted = value.ConvertToType();

Upvotes: 2

sa_ddam213
sa_ddam213

Reputation: 43616

You could use Reflection to handle all the Parse types by calling the TryParse method, this will be a bit faster than handling multiple exceptions using ChangeType

public Type[] PredefinedTypes = new Type[]
{
    typeof(System.SByte),
    typeof(System.Byte), 
    typeof(System.Int16), 
    typeof(System.UInt16), 
    typeof(System.Int32), 
    typeof(System.UInt32), 
    typeof(System.Int64), 
    typeof(System.UInt64), 
    typeof(System.Single), 
    typeof(System.Double), 
    typeof(System.Decimal),
    typeof(System.DateTime),
    typeof(System.Guid)
};


public dynamic ConvertToType(string value)
{
    foreach (var predefinedType in PredefinedTypes.Where(t => t.GetMethods().Any(m => m.Name.Equals("TryParse"))))
    {
        var typeInstance = Activator.CreateInstance(predefinedType);
        var methodParamTypes = new Type[] { typeof(string), predefinedType.MakeByRefType() };
        var methodArgs = new object[] { value, typeInstance };
        if ((bool)predefinedType.GetMethod("TryParse", methodParamTypes).Invoke(predefinedType, methodArgs))
        {
            return methodArgs[1];
        }
    }
    return value
}

Upvotes: 0

psubsee2003
psubsee2003

Reputation: 8751

This is something I wrote previously that might be a help:

public static Boolean CanCovertTo(this String value, Type type)
{
    var targetType = type.IsNullableType() ? Nullable.GetUnderlyingType(type) : type;

    TypeConverter converter = TypeDescriptor.GetConverter(targetType);
    return converter.IsValid(value);
}

The basic idea is if you pass the string and a Type that you want to test, you can check if a conversion will be valid before attempting to covert.

The problem with this design is that TypeConverter.IsValid() is just a wrapper (with some exception handling) for TypeConverter.CanConvertFrom() so you really aren't eliminating the exception handling, but since it is part of the BCL, I tend to think that is going to be a better implementation.

So you can implement this like so:

private static Type[] defaultTypes = new Type[]
{
    typeof(System.SByte),
    typeof(System.Byte), 
    typeof(System.Int16), 
    typeof(System.UInt16), 
    typeof(System.Int32), 
    typeof(System.UInt32), 
    typeof(System.Int64), 
    typeof(System.UInt64), 
    typeof(System.Single), 
    typeof(System.Double), 
    typeof(System.Decimal),
    typeof(System.DateTime),
    typeof(System.Guid)
};

public static dynamic ConvertToType(string value)
{
    return ConvertToType(value, defaultTypes);
}

public static dynamic ConvertToType(string value, Type[] types)
{
    foreach (Type type in types)
    {
        if (!value.CanConvertTo(type))
            continue;
        return Convert.ChangeType(value, type);
    }

    return value;
}

There is not really a great way to do this without the exception handling (even the exception handling in the TypeConverter.IsValid method), so you have to live with it if you really need such a method. But you can limit the need for the exception handling if you implement some of the suggestions in miniBill's answer in addition to some improvements in the design.

Upvotes: 0

miniBill
miniBill

Reputation: 1733

Your approach would work but, as you say, it's not that elegant.

I think you have a couple of ways to improve this code:

  1. Move the array out of the function, as psubsee2003 said
  2. Use the TryParse methods for cheaper testing (no catching involved) (e.g.: Int32.TryParse)
  3. Actually write a parser that, after trimming,
    • Checks if the number is a GUID
      • Does it have '-' in it at a position > 0?
      • if(Guid.TryParse)
        • return result
      • return string (it can't be a number!)
    • Checks if the number is fractional (does it have a dot in it?)
      • Tries to convert to single, double, decimal using the various TryParse
      • If it fails return string
    • Does it start with a minus?
      • Try and parse as Int64, then check size and see where if fits (<256 -> ubyte, < 65536 ushort...)
      • If it fails return string
    • Try and parse as Int64
      • If it works check minimum size it fits into
      • If it fails it could be an integer, but too big, try parsing as double, if it fails return string

Upvotes: 0

Related Questions