35

Compile this simple program:

class Program
{
    static void Foo( Action bar )
    {
        bar();
    }

    static void Main( string[] args )
    {
        Foo( () => Console.WriteLine( "42" ) );
    }
}

Nothing strange there. If we make an error in the lambda function body:

Foo( () => Console.LineWrite( "42" ) );

the compiler returns an error message:

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'

So far so good. Now, let's use a named parameter in the call to Foo:

Foo( bar: () => Console.LineWrite( "42" ) );

This time, the compiler messages are somewhat confusing:

error CS1502: The best overloaded method match for 
              'CA.Program.Foo(System.Action)' has some invalid arguments 
error CS1503: Argument 1: cannot convert from 'lambda expression' to 'System.Action'

What's going on? Why doesn't it report the actual error?

Note that we do get the correct error message if we use an anonymous method instead of the lambda:

Foo( bar: delegate { Console.LineWrite( "42" ); } );
Danko Durbić
  • 7,077
  • 5
  • 34
  • 39
  • 13
    @MatějZábský The point of the post was that the C# compiler isn't fully explaining what the problem is when a named parameter is introduced. `LineWrite` is the example of the compilation error. He already knows what the error is; just why the compiler isn't displaying it that way all the time. – vcsjones Nov 08 '11 at 16:15
  • Using braces seems to make the error [more clear](http://www.ideone.com/Mso6R) (Although I [still don't](http://www.ideone.com/fP7Qm) see a problem negating them. -- unless the mono command-line compiler is just more explicit) – Brad Christie Nov 08 '11 at 16:18
  • 3
    @Brad It's not the braces; it's that ideone.com is using the Mono compiler, rather than the MSFT C# compiler. (Which suggests that this may just be a bug.) – dlev Nov 08 '11 at 16:20
  • @dlev: indeed, was more a curiosity thing than an actual reason; Interesting mono does a better job at reporting the true error. – Brad Christie Nov 08 '11 at 16:21
  • @BradChristie: It's the same error, with or without braces on Visual C# 2010 compiler. – Danko Durbić Nov 08 '11 at 16:22

3 Answers3

36

Why doesn't it report the actual error?

No, that's the problem; it is reporting the actual error.

Let me explain with a slightly more complicated example. Suppose you have this:

class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

OK, what is the error according to the C# specification? You have to read the specification very carefully here. Let's work it out.

  • We have a call to Select as a function call with a single argument and no type arguments. We do a lookup on Select in CustomerCollection, searching for invocable things named Select -- that is, things like fields of delegate type, or methods. Since we have no type arguments specified, we match on any generic method Select. We find one and build a method group out of it. The method group contains a single element.

  • The method group now must be analyzed by overload resolution to first determine the candidate set, and then from that determine the applicable candidate set, and from that determine the best applicable candidate, and from that determine the finally validated best applicable candidate. If any of those operations fail then overload resolution must fail with an error. Which one of them fails?

  • We start by building the candidate set. In order to get a candidate we must perform method type inference to determine the value of type argument R. How does method type inference work?

  • We have a lambda whose parameter types are all known -- the formal parameter is Customer. In order to determine R, we must make a mapping from the return type of the lambda to R. What is the return type of the lambda?

  • We assume that c is Customer and attempt to analyze the lambda body. Doing so does a lookup of FristNmae in the context of Customer, and the lookup fails.

  • Therefore, lambda return type inference fails and no bound is added to R.

  • After all the arguments are analyzed there are no bounds on R. Method type inference is therefore unable to determine a type for R.

  • Therefore method type inference fails.

  • Therefore no method is added to the candidate set.

  • Therefore, the candidate set is empty.

  • Therefore there can be no applicable candidates.

  • Therefore, the correct error message here would be something like "overload resolution was unable to find a finally-validated best applicable candidate because the candidate set was empty."

Customers would be very unhappy with that error message. We have built a considerable number of heuristics into the error reporting algorith that attempts to deduce the more "fundamental" error that the user could actually take action on to fix the error. We reason:

  • The actual error is that the candidate set was empty. Why was the candidate set empty?

  • Because there was only one method in the method group and type inference failed.

OK, should we report the error "overload resolution failed because method type inference failed"? Again, customers would be unhappy with that. Instead we again ask the question "why did method type inference fail?"

  • Because the bound set of R was empty.

That's a lousy error too. Why was the bounds set empty?

  • Because the only argument from which we could determine R was a lambda's whose return type could not be inferred.

OK, should we report the error "overload resolution failed because lambda return type inference failed to infer a return type"? Again, customers would be unhappy with that. Instead we ask the question "why did the lambda fail to infer a return type?"

  • Because Customer does not have a member named FristNmae.

And that is the error we actually report.

So you see the absolutely tortuous chain of reasoning we have to go through in order to give the error message that you want. We can't just say what went wrong -- that overload resolution was given an empty candidate set -- we have to dig back into the past to determine how overload resolution got into that state.

The code that does so is exceedingly complex; it deals with more complicated situations than the one I just presented, including cases where there are n different generic methods and type inference fails for m different reasons and we have to work out from among all of them what is the "best" reason to give the user. Recall that in reality there are a dozen different kinds of Select and overload resolution on all of them might fail for different reasons or the same reason.

There are heuristics in the error reporting of the compiler for dealing with all kinds of overload resolution failures; the one I described is just one of them.

So now let's look at your particular case. What is the real error?

  • We have a method group with a single method in it, Foo. Can we build a candidate set?

  • Yes. There is a candidate. The method Foo is a candidate for the call because it has every required parameter supplied -- bar -- and no extra parameters.

  • OK, the candidate set has a single method in it. Is there an applicable member of the candidate set?

  • No. The argument corresponding to bar cannot be converted to the formal parameter type because the lambda body contains an error.

  • Therefore the applicable candidate set is empty, and therefore there is no finally validated best applicable candidate, and therefore overload resolution fails.

So what should the error be? Again, we can't just say "overload resolution failed to find a finally validated best applicable candidate" because customers would hate us. We have to start digging for the error message. Why did overload resolution fail?

  • Because the applicable candidate set was empty.

Why was it empty?

  • Because every candidate in it was rejected.

Was there a best possible candidate?

  • Yes, there was only one candidate.

Why was it rejected?

  • Because its argument was not convertible to the formal parameter type.

OK, at this point apparently the heuristic that handles overload resolution problems that involve named arguments decides that we've dug far enough and that this is the error we should report. If we do not have named arguments then some other heuristic asks:

Why was the argument not convertible?

  • Because the lambda body contained an error.

And we then report that error.

The error heuristics are not perfect; far from it. Coincidentally I am this week doing a heavy rearchitecture of the "simple" overload resolution error reporting heuristics -- just stuff like when to say "there wasn't a method that took 2 parameters" and when to say "the method you want is private" and when to say "there's no parameter that corresponds to that name", and so on; it is entirely possible that you are calling a method with two arguments, there are no public methods of that name with two parameters, there is one that is private but one of them has a named argument that does not match. Quick, what error should we report? We have to make a best guess, and sometimes there is a better guess that we could have made but were not sophisticated enough to make.

Even getting that right is proving to be a very tricky job. When we eventually get to rearchitecting the big heavy duty heuristics -- like how to deal with failures of method type inference inside of LINQ expressions -- I'll revisit your case and see if we can improve the heuristic.

But since the error message you are getting is completely correct, this is not a bug in the compiler; rather, it is merely a shortcoming of the error reporting heuristic in a particular case.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 4
    tl; dr: The error reporting heuristics for overload resolution failure are different depending on whether or not named arguments are used. I think. – dlev Nov 08 '11 at 20:32
  • @Eric Lippert: +1 great f/u on your comment! I was fumbling around in the dark with a flashlight; your answer is a mega-spotlight – Josh E Nov 08 '11 at 20:32
  • 3
    Eric, this is a fantastic answer. Application developers like me have it easy. When I develop a LOB application, I throw an Exception at the first chance of an error with all relevant details. I don't have to write exceedingly complex code just to report an error. I never realized the amount of effort C# compiler team puts to show us meaningful error messages (90% of the time the error message has been correct). – SolutionYogi Nov 08 '11 at 22:36
  • 5
    @SolutionYogi: Indeed, analysis of correct code is *easy* compared to analysis of erroneous code. – Eric Lippert Nov 08 '11 at 22:46
  • Have you considered showing multiple errors for the same thing, ordered by some sort of heuristic? That would require a different UI in Visual Studio so that you can have "1 error: (1) `Console` does not have a method called `LineWrite`. (2) The lambda could not be converted to a delegate." The way I envisage this idea, Visual Studio would show one error, and would have (+) next to it to show other possible causes for the same error. – configurator Nov 09 '11 at 17:51
  • 7
    @configurator: Yes. We have a somewhat weak and primitive mechanism for this, in that you can produce an error and a linked "location of symbol in previous error" error. I personally would love to have what you suggest: a truly structured error message that you could "dive into" to trace out the whole chain of logic that led to the error. I would also really like overload resolution errors to instead of just saying "best method had bad conversion", to list *every* method that was in the method group and *why* it was not chosen as the best method. (I'd also like a pony.) – Eric Lippert Nov 09 '11 at 18:25
  • @configurator: It is unlikely that we'll get features like that into the compiler proper any time soon; however, there will be better overload resolution analysis available through the Roslyn API, probably. – Eric Lippert Nov 09 '11 at 18:38
  • @EricLippert: Do you think Visual Studio could use that API? – configurator Nov 09 '11 at 19:11
  • Awesome answer. The only thing I take issue with is being worried about customers being unhappy with an error message. Microsoft neutered their OS error messages so badly that they thought "An error has occurred" by itself was acceptable. – Brain2000 Nov 19 '21 at 14:27
6

EDIT: Eric Lippert's answer describes (much better) the issue - please see his answer for the 'real deal'

FINAL EDIT: As unflattering as it is for one to leave a public demonstration of their own ignorance in the wild, there's no gain in veiling ignorance behind a push of the delete button. Hopefully someone else can benefit from my quixotic answer :)

Thanks Eric Lippert and svick for being patient and kindly correcting my flawed understanding!


The reason that you are getting the 'wrong' error message here is because of variance and compiler-inference of types combined with how the compiler handles type resolution of named parameters

The type of the prime example () => Console.LineWrite( "42" )

Through the magic of type inference and covariance, this has the same end result as

Foo( bar: delegate { Console.LineWrite( "42" ); } );

The first block could be either of type LambdaExpression or delegate; which it is depends on usage and inference.

Given that, is it no wonder that the compiler gets confused when you pass it a parameter that's supposed to be an Action but which could be a covariant object of a different type? The error message is the main key that points toward type resolution being the issue.

Let's look at the IL for further clues: All of the examples given compile to this in LINQPad:

IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0005:  brtrue.s    IL_0018
IL_0007:  ldnull      
IL_0008:  ldftn       UserQuery.<Main>b__0
IL_000E:  newobj      System.Action..ctor
IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_001D:  call        UserQuery.Foo

Foo:
IL_0000:  ldarg.0     
**IL_0001:  callvirt    System.Action.Invoke**
IL_0006:  ret         

<Main>b__0:
IL_0000:  ldstr       "42"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret

Note the ** around the call to System.Action.Invoke: callvirt is exactly what it seems like: a virtual method call.

When you call Foo with a named argument, you're telling the compiler that you're passing an Action, when what you're really passing is a LambdaExpression. Normally, this is compiled (note the CachedAnonymousMethodDelegate1 in the IL called after the ctor for Action) to an Action, but since you explicitly told the compiler you were passing an action, it attempts to use the LambdaExpression passed in as an Action, instead of treating it as an expression!

Short: named parameter resolution fails because of the error in the lambda expression (which is a hard failure in and of itself)

Here's the other tell:

Action b = () => Console.LineWrite("42");
Foo(bar: b);

yields the expected error message.

I'm probably not 100% accurate on some of the IL stuff, but I hope I conveyed the general idea

EDIT: dlev made a great point in the comments of the OP about the order of overload resolution also playing a part.

Josh E
  • 7,390
  • 2
  • 32
  • 44
  • If the code actually worked with `LambdaExpression`, as you claim, the IL would contain code that would actually create the expression, using calls like `Expression.Lambda`. But that's not what happens. – svick Nov 08 '11 at 17:22
  • It's not my claim that it uses LambdaExpression, it's the facts of the situation. The error message says "cannot convert from 'lambda expression' to 'System.Action'" - the IL doesn't need code to create an Expression.Lambda because it thinks that the expression has already been created. – Josh E Nov 08 '11 at 18:23
  • Though a valiant attempt, the problem here has nothing to do with variance. – Eric Lippert Nov 08 '11 at 19:57
  • no? what is the correct term for when you can use `x => ...` to represent any one of a) an Action, a Func, (inc. Lambda), Expression, etc? Perhaps my vocabulary needs updating – Josh E Nov 08 '11 at 20:10
  • 2
    No, I mean that the problem is that the error reporting heuristics of the compiler are not doing a good enough job here. That has nothing to do with whether the lambda is convertible to multiple types. – Eric Lippert Nov 08 '11 at 20:28
  • hmmm not sure how much of my answer actually applies anymore. Is there utility in keeping this here? It could confuse readers – Josh E Nov 08 '11 at 20:45
  • @JoshE, `LambdaExpression` is a specific type, that's used when converting a lambda expression to expression tree. The code here doesn't use `LambdaExpression`, but it is a lambda expression. – svick Nov 09 '11 at 02:40
  • ok, so I was confusing the terms `LambdaExpression` as a formal type with 'lambda expression' as a syntactic element? I had always assumed that the type was part of the syntax of `=>`, and were one and the same. Thanks for clearing that up for me! Gotta pay better attention to capitalization hints in those error messages... – Josh E Nov 09 '11 at 03:43
4

Note: Not really an answer, but far too big for a comment.

More interesting results when you throw in type-inference. Consider this code:

public class Test
{
    public static void Blah<T>(Action<T> blah)
    {
    }

    public static void Main()
    {
        Blah(x => { Console.LineWrite(x); });
    }
}

It won't compile, because there's no good way to infer what T should be.
Error Message:

The type arguments for method 'Test.Blah<T>(System.Action<T>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Makes sense. Let's specify the type of x explicitly and see what happens:

public static void Main()
{
    Blah((int x) => { Console.LineWrite(x); });
}

Now things go awry because LineWrite doesn't exist.
Error Message:

'System.Console' does not contain a definition for 'LineWrite'

Also sensible. Now let's add in named arguments and see what happens. First, without specifying the type of x:

public static void Main()
{
    Blah(blah: x => { Console.LineWrite(x); });
}

We would expect to get an error message about not being able to infer type arguments. And we do. But that's not all.
Error Messages:

The type arguments for method 'Test.Blah<T>(System.Action<T>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

'System.Console' does not contain a definition for 'LineWrite'

Neat. Type inference fails, and we're told exactly why the lambda conversion failed. Ok, so let's specify the type of x and see what we get:

public static void Main()
{
    Blah(blah: (int x) => { Console.LineWrite(x); });
}

Error Messages:

The type arguments for method 'Test.Blah<T>(System.Action<T>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

'System.Console' does not contain a definition for 'LineWrite'

Now that is unexpected. Type inference is still failing (I assume because the lambda -> Action<T> conversion is failing, thus negating the compiler's guess that T is int) and reporting the cause of the failure.

TL; DR: I'll be glad when Eric Lippert gets around to looking at the heuristics for these more complex cases.

dlev
  • 48,024
  • 5
  • 125
  • 132
  • 3
    You are correct that in the last case the compiler has gone off the rails. It *should* have been possible to infer the type argument corresponding to T here, and we should not be reporting that type inference has failed. I think this is a bug in the interaction between named argument mapping and method type inference. Named arguments greatly complicate both the specification and the implementation, and we accidentally introduced a great many subtle bugs like this when we implemented them. – Eric Lippert Nov 09 '11 at 00:47