在C#中将lambda函数作为命名parameter passing

编译这个简单的程序:

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' 

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

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

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

为什么不报告实际的错误?

不,这是问题所在。 它正在报告实际的错误。

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

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

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

  • 我们使用一个参数调用Select作为一个函数调用,并且没有types参数。 我们在CustomerCollection的Select中进行查找,search名为Select的可调用事物 – 也就是像委托types的字段或方法。 由于我们没有指定types参数,所以我们匹配任何generics方法Select。 我们find一个,并build立一个方法组。 该方法组包含一个单一的元素。

  • 现在必须通过重载分析来分析方法组,首先确定候选集合 ,然后从中确定可应用的候选集合 ,并由此确定最佳适用候选者 ,并由此确定最终validation的最佳适用候选者 。 如果这些操作中的任何一个失败,则重载parsing必须失败并出错。 哪一个失败?

  • 我们从build立候选集开始。 为了获得候选人,我们必须执行方法types推断来确定types参数R的值。方法types推理是如何工作的?

  • 我们有一个lambda,其参数types都是已知的 – forms参数是Customer。 为了确定R,我们必须做一个从lambda的返回types到R的映射。lambda的返回types是什么?

  • 我们假设c是Customer,并试图分析lambda体。 这样做在客户的上下文中查找FristNmae,查找失败。

  • 因此,lambda返回types推断失败,并且没有绑定被添加到R.

  • 在分析了所有参数之后,R上没有界限。因此,方法types推断无法确定R的types。

  • 因此方法types推断失败。

  • 因此没有方法被添加到候选集。

  • 因此,候选集是空的。

  • 因此不可能有适用的候选人。

  • 因此,这里的正确的错误信息就像“重载决议无法find最终validation的最佳适用候选人,因为候选集是空的”。

客户会对这个错误信息非常不满意。 我们已经在错误报告algorithm中build立了相当多的启发式algorithm,试图推导出用户实际上可以采取的修正错误的更“基本”的错误。 我们的理由:

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

  • 因为方法组中只有一个方法,并且types推断失败。

好的,我们是否应该报告错误“重载parsing失败,因为方法types推断失败”? 再次,客户会不高兴的。 相反,我们再次问“为什么方法types推断失败?”

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

这也是一个糟糕的错误。 为什么界限是空的?

  • 因为从中我们可以确定R的唯一参数是一个lambda的返回types无法推断。

好,我们应该报告错误“重载决议失败,因为lambda返回types推断未能推断返回types”? 再次 ,客户会不高兴的。 相反,我们问“为什么lambda不能推断返回types?”

  • 因为客户没有名为FristNmae的成员。

是我们实际报告的错误。

所以你看到我们必须经历的绝对曲折的推理链,才能给出你想要的错误信息。 我们不能只是说出了什么问题 – 重载解决scheme被赋予了一个空的候选集 – 我们不得不重新回到过去,以确定重载parsing是如何进入该状态的。

这样做的代码非常复杂 ; 它处理比我刚刚介绍的情况更复杂的情况,包括有n种不同的generics方法,types推断由于不同的原因而失败的情况,我们必须从所有的方法中解决什么是“最好的”理由用户。 回想一下,实际上有十几种不同types的Select和重载解决scheme可能因为不同的原因或者相同的原因而失败。

在编译器的错误报告中有启发式的方法来处理各种重载parsing失败; 我所描述的只是其中之一。

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

  • 我们有一个方法组,其中有一个方法,Foo。 我们可以build立一个候选集?

  • 是。 有一个候选人。 Foo方法是调用的候选对象,因为它提供了所有必需的参数 – bar – 并且没有额外的参数。

  • 好的,候选集有一个单一的方法。 有没有适合的候选人?

  • 不可以。对应于bar的参数不能转换为forms参数types,因为lambda体包含一个错误。

  • 因此,适用的候选集是空的,因此没有最终validation的最佳适用候选者,因此重载解决失败。

