Evil Code of the Day: variance and overloading

Whenever a conversion becomes available where it wasn't before, overload resolution can change its behaviour. From C# 1 to C# 2 this happened due to delegate variance with method group conversions - now the same thing is true for generic variance for interfaces.
What does the following code print?

using System;
using System.Collections.Generic;

class Base
{
    public void Foo(IEnumerable<string> strings)
    {
        Console.WriteLine("Strings");
    }
}

class Derived : Base
{
    public void Foo(IEnumerable<object> objects)
    {
        Console.WriteLine("Objects");
    }
}

class Test
{
    static void Main()
    {
        List<string> strings = new List<string>();
        new Derived().Foo(strings);
    }
}
The correct answer is "it depends on which version of C# and .NET framework you're using."
If you're using C# 4.0 and .NET 4.0, then IEnumerable<T> is covariant: there's an implicit conversion from IEnumerable<string> to IEnumerable<object>, so the derived overload is used.
If you're using C# 4.0 but .NET 3.5 or earlier then the compiler still knows about variance in general, but the interface in the framework doesn't have the appropriate metadata to indicate it, so there's no conversion available, and the base class overload is used.
If you're using C# 3.0 or earlier then the compiler doesn't know about generic variance at all, so again the base class overload is used.
So, this is a breaking change, and a fairly subtle one at that - and unlike the method group conversion in .NET 2.0, the compiler in .NET 4.0 beta 1 doesn't issue a warning about it. I'll edit this post when there's an appropriate Connect ticket about it...
In general though, I'd say it's worth avoiding overloading a method declared in a base class unless you really have to. In particular, overloading it using the same number of parameters but more general ones seems to be a recipe for unreadable code.
Previous
Next Post »