在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" ); } );
第一个块可以是LambdaExpression
或delegate
types; 这是取决于使用和推理。
鉴于此,难道编译器会在你传递一个本应该是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中的CachedAnonymousMethodDelegate1
在Action
之后调用),但是由于您明确告诉编译器您正在传递一个操作,它将尝试使用传入的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>
转换失败,因此否定编译器猜测T
是int
) 并报告失败的原因。
TL; DR :当Eric Lippert开始研究这些更复杂的案例的启发式时,我会很高兴。