C # Method overload resolution without selecting a specific general override

This complete C # program illustrates the problem:

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 came across a StackOverlowException exception that I traced back to this call pattern, where I was trying to redirect calls to a more specific overload. To my surprise, the call did not choose a more specific overload, but called back into itself. Obviously, this is due to the fact that the base type is generic, but I don’t understand why it will not choose Execute (string) overload.

Can anyone figure this out?

The above code has been simplified to show the pattern, the actual structure is a little more complicated, but the problem is the same.

+43
generics c # overload-resolution
Mar 29 '17 at 23:09
source share
3 answers

This seems to be mentioned in the C # 5.0 specification, 7.5.3. Overload Resolution:

Overload resolution selects a member of a function to call in the following various contexts inside C #:

  • A call to a method named in an expression call (§7.6.5.1).
  • Calling the instance constructor named in the object-object expression (§7.6.10.1).
  • Invoking an indexer using an access element (§7.6.6).
  • Call a predefined or user-defined operator specified in an expression (§7.3.3 and §7.3.4).

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

When we look at 7.4:

The search for an element of name N with parameters of type K in type T is processed as follows:

• First, the set of available elements with the name N is determined:

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

  • Otherwise, the set consists of all available (§3.5) members with the name N in T, including inherited elements and accessible members with the name N in the 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 the override , the compiler chooses the Execute(string) overload when you put the element.

+30
Mar 29 '17 at 23:21
source share

As Jon Skeet mentioned in the article about overloading when calling a method in a class that also overrides a method with the same name from the class base, the compiler will always use the in-class method instead of overriding, regardless of the "specificity" of the type, provided that the signature " compatible. "

John continues to point out that this is a great argument to avoid overloading the boundaries of inheritance, as this is exactly the unexpected behavior that can happen.

+22
Mar 29 '17 at 23:21
source share

As other answers noted, this is by design.

Consider a less complex 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) resolved by Giraffe.Eat(Food) , and not virtual Animal.Eat(Apple) .

This is a consequence of two rules:

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

I hope this is clear why this should be so. The person who writes the derived class has more knowledge than the person who writes the base class, because the person who writes the derived class used the base class, and not vice versa.

The person who wrote Giraffe said: "I have a way for Giraffe eat any food," and this requires special knowledge about the internal digestion of the giraffe. This information is missing from the base class implementation, which only knows how to eat apples.

Thus, overload resolution should always be a priority for choosing the applicable method of the derived class when choosing the method of the base class, regardless of how efficient the conversion of the argument type is.

(2) The choice of overriding or not overriding the virtual method is not part of the total surface area of ​​the class. This is a private implementation. Therefore, when deciding on an overload resolution, no decision should be made, depending on whether the method is overridden or not.

Overload resolution should never say, "I'm going to select Virtual Animal.Eat(Apple) because it has been overridden."

Now you can say "OK, suppose I am in the Giraffe when I call." The code inside Giraffe has all the knowledge of private implementation details, right? Thus, the solution Animal.Eat(Apple) instead of Giraffe.Eat(Food) called instead of giraffe.Eat(apple) , right? Because he knows that there is a realization that understands the needs of giraffes who eat apples.

This medicine is worse than a disease. Now we have a situation where identical code has a different behavior depending on where it is running! You can imagine calling giraffe.Eat(apple) outside the class, reorganizing it so that it is inside the class, and suddenly changing behaviors!

Or, you can say, hey, I understand that my Giraffe logic is actually quite general to go to the base class, but not to Animal, so I'm going to reorganize 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 unexpectedly have different behavior when overloaded after refactoring? That would be very unexpected!

C # is the language of successful success; we really want to make sure that simple refactoring, such as changing the place in the hierarchy, the method is redefined, do not cause subtle changes in behavior.

Summarizing:

  • Overload resolution prioritizes receivers for other arguments, because calling a specialized code that knows the internals of a receiver is better than calling a more general code that does not.
  • Whether and when the method is redefined when overload resolution is resolved; all methods are treated as if they were never overridden for the purpose of overload resolution. This is an implementation detail, not part of a type surface.
  • Overload resolution issues resolved - modulo availability of course! - the same, no matter where the problem occurs in the code. We do not have one resolution algorithm, where the receiver is of the type containing the code, and the other is when the call is in another 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 /

+16
Mar 30 '17 at 22:41
source share



All Articles