Passing lambda functions as named parameters in C#

前端 未结 3 1714
春和景丽
春和景丽 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:34

    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.
    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
    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.

提交回复
热议问题