那么错误应该是什么? 同样,我们不能只是说“重载决议未能find最终validation的最佳适用人选”,因为客户会讨厌我们。 我们必须开始挖掘错误消息。 为什么重载决议失败?

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

为什么是空的?

  • 因为每个候选人都被拒绝了。

有最好的候选人吗?

  • 是的,只有一个候选人。

为什么被拒绝?

  • 因为它的论点不能转换成forms参数types。

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

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

  • 因为lambda身体包含一个错误。

然后我们报告错误。

错误启发式algorithm并不完美 ; 离得很远。 巧合的是,我本周做了一个“简单的”重载parsing错误报告启发式的重构架构 – 就像什么时候说“没有一个方法需要2个参数”以及何时说“你想要的方法是私有的“什么时候说”没有与该名称对应的参数“等等; 你有两个参数调用一个方法是完全可能的,没有这个名字的公共方法有两个参数,有一个是私有的,但是其中一个有一个不匹配的命名参数。 快,我们应该报告什么错误? 我们必须做一个最好的猜测,有时候我们可以做出更好的猜测,但是做得还不够成熟。

即使做对了,也是一件非常棘手的工作。 当我们最终重新构build大型的重启式启发式时 – 比如如何处理LINQexpression式中的方法types推断失败 – 我会重新审视你的情况,看看我们是否可以改进启发式。

但是由于你得到的错误信息是完全正确的 ,所以这不是编译器中的一个错误。 相反,这仅仅是特定情况下的错误报告启发式的缺点。

编辑:埃里克Lippert的答案描述(好得多)的问题 – 请参阅他的答案为“真正的交易”

最后的编辑:就像人们在野外公开示范自己的无知一样,在按下删除button后面没有隐瞒愚蠢的行为。 希望别人可以从我的quixotic答案:)

感谢Eric Lippert,并且耐心地善待我的错误理解!


你在这里得到'错误的'错误信息的原因是因为types的差异和编译器推理以及编译器如何处理命名参数的typesparsing

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

通过types推断和协方差的魔法,这与最终的结果是一样的

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

第一个块可以是LambdaExpressiondelegatetypes; 这是取决于使用和推理。

鉴于此,难道编译器会在你传递一个本应该是Action的参数时感到困惑,但是这个参数可能是一个不同types的协变对象? 错误信息是指向typesparsing成为问题的主要关键。

让我们来看看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.Invoke调用周围的**: callvirt正是它的样子:一个虚拟的方法调用。

当你用一个命名参数调用Foo时,你告诉编译器你正在传递一个Action ,当你真正传递的是一个LambdaExpression 。 通常,这是编译的(注意IL中的CachedAnonymousMethodDelegate1Action之后调用),但是由于您明确告诉编译器您正在传递一个操作,它将尝试使用传入的LambdaExpression作为Action ,而不是把它当作expression!

Short:命名参数parsing失败,因为lambdaexpression式中的错误(这本身就是一个严重失败)

这是另一个说法:

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

产生预期的错误信息。

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

编辑:dlev在OP的评论中也提到了一个很重要的问题,那就是重载决议的顺序也起了一个作用。

注意:不是一个真正的答案,但太大而不能发表评论。

当您inputtypes推断时,更有趣的结果。 考虑这个代码:

 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>)'的types参数不能从用法中推断出来。 尝试明确指定types参数。

说得通。 让我们明确指定x的types,看看会发生什么:

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

现在事情发生错误,因为LineWrite不存在。
错误消息

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

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

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

我们希望得到一个关于不能推断types参数的错误信息。 而我们呢。 但是,这不是全部
错误消息

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

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

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

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

错误消息

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

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

现在是意想不到的。 types推断仍然失败(我假设因为lambda – > Action<T>转换失败,因此否定编译器猜测Tint报告失败的原因。

TL; DR :当Eric Lippert开始研究这些更复杂的案例的启发式时,我会很高兴。