匿名方法 vs. Lambda 表达式

28

有人能简明地区分匿名方法和Lambda表达式吗?

匿名方法的用法:

private void DoSomeWork()
{
    if (textBox1.InvokeRequired)
    {
        //textBox1.Invoke((Action)(() => textBox1.Text = "test"));
        textBox1.Invoke((Action)delegate { textBox1.Text = "test"; });
    }
}

这只是将普通的 Lambda 表达式转换为强类型委托,还是有更多潜在的内容。


我很清楚像下面这样强类型的委托:

UpdateTextDelegate mydelegate = new UpdateTextDelegate(MethodName)

对我来说,作为类型为System.Delegate的参数已经足够了,但是匿名方法的概念对我来说还比较新。


1
你看过这个了吗?还有其他问题吗?https://dev59.com/iG025IYBdhLWcg3wblc3 - Lasse V. Karlsen
我完全理解什么是匿名方法/lambda表达式及其用法,我的问题是它们是否与匿名委托有任何区别,还是它们只是相同的? - sarepta
2
好的,我刚刚写了一堆文本,希望你能在那里找到答案,如果没有,请告诉我 :) - Lasse V. Karlsen
3个回答

65

什么是匿名方法?它真的是匿名的吗?它有名称吗?这些都是很好的问题,让我们从这些问题开始,并随着进展逐步了解lambda表达式。

当你这样做时:

public void TestSomething()
{
    Test(delegate { Debug.WriteLine("Test"); });
}

实际发生了什么?

编译器首先决定采取方法的“body”,即:

Debug.WriteLine("Test");

将其分离成一个方法。

编译器现在需要回答两个问题:

  1. 我应该把这个方法放在哪里?
  2. 这个方法的签名应该是什么样子的?

