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

Dan*_*bić 35 c# lambda named-parameters

编译这个简单的程序:

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

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

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

Foo( () => Console.LineWrite( "42" ) );
Run Code Online (Sandbox Code Playgroud)

编译器返回一条错误消息:

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.现在,让我们在调用中使用命名参数Foo:

Foo( bar: () => Console.LineWrite( "42" ) );
Run Code Online (Sandbox Code Playgroud)

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

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'
Run Code Online (Sandbox Code Playgroud)

这是怎么回事?为什么不报告实际错误?

请注意,如果我们使用匿名方法而不是lambda,我们会收到正确的错误消息:

Foo( bar: delegate { Console.LineWrite( "42" ); } );
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 35

为什么不报告实际错误?

不,那就是问题; 这报告的实际错误.

让我用一个稍微复杂的例子来解释.假设你有这个:

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

好的,根据C#规范的错误是什么?你必须在这里仔细阅读规范.让我们解决吧.

  • 我们调用Select作为函数调用,只有一个参数而没有类型参数.我们对Select in CustomerCollection进行查找,搜索名为Select的可调用事物 - 即委托类型字段或方法.由于我们没有指定类型参数,因此我们匹配任何泛型方法Select.我们找到一个并从中构建一个方法组.方法组包含单个元素.

  • 现在必须通过重载分辨率来分析方法组,以首先确定候选集,然后从中确定适用的候选集,并从中确定最佳适用候选,并从中确定最终验证的最佳适用候选.如果任何这些操作失败,则重载解析必须失败并出现错误.哪一个失败了?

  • 我们从构建候选集开始.为了获得候选者,我们必须执行方法类型推断来确定类型参数R的值.方法类型推断如何工作?

  • 我们有一个lambda,其参数类型都是已知的 - 形式参数是Customer.为了确定R,我们必须从lambda的返回类型到R进行映射.λ的返回类型是什么?

  • 我们假设c是Customer并尝试分析lambda体.这样做会在Customer的上下文中查找FristNmae,并且查找失败.

  • 因此,lambda返回类型推断失败,并且没有向R添加绑定.

  • 在分析了所有参数之后,R上没有边界.因此,方法类型推断无法确定R的类型.

  • 因此方法类型推断失败.

  • 因此,候选集中没有添加任何方法.

  • 因此,候选集是空的.

  • 因此,没有适用的候选人.

  • 因此,这里的正确错误消息将是"重载解析无法找到最终验证的最佳适用候选者,因为候选集是空的".

客户会对该错误消息非常不满意.我们在错误报告算法中构建了大量的启发式算法,试图推断出用户可以实际采取措施修复错误的"基本"错误.我们的理由:

  • 实际错误是候选集是空的.候选人为什么空着?

  • 因为方法组中只有一个方法,并且类型推断失败.

好的,我们是否应该报告错误"重载解析失败,因为方法类型推断失败"?客户也会对此感到不满.相反,我们再次提出问题"为什么方法类型推断失败?"

  • 因为R的绑定集是空的.

这也是一个糟糕的错误.为什么边界设置为空?

  • 因为我们可以确定R的唯一参数是lambda,其返回类型无法推断.

好吧,我们是否应该报告错误"重载解析失败,因为lambda返回类型推断无法推断返回类型"? 此外,客户会不高兴这一点.相反,我们问的问题是"为什么lambda无法推断出返回类型?"

  • 因为Customer没有名为FristNmae的成员.

是我们实际报告错误.

因此,您会看到我们必须经历的绝对曲折的推理链,以便提供您想要的错误消息.我们不能只是说出了什么问题 - 重载决议被赋予了一个空的候选集 - 我们必须重新回到过去来确定重载决策如何进入该状态.

这样做的代码非常复杂 ; 它处理的情况比我刚才介绍的情况更复杂,包括有n种不同的泛型方法和类型推断由于不同的原因失败的情况,我们必须在所有这些情况中解决什么是给出的"最佳"理由用户.回想一下,实际上有十几种不同的Select和重载决策可能由于不同的原因或相同的原因而失败.

编译器的错误报告中有启发式处理各种过载解析失败; 我描述的只是其中之一.

