6

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?

Community
  • 1
  • 1
Colin Burnett
  • 11,150
  • 6
  • 31
  • 40
  • 3
    I agree this seems weird, but it is correct according to the spec. This is a consequence of the interaction of two rules: (1) overload resolution applicability checking happens BEFORE constraint checking, and (2) applicable methods in derived classes are ALWAYS better than applicable methods in base classes. Both are reasonably sensible rules; they just happen to interact particularly badly in your case. – Eric Lippert May 28 '09 at 21:32
  • 1
    For details see section 7.5.5.1, specifically the bits that say: (1) "If the best method is a generic method, the type arguments (supplied or inferred) are checked against the constraints..." and (2) "the set of candidate methods is reduced to contain only methods from the most derived types..." – Eric Lippert May 28 '09 at 21:37
  • 2
    Ultimately your problem here is a design problem. You are overloading a method "DoStuff" to mean both "do stuff to a single value of type T", and "do stuff to a sequence of values of type T". This runs into serious "intention resolution" problems in many ways -- for example, when "type T" is itself a sequence. You will find that the existing collection classes in the BCL have been carefully designed to avoid this problem; methods that take an item are called "Frob", methods that take a sequence of items are called "FrobRange", for example "Add" and "AddRange" on lists. – Eric Lippert May 28 '09 at 21:56
  • Eric, you bring up excellent points. Can you answer a question for me? Exactly what rule(s) make the extension method solution work? – dss539 May 28 '09 at 22:19
  • 2
    This is a consequence of the rule that an applicable method that is actually ON a type is ALWAYS better than an extension method. Extension methods are only checked as a last resort; we don't want extension methods "hijacking" methods on your type. So in this case, the applicable base class method is discovered and used, and the extension method is never checked. Only if the class contains no applicable methods will we check for the extension method. – Eric Lippert May 28 '09 at 23:06
  • @Eric, cheers for the clarifications. Really appreciate you weighing in on this one and spreading the knowledge. – Eoin Campbell May 28 '09 at 23:16
  • Thanks, Eric. So basically it means that the base class's methods have precedence over the extension method. I also notice that the "most specific" extension method is preferred over the "most general" so you can derive from Container and override its DoStuff extension method even. – dss539 May 29 '09 at 00:46

6 Answers6

3

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" });
Eoin Campbell
  • 43,500
  • 17
  • 101
  • 157
  • So the compiler picks an overload by only the types of the parameters and not the constraints placed upon them (not as a whole of parameters+constraints)? That seems rather weak and half-baked. It *can* find a method that it *can* use but it's opting *not* to. – Colin Burnett May 28 '09 at 18:22
  • no it can't... neither of your methods satisfy the type your passing in. Container can currently only execute a DoStuff(IEnumerable) OR DoStuff(IEnumerable) <-- which is nothing since string is a sealed class. – Eoin Campbell May 28 '09 at 18:27
  • It can't? What happened to BaseContainer.DoStuff(T)? Regarding your fix edit. I want a container of string and the string "Hello World" to be DoStuff'ed. If I can't fix the class then DoStuff(new string[] {"Hello World"}); is what I want, but that's a really crummy API to provide (I think). – Colin Burnett May 28 '09 at 18:30
  • 1
    Your first sentence of your first comment is correct. The compiler picks the overload based on the types of the parameters, and not the constraints. The constraints are not part of the _signature_. Your third sentence is also correct. Your second sentence is not correct; this is neither weak nor half-baked. These rules were carefully designed, extremely strong, and are fully baked. – Eric Lippert May 28 '09 at 21:42
3

As Eric Lippert explained, the compiler chooses the DoStuff<Tother>(IEnumerable<Tother>) where Tother : T {} method because it chooses methods before checking constraints. Since string can do IEnumerable<>, the compiler matches it to that child class method. The compiler is working correctly as described in the C# specification.

The method resolution order you desire can be forced by implementing DoStuff as an extension method. Extension methods are checked after base class methods, so it will not try to match string against DoStuff's IEnumerable<Tother> until after it has tried to match it against DoStuff<T>.

The following code demonstrates the desired method resolution order, covariance, and inheritance. Please copy/paste it into a new project.

This biggest downside I can think of so far is that you can not use base in the overriding methods, but I think there are ways around that (ask if you are interested).

using System;
using System.Collections.Generic;

