C# lambda表达式是否可以不声明变量/参数?

12
当一个方法以 lambda 表达式作为参数,例如 Enumerable.Where,在表达式中没有声明变量或方法参数时,它被称为什么?
例如,我熟悉这种 lambda 表达式语法:
public string GetDigits(string input)
{
    return new String(input.Where(i => Char.IsDigit(i)).ToArray());
}

然而,我很惊讶地发现这也可以写成:

public string GetDigits(string input)
{
    return new String(input.Where(Char.IsDigit).ToArray());
}

在第二个代码片段中发生了什么,Char.IsDigit()方法被(显然)隐含参数调用了?这种语法叫做什么?

ReSharper还会在适用的情况下建议简化语法。 - Uwe Keim
那么它就不再是一个lambda表达式,而是将委托作为参数传递给另一个方法。 - Patrik
1
您正在查看一个方法组 - Jeroen Vannevel
2
你可能想看一下这个问题和答案:https://dev59.com/6G865IYBdhLWcg3wYNjj - Dennis_E
喜欢在StackOverflow上强大的C#社区。感谢提供这些优秀的答案! :-) - Jon Schneider
1
顺便提一下,“丢弃参数”的能力被称为η-转换(发音为eta-conversion),大致意思是arg => fct (arg)与直接使用fct“对象”(而不是函数的应用)是相同的。这在适用时允许一些可能的编译器优化(例如,请参见@Dennis_E的链接)。 - Sehnsucht
4个回答

17

方法不接受lambda作为参数,它们接受delegate作为参数。Lambda只是创建委托的一种方式。

另一种方式是提供方法组,就像在您的第二个示例中所做的那样,该方法组可以转换为委托。

类似的方式是使用匿名方法特性。虽然当添加Lambda时,匿名方法功能基本上被替换掉了,因此您不会经常看到它。使用该语法的示例如下:

Func<char, bool> predicate = delegate(char c) { return Char.IsDigit(c); };

另一种方法是使用Delegate.CreateDelegate创建委托对象。(尽管这不是经常见到的方法。)

最后一种方法是使用其他方式创建的委托变量。(其他方式将使用上述其中一种选项创建委托。)

在第二个片段中发生了什么,似乎调用了Char.IsDigit()方法并带有隐式参数? 这种语法叫什么名字?

实际上它并没有被调用。这正是关键所在。我们试图创建一个委托对象。委托对象是一个跟踪要调用的方法和要在哪个对象上调用该方法的对象。然后可以调用该委托对象,它将调用用于创建它的方法。
因此,在这里,您并没有调用IsDigit,而是创建指向IsDigit方法的委托对象,并且每当该委托对象被调用时,它都会调用该方法。

使用lambda表达式时,创建了一个新的方法,可能是在新类中(其名称在运行时可能不具备名称),该匿名方法的主体将调用IsDigit。然后,lambda表达式解析为指向该匿名方法的委托对象,其保持具有方法的语义,当调用该方法时,它将调用匿名方法,该匿名方法在其实现中调用IsDigit。这添加了一层间接性(在运行时可能会被优化掉),以完成相同的事情。


当您使用lambda时,您正在创建一个新类,但仅当lambda关闭实例变量或字段时,否则它将被缓存在调用类中。 - Yuval Itzchakov
能否使用一个非静态方法作为委托,而该方法本身无法直接访问?因此,如果 Char.IsDigit 不是静态的,你仍然可以将其用作委托吗? - Tim Schmelter
1
@TimSchmelter,您可以使用非静态方法的方法组来创建委托。唯一要求是方法组可转换为委托的前提条件是该方法组中存在一个最佳重载,其与所需委托的签名匹配且唯一。如果没有重载具有正确的签名或存在歧义,则会出现错误。 - Servy

11

Enumerable.Where 的签名是:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate
)

这个:

input.Where(i => Char.IsDigit(i))

等价于写成:

Func<char, bool> temp = i => Char.IsDigit(i);
input.Where(temp);

所以它创建了一个带有参数i的匿名函数,并调用Char.IsDigit

这个:

input.Where(Char.IsDigit)

等同于

Func<char, bool> temp = Char.IsDigit;
input.Where(temp);

等同于:

Func<char, bool> temp = new Func<char, bool>(Char.IsDigit);
input.Where(temp);

它创建了一个委托到Char.IsDigit,然后将其传递给input.Where方法。

因此,第二个方法移除了"中间人"(即匿名函数)。在这种特定情况下,这是"合法的",因为匿名函数的i参数被直接传递给了Char.IsDigit。如果情况不同,那么结果就会不同:

input.Where(i => !Char.IsDigit(i))

在这种情况下,您无法删除中间人(匿名函数)。

对于所有这些内容,没有名称(或者您可以将第一个称为“创建并传递代理到匿名函数”,第二个称为“创建并传递从方法组创建的代理”……但它们不是美丽的口号,它们更像是对你正在做什么的描述)


所有这些没有一个名称。但实际上有:这就是方法组。请参见https://dev59.com/9nNA5IYBdhLWcg3wn_bD - Jeroen Vannevel
@JeroenVannevel 不,方法组是 input.Where(Char.IsDigit) 中的 Char.IsDigit。从方法组创建委托(这就是所做的)没有名称(或其名称正是从方法组创建委托)。 - xanatos
@TimSchmelter 被偷了 :-) - xanatos
@xanatos 没有一个动作名称可以代表“在期望委托时使用方法组”,但你可以使用这个(或类似的)来解释正在发生的事情。你是正确的,没有一个单词或单一术语可以代表该概念,但你至少可以简洁明了地表达这个概念。 - Servy
1
@Servy 没错,我已经添加了一些文本来进行简短的描述,而不是使用标语。 - xanatos

8
由于编译器会在找到单个符合预期签名的方法时,将方法组隐式转换为委托,因此本例中需要一个以单个字符作为输入并返回布尔值的委托。请注意保留HTML标记。

5
您的Where期望一个Func<char, bool>,它是一个接受char参数并返回bool的方法的委托。与此委托匹配的任何内容都是该Where的有效参数。
  • 您最初编写的lambda通过类型推断与此委托匹配:基于可枚举源的通用参数,编译器期望 i char,并将返回类型推断为bool,因为这是lambda内部的方法调用表达式会返回的值。
  • Char.IsDigit方法本身也符合此要求。因此,引用该方法是表示相同内容的另一种有效方式。这称为方法组。

如果考虑到对于每个lambda表达式,编译器都会生成一个匿名方法,并将该匿名方法传递给期望该委托的位置,则这两个可能的Where参数的语义等价性也是有意义的。

为了说明这一点,请考虑您的原始代码片段:

Where(i => Char.IsDigit(i))

上述代码由编译器进行了转换,转换结果请参考此处
bool AnAnonymousMethod(char i)
{
    return Char.IsDigit(i);
}

然后:

Where(AnAnonymousMethod)

正如您所看到的,在没有捕获变量的情况下,lambda语法仅仅是一种用于编写匿名方法并将此新编写的方法的方法组作为参数传递到任何兼容的委托所需位置的语法糖。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接