现在让我们来看看你的具体情况.什么是真正的错误?

  • 我们有一个方法组,其中包含一个方法,Foo.我们可以建立候选人集吗?

  • 是.有候选人.方法Foo是调用的候选者,因为它提供了所有必需的参数 - bar - 并且没有额外的参数.

  • 好的,候选集中有一个方法.是否有候选人的适用成员?

  • 否.对应于bar的参数无法转换为形式参数类型,因为lambda正文包含错误.

  • 因此,适用的候选集是空的,因此没有最终验证的最佳适用候选者,因此重载解析失败.

那错误应该是什么?同样,我们不能只说"过载解决方案未能找到最终验证的最佳适用候选人",因为客户会讨厌我们.我们必须开始挖掘错误消息.为什么重载决策失败?

  • 因为适用的候选集是空的.

它为什么是空的?

  • 因为其中的每个候选人都被拒绝了.

有没有最好的候选人?

  • 是的,只有一名候选人.

为什么被拒绝了?

  • 因为它的参数不能转换为形式参数类型.

好的,在这一点上,显然处理涉及命名参数的重载解决问题的启发式决定我们已经挖得足够远,这就是我们应该报告的错误.如果我们没有命名参数,那么其他一些启发式问题:

为什么这个论点不可兑换?

  • 因为lambda体包含错误.

然后我们报告错误.

错误启发式并不完美 ; 离得很远.巧合的是,本周我正在对"简单"的重载分辨率错误报告启发式进行大量重新设计 - 只是说什么时候说"没有一个方法需要2个参数",什么时候说"你想要的方法是私有的" "何时说"没有与该名称相对应的参数",依此类推; 完全有可能你正在调用一个带有两个参数的方法,没有带有两个参数的那个名称的公共方法,有一个是私有的,但是其中一个有一个不匹配的命名参数.快,我们应该报告什么错误?我们必须做出最好的猜测,有时候我们可以做出更好的猜测但是不够复杂.

即使做到这一点也证明是一项非常棘手的工作.当我们最终重新构建重要的重型启发式方法时 - 比如如何处理LINQ表达式中方法类型推断的失败 - 我将重新审视你的情况,看看我们是否可以改进启发式算法.

但是,由于您收到的错误消息是完全正确的,这不是编译器中的错误; 相反,它只是在特定情况下错误报告启发式的缺点.

  • @configurator:是的.我们有一个有点弱和原始的机制,因为你可以产生一个错误和一个链接的"先前错误中的符号位置"错误.我个人希望得到你的建议:一个真正有条理的错误信息,你可以"深入"找出导致错误的整个逻辑链.我也非常喜欢重载分辨率错误而不只是说"最佳方法有不良转换",列出方法组中的*every*方法和*为什么*它没有被选为最佳方法.(我也喜欢小马.) (7认同)
  • @SolutionYogi:事实上,与错误代码的分析相比,正确代码的分析是*简单*. (5认同)
  • TL; dr:过载解析失败的错误报告启发式方法因使用命名参数而异.我认为. (4认同)
  • 埃里克,这是一个很棒的答案.像我这样的应用开发人员很容易.当我开发一个LOB应用程序时,我会在第一次出现错误时抛出异常,并提供所有相关细节.我不必编写极其复​​杂的代码来报告错误.我从未意识到C#编译器团队为向我们展示有意义的错误消息所付出的努力(错误消息的正确率为90%). (3认同)

Jos*_*h E 6

编辑:埃里克利珀的答案描述(好)的问题-请他的回答为"实打实"

最后的编辑:由于人们不喜欢在公共场合展示他们自己的无知,所以在推动删除按钮后面无知.希望其他人可以从我的不切实际的答案中受益:)

感谢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
Run Code Online (Sandbox Code Playgroud)

请注意,围绕调用的**System.Action.Invoke:callvirt正是它的样子:虚拟方法调用.

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

简短:命名参数解析失败,因为lambda表达式中的错误(这本身就是一个硬故障)

这是另一个说法:

Action b = () => Console.LineWrite("42");
Foo(bar: b);
Run Code Online (Sandbox Code Playgroud)

产生预期的错误消息.

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

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

  • 不,我的意思是问题是编译器的错误报告启发式在这里做得不够好.这与lambda是否可以转换为多种类型无关. (2认同)