namespace MethodResolutionExploit
{
    public class BaseContainer<T> : IEnumerable<T>
    {
        public void DoStuff(T item) { Console.WriteLine("\tbase"); }
        public IEnumerator<T> GetEnumerator() { return null; }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return null; }
    }        
    public class Container<T> : BaseContainer<T> { }
    public class ContainerChild<T> : Container<T> { }
    public class ContainerChildWithOverride<T> : Container<T> { }
    public static class ContainerExtension
    {
        public static void DoStuff<T, Tother>(this Container<T> container, IEnumerable<Tother> collection) where Tother : T
        {
            Console.WriteLine("\tContainer.DoStuff<Tother>()");
        }
        public static void DoStuff<T, Tother>(this ContainerChildWithOverride<T> container, IEnumerable<Tother> collection) where Tother : T
        {
            Console.WriteLine("\tContainerChildWithOverride.DoStuff<Tother>()");
        }
    }

    class someBase { }
    class someChild : someBase { }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("BaseContainer:");
            var baseContainer = new BaseContainer<string>();
            baseContainer.DoStuff("");

            Console.WriteLine("Container:");
            var container = new Container<string>();
            container.DoStuff("");
            container.DoStuff(new List<string>());

            Console.WriteLine("ContainerChild:");
            var child = new ContainerChild<string>();
            child.DoStuff("");
            child.DoStuff(new List<string>());

            Console.WriteLine("ContainerChildWithOverride:");
            var childWithOverride = new ContainerChildWithOverride<string>();
            childWithOverride.DoStuff("");
            childWithOverride.DoStuff(new List<string>());

            //note covariance
            Console.WriteLine("Covariance Example:");
            var covariantExample = new Container<someBase>();
            var covariantParameter = new Container<someChild>();
            covariantExample.DoStuff(covariantParameter);

            // this won't work though :(
            // var covariantExample = new Container<Container<someBase>>();
            // var covariantParameter = new Container<Container<someChild>>();
            // covariantExample.DoStuff(covariantParameter);

            Console.ReadKey();
        }
    }
}

Here is the output:

BaseContainer:
        base
Container:
        base
        Container.DoStuff<Tother>()
ContainerChild:
        base
        Container.DoStuff<Tother>()
ContainerChildWithOverride:
        base
        ContainerChildWithOverride.DoStuff<Tother>()
Covariance Example:
        Container.DoStuff<Tother>()

Can you see any problems with this work around?

dss539
  • 6,804
  • 2
  • 34
  • 64
  • Hmm, interesting idea. I didn't put those overloads on BaseContainer because I'm trying to keep that class very basic. I could easily put DoStuff on BaseContainer but that kind defeats my goal of keeping BaseContainer basic. Extension means I technically keep BaseContainer the way I want it but... :) – Colin Burnett May 28 '09 at 19:50
  • Colin, it *doesn't* put the overloads on the BaseContainer. i.e. it doesn't junk up BaseContainer's intellisense. Try it please. If I misunderstood, please clarify. – dss539 May 28 '09 at 19:53
  • My apologies, I misread the method prototype. Using an extension on a generic class does, however, mean that I have to invoke it by c.DoStuff instead of just c.DoStuff, right? – Colin Burnett May 28 '09 at 20:16
  • I think you missed another piece.... Console.WriteLine(c.DoStuff(new someChild())); Console.WriteLine(c.DoStuff(test)); So no, you invoke it as you please. No need to specify type parameters. I strongly urge you to copy paste this code into a new console app just to test it. I think you might like it. – dss539 May 28 '09 at 22:23
2

The compiler will try to match the parameter to IEnumerable<T>. The String type implements IEnumerable<char>, so it assumes that T is "char".

After that, the compiler checks the other condition "where OtherT : T", and that condition is not met. Hence the compiler error.

Philippe Leybaert
  • 168,566
  • 31
  • 210
  • 223
2

My GUESS, and this is a guess because I don't really know, is that it first looks in the derived class to resolve the method call (because your object is that of the derived type). If, and only if it can't, it moves on to looking at the base classes methods to resolve it. In your case, since it CAN resolve it using the

DoStuff <Tother>(IEnumerable<Tother> collection)

overload, it tried to jam it into that. So it CAN resolve it as far as the parameter is concerned, but then it hits a snag on the constraints. At that point, it's already resolved your overload, so it doesn't look any further, but just throws up an error. Make sense?

BFree
  • 102,548
  • 21
  • 159
  • 201
  • Except it's implicitly assuming Tother = char regardless of the fact that I'm not specifying that. I have never seen the generic on a method just simply assumed because it can jam the argument into one. – Colin Burnett May 28 '09 at 18:16
  • 1
    Have you never used LINQ? Most of LINQ is built on the idea that the type parameter may be assumed if the type is provided on one of the method parameters. – Matthew Whited May 28 '09 at 18:33
  • 2
    You are specifying that Tother is char, by passing in an IEnumerable, aka "string". You might not be familiar with the "method type inference" feature of C#, but it has been around since C# 2.0. See the "type inference" section of my blog if you want details on how the method type inference algorithms work; they are quite fascinating. – Eric Lippert May 28 '09 at 21:44
1

I think it has something to do with the fact that char is a value type and string is a reference type. It looks like you're defining

TOther : T

and char does not derive from string.

n8wrl
  • 19,439
  • 4
  • 63
  • 103
0

I'm not really clear on what you're trying to accomplish, what's stopping you from just using two methods, DoStuff(T item) and DoStuff(IEnumerable<T> collection)?