第二个问题很容易回答。 delegate { 部分就是答案。该方法不带参数(在 delegate{ 之间没有任何内容),并且由于我们不关心它的名称(因此是“匿名”的一部分),因此我们可以这样声明该方法:

public void SomeOddMethod()
{
    Debug.WriteLine("Test");
}

但是为什么它要这样做呢?

让我们看看委托,例如 Action 到底是什么。

委托是一个指向两个东西的引用(指针),如果我们暂时忽略 .NET 中委托实际上是多个单一“委托”链接列表的事实:

  1. 对象实例
  2. 该对象实例上的方法

因此,有了这个知识,第一段代码实际上可以重写为:

public void TestSomething()
{
    Test(new Action(this.SomeOddMethod));
}

private void SomeOddMethod()
{
    Debug.WriteLine("Test");
}

现在,问题在于编译器无法知道Test对其所给的委托实际执行了什么操作。由于委托的一半是对要调用方法的实例的引用,即上面示例中的this,因此我们不知道会引用多少数据。
例如,考虑上面的代码是巨大对象的一部分,但这个对象只是临时存在的。同时考虑Test将把该委托存储在某个长期存在的位置。这个“长期存在”的时间也将与那个巨大对象的生命周期绑定在一起,并长期保留对它的引用,这可能不好。
因此,编译器不仅创建一个方法,还创建一个类来保存它。这回答了第一个问题:“我应该把它放在哪里?”。
因此,上面的代码可以重写如下:
public void TestSomething()
{
    var temp = new SomeClass;
    Test(new Action(temp.SomeOddMethod));
}

private class SomeClass
{
    private void SomeOddMethod()
    {
        Debug.WriteLine("Test");
    }
}

那就是说,在这个例子中,匿名方法的真正含义就是这样。
如果你开始使用局部变量,情况会变得有些复杂,请考虑以下示例:
public void Test()
{
    int x = 10;
    Test(delegate { Debug.WriteLine("x=" + x); });
}

这就是发生在引擎盖下的事情,或者至少非常接近它的内容:
public void TestSomething()
{
    var temp = new SomeClass;
    temp.x = 10;
    Test(new Action(temp.SomeOddMethod));
}

private class SomeClass
{
    public int x;

    private void SomeOddMethod()
    {
        Debug.WriteLine("x=" + x);
    }
}

编译器创建一个类,将方法所需的所有变量提取到该类中,并重写对局部变量的所有访问,以访问匿名类型上的字段。 类和方法的名称有点奇怪,让我们询问 LINQPad 它会是什么:
void Main()
{
    int x = 10;
    Test(delegate { Debug.WriteLine("x=" + x); });
}

public void Test(Action action)
{
    action();
}

如果我让LINQPad输出此程序的IL(中间语言),我会得到以下结果:
// var temp = new UserQuery+<>c__DisplayClass1();
IL_0000:  newobj      UserQuery+<>c__DisplayClass1..ctor
IL_0005:  stloc.0     // CS$<>8__locals2
IL_0006:  ldloc.0     // CS$<>8__locals2

// temp.x = 10;
IL_0007:  ldc.i4.s    0A 
IL_0009:  stfld       UserQuery+<>c__DisplayClass1.x

// var action = new Action(temp.<Main>b__0);
IL_000E:  ldarg.0     
IL_000F:  ldloc.0     // CS$<>8__locals2
IL_0010:  ldftn       UserQuery+<>c__DisplayClass1.<Main>b__0
IL_0016:  newobj      System.Action..ctor

// Test(action);
IL_001B:  call        UserQuery.Test

Test:
IL_0000:  ldarg.1     
IL_0001:  callvirt    System.Action.Invoke
IL_0006:  ret         

<>c__DisplayClass1.<Main>b__0:
IL_0000:  ldstr       "x="
IL_0005:  ldarg.0     
IL_0006:  ldfld       UserQuery+<>c__DisplayClass1.x
IL_000B:  box         System.Int32
IL_0010:  call        System.String.Concat
IL_0015:  call        System.Diagnostics.Debug.WriteLine
IL_001A:  ret         

<>c__DisplayClass1..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret         

在这里,您可以看到类的名称为UserQuery+<>c__DisplayClass1,方法的名称为<Main>b__0。我在生成此代码的C#代码中进行了编辑,在上面的示例中,LINQPad除了IL之外什么也没有生成。
小于号和大于号的存在是为了确保您无法意外创建与编译器为您生成的类型和/或方法相匹配的内容。
所以,这基本上就是匿名方法。
那么这是什么?
Test(() => Debug.WriteLine("Test"));

在这种情况下,它是相同的,它是生产匿名方法的快捷方式。

您可以用两种方式编写此代码:

() => { ... code here ... }
() => ... single expression here ...

在第一种形式中,您可以编写与普通方法体中相同的所有代码。在第二种形式中,您只允许编写一个表达式或语句。
然而,在这种情况下,编译器将会把它视为:
() => ...

与此相同的方式:
delegate { ... }

他们仍然是匿名方法,只是 () => 语法是到达它的快捷方式。
那么,既然这是到达它的快捷方式,为什么我们要使用它呢?
嗯,它使我们在 LINQ 中更加便捷。
考虑以下 LINQ 语句:
var customers = from customer in db.Customers
                where customer.Name == "ACME"
                select customer.Address;

这段代码被重写为如下形式:

var customers =
    db.Customers
      .Where(customer => customer.Name == "ACME")
      .Select(customer => customer.Address");

如果你使用 delegate { ... } 语法,你将不得不重写表达式并添加 return ... 等代码,这看起来会更加奇怪。因此,Lambda 语法被添加,使我们编写上述代码时更加轻松。

那么什么是表达式呢?

到目前为止,我还没有展示过如何定义 Test,但让我们为上面的代码定义 Test

public void Test(Action action)

这就可以了。它表示“我需要一个委托,其类型为Action(不带参数,不返回值)”。
然而,Microsoft还添加了一种不同的定义该方法的方式:
public void Test(Expression<Func<....>> expr)

请注意,我省略了一部分内容,即....部分,请回到1处继续阅读。
这段代码与以下调用配对使用:
Test(() => x + 10);

实际上不会传递委托或可立即调用的任何内容。相反,编译器将重写此代码为类似于以下代码的东西(但并不完全相同):

var operand1 = new VariableReferenceOperand("x");
var operand2 = new ConstantOperand(10);
var expression = new AdditionOperator(operand1, operand2);
Test(expression);

基本上编译器将构建一个Expression<Func<...>>对象,其中包含对变量、字面值、使用的运算符等的引用,并将该对象树传递给方法。
为什么?
好吧,考虑上面的db.Customers.Where(...)部分。
如果代码实际上要求数据库一次找到单个正确的客户,而不是从数据库下载所有客户(以及它们的所有数据),循环遍历它们所有人,找出哪个客户有正确的名称等,那不是很好吗?
这就是表达式背后的目的。Entity Framework、Linq2SQL或任何其他支持LINQ的数据库层将获取该表达式,分析它,拆开它,并编写出格式正确的SQL以针对数据库执行。
如果我们仍然将委托提供给包含IL的方法,它绝不能做到这一点。它之所以能够做到这一点,是因为有几件事情:
  1. Expression<Func<...>> 中适用于 lambda 表达式的语法受限(无语句等)。
  2. 省略花括号的 lambda 语法告诉编译器这是一种更简单的代码形式。

因此,让我们总结一下:

  1. 匿名方法实际上并不是完全匿名的,它们最终会成为一个有名称的类型,具有一个命名的方法,只是您不必自己命名这些内容
  2. 在幕后有很多编译器魔法来移动东西,以便您不必这样做
  3. 表达式和委托是查看某些相同事物的两种方式
  4. 表达式用于希望知道代码执行情况及其方式的框架,以便他们可以利用该知识来优化过程(例如编写SQL语句)
  5. 委托用于仅关注能够调用方法的框架

脚注:

  1. The .... part for such a simple expression is meant for the type of return value you get from the expression. The () => ... simple expression ... only allows expressions, that is, something that returns a value, and it cannot be multiple statements. As such, a valid expression type is this: Expression<Func<int>>, basically, the expression is a function (method) returning an integer value.

    Note that the "expression that returns a value" is a limit for Expression<...> parameters or types, but not of delegates. This is entirely legal code if the parameter type of Test is an Action:

    Test(() => Debug.WriteLine("Test"));
    

    Obviously, Debug.WriteLine("Test") doesn't return anything, but this is legal. If the method Test required an expression however, it would not be, as an expression must return a value.


6
“匿名”这个术语实际上非常贴切。想象一下,如果有人向一个好事捐赠了大量的钱,显然这个人有名字,只是受益者不知道。在这种情况下,这种方法的名称对您来说是未知的,但它确实有一个名字。 - Lasse V. Karlsen
优秀的讨论,但是当尝试将它们作为表达式处理时,除了语法糖之外还存在微妙的差异。如果您尝试将匿名委托传递给仅接受 Expression<Func<T>> 的方法,会发生什么? - Jim Wooley
你不能这样做。你会得到一个编译器错误,"无法将匿名方法表达式转换为表达式树"。这是在将方法声明为public void Test(Expression<Func<int>> expr)时,使用Test(delegate { return 10; });时发生的。请注意,我并不是说没有我没有涉及到的问题,如果你能找到一些问题,我很乐意扩展它们,但是将委托传递给需要表达式的方法是行不通的。 - Lasse V. Karlsen
@LasseV.Karlsen 确切地说,这就是匿名委托和Lambda表达式之间的区别。请注意,您也无法将多行Lambda作为表达式传递。 - Jim Wooley
看 .NET CORE 的代码,Lambda 函数显然会捕获在其传递的词法作用域中的方法,并将它们绑定到从中传递的实例上。这是正确的吗? - user5389726598465

8

有一个微妙的差别需要您注意。考虑以下查询(使用惯用的NorthWind数据库)。

Customers.Where(delegate(Customers c) { return c.City == "London";});
Customers.Where(c => c.City == "London");

第一个使用匿名委托,第二个使用Lambda表达式。如果您评估两者的结果,您将看到相同的结果。但是,查看生成的SQL,我们将看到完全不同的故事。第一个生成

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [Customers] AS [t0]

第二个生成的内容是
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [Customers] AS [t0]
WHERE [t0].[City] = @p0

请注意,在第一个例子中,where条件并没有被传递到数据库中。原因是什么?编译器能够确定Lambda表达式是一个简单的单行表达式,可以保留为表达式树;而匿名委托不是Lambda表达式,不能作为Expression<Func<T>>进行包装。因此,在第一个案例中,Where扩展方法的最佳匹配是扩展IEnumerable的版本,而不是IQueryable版本,后者需要一个Expression<Func<T, bool>>
此时,匿名委托几乎没有用处。它更冗长、不够灵活。通常情况下,我建议始终使用Lambda语法而非匿名委托语法,并提高可解析性和语法简洁性。

1

准确地说,你所称的“匿名委托”实际上是一个匿名方法。

嗯,无论是lambda表达式还是匿名方法都只是语法糖。编译器将为您生成至少一个“常规”方法,尽管有时(在闭包的情况下)它会生成一个嵌套类,其中包含不再是匿名的方法。


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