Wednesday, May 21, 2008

Extension Methods

I've been having a good time with extension methods in C# 3.0 lately. You know, kids to bed, having a beer, reading digg.com and thinking, "What am I doing?!?! I wanna write some extension methods! Hell Yes!!!" </sarcasm>

If you don't know about, or understand extension methods - google it. This post is NOT for the beginning developer. I'm not providing complete examples that you can just pop in your code. You need to understand where to put these extension methods (in some static class in a special namespace that isn't ALWAYS included). I'm not catering to the script kiddies here (script kiddies: go back to the IRC #hak5 chat room please).

The first thing everyone seems to want to do is take all of the static methods from the static "Convert" class and apply them to the appropriate objects. Certainly a reasonable approach. However, instead of writing it all out, we can just codegen all of the methods. The Jedi is lazy - most of you know that already.
This code will popup a messagebox that you can copy the code for all 100+ (I'm not counting) conversion methods. The end result is that all primitives have a "ToWhatever()" method that makes it easy to do conversions.

static void Main()
{
StringBuilder code = new StringBuilder();
foreach (var m in typeof(Convert).GetMethods(
BindingFlags.Public |
BindingFlags.Static))
{
ParameterInfo[] parameters =
m.GetParameters();
code.AppendFormat("public static {0} {1}(",
m.ReturnParameter.ParameterType.Name,
m.Name);
bool first = true;
foreach (var p in parameters)
{
if (first)
{
code.Append("this ");
first = false;
}
code.AppendFormat("{0} {1}, ",
p.ParameterType.Name,
p.Name);
}
code.Remove(code.Length - 2, 2);
code.AppendFormat(") {{ return Convert.{0}(",
m.Name);
foreach (var p in m.GetParameters())
{
code.AppendFormat("{0}, ", p.Name);
}
code.Remove(code.Length - 2, 2);
code.Append("); }");
code.AppendLine();
}
MessageBox.Show(code.ToString());
}

Sweet... But, that code will throw on exceptions... What if we wanted a "TryParse" type of equivalent for every single one? Time for another approach.
All of these objects are IConvertible.... Hmmmm... how about this approach!

public static T To<T>(this IConvertible obj)
{
return (T)Convert.ChangeType(obj, typeof(T));
}

Sweet! Now we can do

int i = myString.To<int>();

So adding a couple helper methods is easy as pie.

public static T ToOrDefault<T>
(this IConvertible obj)
{
try
{
return To<T>(obj);
}
catch
{
return default(T);
}
}

public static bool ToOrDefault<T>
(this IConvertible obj,
out T newObj)
{
try
{
newObj = To<T>(obj);
return true;
}
catch
{
newObj = default(T);
return false;
}
}

public static T ToOrOther<T>
(this IConvertible obj,
T other)
{
try
{
return To<T>obj);
}
catch
{
return other;
}
}

public static bool ToOrOther<T>
(this IConvertible obj,
out T newObj,
T other)
{
try
{
newObj = To<T>(obj);
return true;
}
catch
{
newObj = other;
return false;
}
}

public static T ToOrNull<T>
(this IConvertible obj)
where T : class
{
try
{
return To<T>(obj);
}
catch
{
return null;
}
}

public static bool ToOrNull<T>
(this IConvertible obj,
out T newObj)
where T : class
{
try
{
newObj = To<T>(obj);
return true;
}
catch
{
newObj = null;
return false;
}
}

Now we can ask for default (calls blank constructor or "0" for numerics) on failure, specify a "default" value (I call it "other"), or ask for null (where T : class). I've also provided both silent exception models, and a typical TryParse model that returns a bool indicating the action taken, and an out param holds the new value.
So our code can do things like this

string a = myInt.ToOrDefault<string>();
//note type inference
DateTime d = myString.ToOrOther(DateTime.MAX_VALUE);
double d;
//note type inference
bool didItGiveDefault = myString.ToOrDefault(d);
string s = myDateTime.ToOrNull<string>();

Oddly type inference doesn't pickup on expected return types. I'd think it would. Thoughts?

So - I'd think that's about the best you can get. I couldn't get Nullable types to roll into the whole thing very cleanly. I tried for about 20 minutes before I threw in the towel.

Enjoy!

3 comments:

Tracy Page said...

You are such a geek. Muah!

Anonymous said...

That's what I am using:

public static bool IsTypeCode<T>(this IConvertible obj, T newObj)
{
try
{
Convert.ChangeType(obj, (TypeCode)Convert.ToInt32(newObj));
return true;
}
catch
{
return false;
}
}

usage:
string s = "123.4";
bool ok;
ok = s.IsTypeCode(TypeCode.Double);
returns ok = true;

ok = s.IsTypeCode(TypeCode.Byte);
returns ok = false;


Greetings Rob

Matthew Jones said...

Excellent post! I really love this approach. It's very neat and tidy and gets rid of loads of boiler plate code!

Thankyou so much for sharing :-)