Passing lambda functions as named parameters in C#

前端 未结 3 1711
春和景丽
春和景丽 2021-01-31 15:54

Compile this simple program:

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

    static void Main( string[] args )
    {
        Fo         


        
3条回答
  •  温柔的废话
    2021-01-31 16:26

    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 Select(Func 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.

提交回复
热议问题