General type conversion without risking Exceptions
I am working on a control that can take a number of different datatypes (anything that implements IComparable).
I need to be able to compare these with another variable passed in.
If the main datatype is a DateTime, and I am passed a String, I need to
- attempt to convert the String to a DateTime to perform a Date comparison.
- if the String cannot be converted to a DateTime then do a String comparison.
So I need a general way to attempt to convert from any type to any type. Easy enough, .Net provides us with the TypeConverter class.
Now, the best I can work out to do to determine if the String can be converted to a DateTime is to use exceptions. If the ConvertFrom raises an exception, I know I cant do the conversion and have to do the string comparison.
The following is the best I got :
string theString = "99/12/2009";
DateTime theDate = new DateTime ( 2009, 11, 1 );
IComparable obj1 = theString as IComparable;
IComparable obj2 = theDate as IComparable;
try
{
TypeConverter converter = TypeDescriptor.GetConverter ( obj2.GetType () );
if ( converter.CanConvertFrom ( obj1.GetType () ) )
{
Console.WriteLine ( obj2.CompareTo ( converter.ConvertFrom ( obj1 ) ) );
Console.WriteLine ( "Date comparison" );
}
}
catch ( FormatException )
{
Console.WriteLine ( obj1.ToString ().CompareTo ( obj2.ToString () ) );
Console.WriteLine ( "String comparison" );
}
Part of our standards at work state that :
Exceptions should only be raised when an Exception situation - ie. an error is encountered.
But this is not an exceptional situation. I need another way around it.
Most variable types have a TryParse method which returns a boolean to allow you to determine if the conversion has succeeded or not. But there is no TryConvert method available to TypeConverter. CanConvertFrom only dermines if it is possible to convert between these types and doesnt consider the actual data to be converted. The IsValid method is also useless.
Any ideas?
EDIT
I cannot use AS and IS. I do not know either data types at compile time. So I dont know what to As and Is to!!!
EDIT
Ok nailed the bastard. Its not as tidy as Marc Gravells, but it works (I hope). Thanks for the inpiration Marc. Will work on tidying it up when I get the time, but I've got a bit stack of bugfixes that I have to get on with.
public static class CleanConverter
{
/// <summary>
/// Stores the cache of all types that can be converted to all types.
/// </summary>
private static Dictionary<Type, Dictionary<Type, ConversionCache>> _Types = new Dictionary<Type, Dictionary<Type, ConversionCache>> ();
/// <summary>
/// Try parsing.
/// </summary>
/// <param name="s"></param>
/// <param name="value"></param>
/// <returns></returns>
public static bool TryParse ( IComparable s, ref IComparable value )
{
// First get the cached conversion method.
Dictionary<Type, ConversionCache> type1Cache = null;
ConversionCache type2Cache = null;
if ( !_Types.ContainsKey ( s.GetType () ) )
{
type1Cache = new Dictionary<Type, ConversionCache> ();
_Types.Add ( s.GetType (), type1Cache );
}
else
{
type1Cache = _Types[s.GetType ()];
}
if ( !type1Cache.ContainsKey ( value.GetType () ) )
{
// We havent converted this type before, so create a new conversion
type2Cache = new ConversionCache ( s.GetType (), value.GetType () );
// Add to the cache
type1Cache.Add ( value.GetType (), type2Cache );
}
else
{
type2Cache = type1Cache[value.GetType ()];
}
// Attempt the parse
return type2Cache.TryParse ( s, ref value );
}
/// <summary>
/// Stores the method to convert from Type1 to Type2
/// </summary>
internal class ConversionCache
{
internal bool TryParse ( IComparable s, ref IComparable value )
{
if ( this._Method != null )
{
// Invoke the cached TryParse method.
object[] parameters = new object[] { s, value };
bool result = (bool)this._Method.Invoke ( null, parameters);
if ( result )
value = parameters[1] as IComparable;
return result;
}
else
return false;
}
private MethodInfo _Method;
开发者_C百科 internal ConversionCache ( Type type1, Type type2 )
{
// Use reflection to get the TryParse method from it.
this._Method = type2.GetMethod ( "TryParse", new Type[] { type1, type2.MakeByRefType () } );
}
}
}
Are generics an option? Here's a cheeky hack that hunts the TryParse
method and calls it via a (cached) delegate:
using System;
using System.Reflection;
static class Program
{
static void Main()
{
int i; float f; decimal d;
if (Test.TryParse("123", out i)) {
Console.WriteLine(i);
}
if (Test.TryParse("123.45", out f)) {
Console.WriteLine(f);
}
if (Test.TryParse("123.4567", out d)) {
Console.WriteLine(d);
}
}
}
public static class Test
{
public static bool TryParse<T>(string s, out T value) {
return Cache<T>.TryParse(s, out value);
}
internal static class Cache<T> {
public static bool TryParse(string s, out T value)
{
return func(s, out value);
}
delegate bool TryPattern(string s, out T value);
private static readonly TryPattern func;
static Cache()
{
MethodInfo method = typeof(T).GetMethod(
"TryParse", new Type[] { typeof(string), typeof(T).MakeByRefType() });
if (method == null) {
if (typeof(T) == typeof(string))
func = delegate(string x, out T y) { y = (T)(object)x; return true; };
else
func = delegate(string x, out T y) { y = default(T); return false; };
} else {
func = (TryPattern) Delegate.CreateDelegate(typeof(TryPattern),method);
}
}
}
}
If it is not possible to write it without exceptions, you can isolate the problematic code by refactoring it into a method like so:
public static bool TryConvert<T, U>(T t, out U u)
{
try
{
TypeConverter converter = TypeDescriptor.GetConverter(typeof(U));
if (!converter.CanConvertFrom(typeof(T)))
{
u = default(U);
return false;
}
u = (U)converter.ConvertFrom(t);
return true;
}
catch (Exception e)
{
if (e.InnerException is FormatException)
{
u = default(U);
return false;
}
throw;
}
}
Ideally you should be passing in nullable types as the output parameter, so that null represents an undefined value (because it couldn't do a conversion) rather than the default (i.e. 0 for int)
I would argue that this code really should throw exceptions when it can't figure out a conversion. If the two arguments passed in are DateTime.Now
and Color.Fuschsia
, you can make no meaningful comparison between them, so any value you return would be wrong. That's the definition of the right time to throw an exception.
If you absolutely need to avoid exceptions, it's not possible to do what you want with arbitrary types. Every type has its own rules about which values it can parse, and the converter has no way to tell that in advance. (That is to say, as you've noticed, it knows that you can sometimes convert a string
to a DateTime
, but it is not designed to know that "1/1/2010" is a valid DateTime
while "Fred" is not.)
So I need a general way to attempt to convert from any type to any type. Easy enough, .Net provides us with the
TypeConverter
class.
You're asking too much.
class Animal { }
class Dog : Animal { }
class Cat : Animal { }
Should I be able to convert a Cat
to a Dog
?
You'll find your problem is far easier to solve if you specify more precisely (preferably exactly) what you want the behavior of the method to be. So, write down the expected inputs and what you want the output to be in each possible case. Then your method should write itself.
So right now we have this specification:
If the main datatype is a
DateTime
, and I am passed aString
, I need toattempt to convert the
String
to aDateTime
to perform aDate
comparison. if theString
cannot be converted to aDateTime
then do aString
comparison.
int CompareTo(DateTime d, object o) {
string s = o as string;
if(s != null) {
DateTime dt;
if(dt.TryParse(s, out dt)) {
return d.CompareTo(dt);
}
else {
return d.ToString().CompareTo(s);
}
}
throw new InvalidOperationException();
}
精彩评论