C# Method overload resolution not selecting concre

2019-02-03 22:43发布

问题:

This complete C# program illustrates the issue:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

I ran into a StackOverlowException that I traced back to this call pattern where I was trying to forward calls to a more specific overload. To my surprise, the invocation was not selecting the more specific overload however, but calling back into itself. It clearly has something to do with the base type being generic, but I don't understand why it wouldn't select the Execute(string) overload.

Does anyone have any insight into this?

The above code was simplified to show the pattern, the actual structure is a bit more complicated, but the issue is the same.

回答1:

Looks like this is mentioned in the C# specification 5.0, 7.5.3 Overload Resolution:

Overload resolution selects the function member to invoke in the following distinct contexts within C#:

  • Invocation of a method named in an invocation-expression (§7.6.5.1).
  • Invocation of an instance constructor named in an object-creation-expression (§7.6.10.1).
  • Invocation of an indexer accessor through an element-access (§7.6.6).
  • Invocation of a predefined or user-defined operator referenced in an expression (§7.3.3 and §7.3.4).

Each of these contexts defines the set of candidate function members and the list of arguments in its own unique way, as described in detail in the sections listed above. For example, the set of candidates for a method invocation does not include methods marked override (§7.4), and methods in a base class are not candidates if any method in a derived class is applicable (§7.6.5.1).

When we look at 7.4:

A member lookup of a name N with K type parameters in a type T is processed as follows:

• First, a set of accessible members named N is determined:

  • If T is a type parameter, then the set is the union of the sets of
    accessible members named N in each of the types specified as a primary constraint or secondary constraint (§10.1.5) for T, along with the set of accessible members named N in object.

  • Otherwise, the set consists of all accessible (§3.5) members named N in T, including inherited members and the accessible membersnamed N in object. If T is a constructed type, the set of members is obtained by substituting type arguments as described in §10.3.2. Members that include an override modifier are excluded from the set.

If you remove override the compiler picks the Execute(string) overload when you cast the item.



回答2:

As mentioned in Jon Skeet's article on overloading, when invoking a method in a class that also overrides a method with the same name from a base class, the compiler will always take the in-class method instead of the override, regardless of the "specificness" of type, provided that the signature is "compatible".

Jon goes on to point out that this is an excellent argument for avoiding overloading across inheritance boundaries, since this is exactly the kind of unexpected behavior that can occur.



回答3:

As other answers have noted, this is by design.

Let's consider a less complicated example:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

The question is why giraffe.Eat(apple) resolves to Giraffe.Eat(Food) and not the virtual Animal.Eat(Apple).

This is a consequence of two rules:

(1) The type of the receiver is more important than the type of any argument when resolving overloads.

I hope it is clear why this must be the case. The person writing the derived class has strictly more knowledge than the person writing the base class, because the person writing the derived class used the base class, and not vice versa.

The person who wrote Giraffe said "I have a way for a Giraffe to eat any food" and that requires special knowledge of the internals of giraffe digestion. That information is not present in the base class implementation, which only knows how to eat apples.

So overload resolution should always prioritize choosing an applicable method of a derived class over choosing a method of a base class, regardless of the betterness of the argument type conversions.

(2) Choosing to override or not override a virtual method is not part of the public surface area of a class. That's a private implementation detail. Therefore no decision must be made when doing overload resolution that would change depending on whether or not a method is overridden.

Overload resolution must never say "I'm going to choose virtual Animal.Eat(Apple) because it was overridden".

Now, you might well say "OK, suppose I am inside Giraffe when I am making the call." Code inside Giraffe has all the knowledge of private implementation details, right? So it could make the decision to call virtual Animal.Eat(Apple) instead of Giraffe.Eat(Food) when faced with giraffe.Eat(apple), right? Because it knows that there is an implementation that understands the needs of giraffes that eat apples.

That's a cure worse than the disease. Now we have a situation where identical code has different behaviour depending on where it is run! You can imagine having a call to giraffe.Eat(apple) outside of the class, refactor it so that it is inside of the class, and suddenly observable behaviour changes!

Or, you might say, hey, I realize that my Giraffe logic is actually sufficiently general to move to a base class, but not to Animal, so I am going to refactor my Giraffe code to:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

And now all calls to giraffe.Eat(apple) inside Giraffe suddenly have different overload resolution behaviour after the refactoring? That would be very unexpected!

C# is a pit-of-success language; we want very much to make sure that simple refactorings like changing where in a hierarchy a method is overridden do not cause subtle changes in behaviour.

Summing up:

  • Overload resolution prioritizes receivers over other arguments because calling specialized code that knows the internals of the receiver is better than calling more general code that does not.
  • Whether and where a method is overridden is not considered during overload resolution; all methods are treated as though they were never overridden for purposes of overload resolution. It's an implementation detail, not part of the surface of the type.
  • Overload resolution problems are solved -- modulo accessibility of course! -- the same way no matter where the problem occurs in the code. We do not have one algorithm for resolution where the receiver is of the type of the containing code, and another for when the call is in a different class.

Additional thoughts on related issues can be found here: https://ericlippert.com/2013/12/23/closer-is-better/ and here https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/