Method invocation in C# is tricky

A few days ago, a friend o mine had a very interesting issue with C# code he had written. Let’s take a look:

 


    class Program
    {
        static void Main(string[] args)
        {
            var foos = new List<Foo>();
            Bar(foos);
        }

        static void Bar<TFoo>(TFoo foo) where TFoo : Foo {}
        static void Bar<TFoo>(IEnumerable<TFoo> foos) where TFoo : Foo {}
    }

    public class Foo { }

 

There are two overloads of the generic method called Bar. The first one accepts a type parameter TFoo which is additionally restricted – it must derive from Foo type. A second overload accepts IEnumerable<TFoo> with the same constraint. Now we got to the question which for most of you will probably seem obvious. Which overload was called inside the Main method? If you picked first one… I’m sorry but you were wrong. BUT… same applies to folks who picked the second overload because the above code doesn’t even compile:

 

 

 

I must admit, I was truly confused when I saw that for the first time (for a short moment I even thought that I found a compiler error). Why is that? Well obviously most of us would say that since foos variable was declared as List<Foo> the correct overload of Bar method is number two. However, the C# compiler stubbornly tries to call the first one, generating the above error.

 

Method invocation in C#

To find out why the compiler produces that weird error, we should know the answers to two basic questions:

  • Why was the first overload picked as a better one?
  • After selecting the first overload, why didn’t the compiler perform any „fallback” to the second overload when checked TFoo parameter against generic constraint? Isn’t that what they are for?

I’m not going to explain the whole process of method invocations in C# mostly because I’m not an expert in this topic and there are still lots of things I don’t understand. However, after a while of digging into C# language specification, I found answers to the above questions.

In order to answer them, we should know (simplified) steps of choosing a method to invoke. According to C# spec on GitHub, the following steps are:

  1. A candidate set of methods is selected from a method group. Method group identifies the one method to invoke or the set of overloaded methods from which to choose a specific method to invoke.
  2. The set of candidate methods is reduced to contain only methods from the most derived types (so the most specific)
  3. If the resulting set is empty then the compiler tries to process the invocation as an extension method invocation.
  4. The best method candidate is selected from the set.
  5. Final validation of the chosen best method is performed.

The answer to the first question is definitely related to the fourth point. The C# spec describes the whole process in the section called „Better function member”. We can read that the selection of the „winner” is performed in two phases:

  1. Comparing the argument list with a parameter types of each candidate method.
  2. If the parameter type sequences of all candidates are equivalent, additional tie-breaking rules are applied to choose the best candidate method.

Let’s take a look at the description of the first phase:
 

„Given an argument list A with a set of argument expressions {E1, E2, …, En} and two applicable function members Mp and Mq with parameter types {P1, P2, …, Pn} and {Q1, Q2, …, Qn}, Mp is defined to be a better function member than Mq if

-for each argument, the implicit conversion from Ex to Qx is not better than the implicit conversion from Ex to Px, and
-for at least one argument, the conversion from Ex to Px is better than the conversion from Ex to Qx.”

 
Knowing this, let’s get back to the code from the beginning of this article and analyze it once again:

 


        static void Bar<TFoo>(TFoo foo) where TFoo : Foo {}
        static void Bar<TFoo>(IEnumerable<TFoo> foos) where TFoo : Foo {}

 

All right, for a moment forget about generic constraints we have here and focus on the parameter types of each overload. In the first one, we can call Bar without any implicit conversion like so:

 


        static void Main(string[] args)
        {
            var foos = new List<Foo>();
            Bar<List<Foo>>(foos);
        }

        static void Bar<TFoo>(TFoo foo) {}

 

Things look different in the second overload because here we do an implicit conversion from List<Foo> to IEnumerable<Foo>.

According to the rules from C# spec, Bar<TFoo>(TFoo foo) is a better candidate because for at least one (and only) argument there was a better conversion (from List<Foo> to List<Foo>), so tie-breaking rules are not needed here.

But what about the generic constraints which would change the final verdict? Well, this leads us to the fifth point of method invocation. As the language specification says:
 

„If the best method is a generic method, the type arguments (supplied or inferred) are checked against the constraints (Satisfying constraints) declared on the generic method. If any type argument does not satisfy the corresponding constraint(s) on the type parameter, a binding-time error occurs.”

 
So the mystery has been revealed! Generic constraints are checked after the best method candidate is selected. Since in our case, they weren’t fulfilled the compiler generated mentioned binding-time error.

But there’s still one more thing we should clarify. Why doesn’t the compiler perform any sort of fallback to the „less correct” but compilable candidate? Was it hard to implement? Is it an oversight in the algorithm? No, in fact it was a design decision which was confirmed by Eric Lippert on MSDN:
 

„If the best possible match between the arguments and the signature of the method identify a method that is for whatever reason not possible to call, then you need to choose your arguments more carefully so that the bad thing is no longer the best match. We figure that you want to be told that there’s a problem, rather than silently falling back to a less-good choice.”

 
Well as you can see, sometimes problems that seem very simple to solve can lead us to very interesting and complicated programming topics. I’ll definitely study this area more and who knows… maybe some interesting articles will appear on my blog. We will see 🙂

You may also like...