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" ); } );
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.
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.
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.
来源:https://stackoverflow.com/questions/8053491/passing-lambda-functions-as-named-parameters-in-c-sharp