Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics, inheritance, and failed method resolution of C# compiler

I ran across a compilation issue today that baffled me. Consider these two container classes.

public class BaseContainer<T> : IEnumerable<T>
{
    public void DoStuff(T item) { throw new NotImplementedException(); }

    public IEnumerator<T> GetEnumerator() { }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { }
}
public class Container<T> : BaseContainer<T>
{
    public void DoStuff(IEnumerable<T> collection) { }

    public void DoStuff <Tother>(IEnumerable<Tother> collection)
        where Tother: T
    {
    }
}

The former defines DoStuff(T item) and the latter overloads it with DoStuff <Tother>(IEnumerable<Tother>) specifically to get around the absence of covariance/contravariance of C# (until 4 I hear).

This code

Container<string> c = new Container<string>();
c.DoStuff("Hello World");

hits a rather strange compilation error. Note the absence of <char> from the method call.

The type 'char' cannot be used as type parameter 'Tother' in the generic type or method 'Container.DoStuff(System.Collections.Generic.IEnumerable)'. There is no boxing conversion from 'char' to 'string'.

Essentially, the compiler is trying to jam my call to DoStuff(string) into Container.DoStuff<char>(IEnumerable<char>) because string implements IEnumerable<char>, rather than use BaseContainer.DoStuff(string).

The only way I've found to make this compile is to add DoStuff(T) to the derived class

public class Container<T> : BaseContainer<T>
{
    public new void DoStuff(T item) { base.DoStuff(item); }

    public void DoStuff(IEnumerable<T> collection) { }

    public void DoStuff <Tother>(IEnumerable<Tother> collection)
        where Tother: T
    {
    }
}

Why is the compiler trying to jam a string as IEnumerable<char> when 1) it knows it can't (given the presence of a compilation error) and 2) it has a method in the base class that compiles fine? Am I misunderstanding something about generics or virtual method stuff in C#? Is there another fix other than adding a new DoStuff(T item) to Container?

like image 205
Colin Burnett Avatar asked May 28 '09 18:05

Colin Burnett


1 Answers

Edit

Ok... I think I see your confusion now. You would have expected DoStuff(string) to have kept the parameter as a string and walked the BaseClass Method List first looking for a suitable signature, and failing that fallback to trying to cast the parameter to some other type.

But it happened the other way around... Instead Container.DoStuff(string) went, meh "theres a base class method there that fits the bill, but I'm going to convert to an IEnumerable and have a heart attack about what's available in the current class instead...

Hmmm... I'm sure Jon or Marc would be able to chime in at this point with the specific C# Spec paragraph covering this particular corner case

Original

Both Methods expect an IEnumerable Collection

You're passing an individual string.

The compiler is taking that string and going,

Ok, I have a string, Both methods expect an IEnumerable<T>, So I'll turn this string into an IEnumerable<char>... Done

Right, Check the first method... hmmm... this class is a Container<string> but I have an IEnumerable<char> so that's not right.

Check the second method, hmmm.... I have an IEnumerable<char> but char doesn't implement string so that's not right either.

COMPILER ERROR

So what#s the fix, well it completely depends what your trying to achieve... both of the following would be valid, essentially, your types usage is just incorrect in your incarnation.

        Container<char> c1 = new Container<char>();
        c1.DoStuff("Hello World");

        Container<string> c2 = new Container<string>();
        c2.DoStuff(new List<string>() { "Hello", "World" });
like image 78
Eoin Campbell Avatar answered Nov 15 '22 08:11

Eoin Campbell