Nerdy tidbits from my life as a software engineer

Tuesday, July 8, 2008

Generic Type Inference

If you've been doing anything with Linq lately, you've probably noticed that the extension methods you've been calling have had generic arguments that you've been conveniently leaving out. And yet, your queries are compiling and running without any problems. How is that possible? The answer is type inference.

Let's take something very simple:

public static void DoSomething<T>(T  t)
{   
    ...
}

DoSomething(23.0); // This works fine

See how there is no generic argument in the call to DoSomething? This code is legal and will compile without any problems. The reason why is because the compiler is able to infer the type of T based on the argument into DoSomething. This is how all of your fun Linq queries and lambda expressions are able to work without forcing you to figure out all of the generic arguments. Otherwise, the syntax would be too complicated to use.

What's interesting about type inference, however, is that it only works if one or more of the arguments into the method are generic arguments. The compiler cannot, for instance, infer a generic argument based on the return type:

public static T DoSomething<T>()
{
  return default(T);
}

double d = DoSomething(); // Won't compile!

In this case, the compiler will barf because it can't infer T based on the return type of the expression. The only way to get this to work is either to pass in and argument of type T so that the compiler can infer T, or to explicitly type T as a generic argument. For instance, if you don't want to type out some fully-qualified name to specify T, you can do this:

public static T DoSomething<T>(T ignored)
{
  return default(T);
}
double d = DoSomething(0.0); // This will work

I have a case in some code I'm working on where I need to convert one enumeration into another. The two enumerations are identical, but one was generated using svcutil.exe and is used in a WCF client. Unfortunately, there are places where I have namespace conflicts - both the auto-generated enumeration and the enumeration that it was generated from need to be used in the same place - and also converted from one to the other. To handle the conversion of one type into another, I wrote a simple extension method:

/// <summary>
/// Converts an enumeration value for oldValue into T.  This is needed because 
/// several of our enumerations are copied into multiple places due to the client / server
/// nature of the WCF services world.  IE, any  time we have an enumeration that is exposed
/// via a WCF service, a version of that enumeration is generated in the Clients namespace - 
/// and this value must be converted over and over again.
/// </summary>
/// <typeparam name="T">The type to convert to.</typeparam>
/// <param name="oldValue">The old value.</param>
/// <returns>An equivelant instance of type T.</returns>
public static T ConvertEnum<T>(this Enum oldValue) where T : struct
{
    return (T)Enum.Parse(typeof(T), oldValue.ToString());
}

I added the where T : struct clause only because you cannot use where T : Enum, which would be ideal. This is an unfortunate method, but it is making life easier in a number of places. Sadly, the problem is that due to the namespace conflict, I end up having to enter a fully-qualified generic argument into T. This is a long name, and it would really be nice if the compiler could infer the type for me. What I did find, however, is that simply adding an argument of type T allowed the compiler to figure everything out for me - which makes things a bit cleaner syntactically:

/// <summary>
/// This alternate version of ConvertEnum allows the compiler to infer type T.
/// </summary>
/// <typeparam name="T">The type to convert to.</typeparam>
/// <param name="oldValue">The old value.</param>
/// <param name="ignored">Not used, but allows the compiler to infer type T.</param>
/// <returns>A converted instance of type T</returns>
public static T ConvertEnum<T>(this Enum oldValue, T ignored) where T : struct
{
    return (T)Enum.Parse(typeof(T), oldValue.ToString());
}
...
info.System = system.ConvertEnum(info.System);

I'd much rather be able to omit the argument entirely, but this is at least a bit cleaner to look at than:

info.System = system.ConvertEnum<My.Really.Long.Namespace.ESystems>();