将lambda函数作为C#中的命名参数传递

时间:2011-11-08 16:11:15

标签: c# lambda named-parameters

编译这个简单的程序:

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

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

那里没什么奇怪的。如果我们在lambda函数体中出错:

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

编译器返回错误消息:

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

到目前为止一切顺利。现在,让我们在调用Foo

时使用命名参数
Foo( bar: () => Console.LineWrite( "42" ) );

这一次,编译器消息有些令人困惑:

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'

发生了什么事?为什么不报告实际错误?

请注意,如果我们使用匿名方法而不是lambda:

,我们会收到正确的错误消息
Foo( bar: delegate { Console.LineWrite( "42" ); } );

2 个答案:

答案 0 :(得分:6)

编辑:Eric Lippert的答案描述了(更多更好)这个问题 - 请参阅他对“真实交易”的回答

最终编辑: 对于一个人在野外公开展示他们自己的无知而言并不令人讨厌,在删除按钮的背后隐藏无知是没有收获的。希望其他人可以从我的不切实际的答案中受益:)

感谢Eric Lippert和svick的耐心和善意纠正我的错误理解!


您在此处收到“错误”错误消息的原因是因为类型的差异和编译器推断以及编译器处理命名参数的类型解析的方式

主要例子的类型 () => Console.LineWrite( "42" )

通过类型推理和协方差的魔力,这与

具有相同的最终结果

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

第一个块可以是LambdaExpressiondelegate类型;这取决于使用和推断。

鉴于此,难怪编译器在传递一个应该是Action但可能是不同类型的协变对象的参数时会感到困惑吗? 错误消息是指向类型解析的主键。

让我们看一下IL的进一步线索: 给出的所有示例都在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

请注意System.Action.Invokecallvirt调用的**正是它的样子:虚拟方法调用。

当您使用命名参数调用Foo时,您告诉编译器您正在传递Action,当您真正传递的是LambdaExpression。通常,这是编译的(请注意IL中的CachedAnonymousMethodDelegate1Action的ctor之后调用)到Action,但是由于您明确告诉编译器您正在传递操作,它会尝试使用作为LambdaExpression传入的Action,而不是将其视为表达式!

简短:由于lambda表达式中的错误(这本身就是一个严重的失败),命名参数解析失败了

这是另一个告诉:

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

产生预期的错误消息。

我对某些IL的东西可能不是100%准确,但我希望我传达了一般的想法

编辑:dlev在OP的评论中提出了一个很好的观点,即重载决策的顺序也起到了作用。

答案 1 :(得分:4)

注意:这不是一个真正的答案,但对评论来说太大了。

投入类型推断时会产生更有趣的结果。请考虑以下代码:

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

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

它不会编译,因为没有好的方法来推断T应该是什么 错误消息

  

方法'Test.Blah<T>(System.Action<T>)'的类型参数不能   从用法推断。尝试指定类型参数   明确。

有道理。让我们明确指定x的类型,看看会发生什么:

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

现在出现问题因为LineWrite不存在 错误消息

  

'System.Console'不包含'LineWrite'的定义

也是明智的。现在让我们添加命名参数,看看会发生什么。首先,不指定x的类型:

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

我们希望得到一条关于无法推断类型参数的错误消息。我们做到了。 但那不是全部 错误消息

  

方法'Test.Blah<T>(System.Action<T>)'的类型参数不能   从用法推断。尝试指定类型参数   明确。

     

'System.Console'不包含'LineWrite'的定义

纯。类型推断失败,我们被告知为什么lambda转换失败了。好的,让我们指定x的类型,看看我们得到了什么:

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

错误消息

  

方法'Test.Blah<T>(System.Action<T>)'的类型参数不能   从用法推断。尝试指定类型参数   明确。

     

'System.Console'不包含'LineWrite'的定义

现在 是意外的。类型推断仍然失败(我假设因为lambda - &gt; Action<T>转换失败,从而否定编译器猜测Tint报告失败的原因。

<强> TL; DR :当Eric Lippert到处寻找这些更复杂案例的启发式时,我会很高兴。