【问题标题】:Anonymous methods vs. lambda expression [duplicate]匿名方法与 lambda 表达式
【发布时间】:2023-09-10 19:22:01
【问题描述】:

谁能提供匿名方法和 lambda 表达式之间的简明区别?

匿名方法的使用:

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

是否只是一个普通的 lambda 表达式被强制转换为强类型委托,还是有更多的秘密。

我很清楚,像 follow 这样的强类型委托

UpdateTextDelegate mydelegate = new UpdateTextDelegate(MethodName)

作为System.Delegate 类型的参数就足够了,但是匿名方法的想法对我来说是相当新的。

【问题讨论】:

  • 你看过这个吗?可能还有其他问题? *.com/questions/6008097/…
  • 我完全理解什么是匿名方法/lambda 表达式及其用法,我的问题是它们与匿名代表有什么不同还是完全相同
  • 好吧,我刚刚写了一大堆文字,所以希望你能在那里找到答案,如果不告诉我:)

标签: c# lambda delegates anonymous-methods


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    什么是匿名方法?真的是匿名的吗?它有名字吗?都是好问题,所以让我们从它们开始,随着我们的进展逐步研究 lambda 表达式。

    当你这样做时:

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

    实际发生了什么?

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

    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+&lt;&gt;c__DisplayClass1,方法名是&lt;Main&gt;b__0。我在生成此代码的 C# 代码中进行了编辑,LINQPad 除了上面示例中的 IL 之外什么都不生成。

    小于和大于号用于确保您不会意外创建与编译器为您生成的内容相匹配的类型和/或方法。

    所以这基本上就是匿名方法。

    那么这是什么?

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

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

    你可以用两种方式写这个:

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

    在它的第一种形式中,您可以编写您在普通方法体中执行的所有代码。在第二种形式中,您可以编写一个表达式或语句。

    然而,在这种情况下编译器会这样处理:

    () => ...
    

    同理:

    delegate { ... }
    

    它们仍然是匿名方法,只是() =&gt; 语法是获取它的捷径。

    所以,如果它是实现它的捷径,我们为什么要拥有它?

    嗯,它使生活变得更轻松,因为它被添加的目的是 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 类型(不带参数,不返回值)”。

    不过,微软也添加了不同的方式来定义这个方法:

    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&lt;Func&lt;...&gt;&gt; 对象,其中包含对变量的引用、文字值、使用的运算符等,并将该对象树传递给方法。

    为什么?

    好吧,考虑上面的db.Customers.Where(...) 部分。

    如果不是将所有客户(以及他们的所有数据)从数据库下载到客户端,而是遍历所有客户,找出哪个客户的姓名正确等等,代码实际上会询问,这不是很好吗?数据库来一次找到那个单一的、正确的客户?

    这就是表达的目的。实体框架、Linq2SQL 或任何其他此类支持 LINQ 的数据库层将采用该表达式、对其进行分析、将其分离,并编写一个格式正确的 SQL 以针对数据库执行。

    如果我们仍然将其委托给包含 IL 的方法,它可能永远不会这样做。它只能这样做,因为有几件事:

    1. 适用于Expression&lt;Func&lt;...&gt;&gt; 的 lambda 表达式中允许的语法是有限的(无语句等)
    2. 没有大括号的 lambda 语法,它告诉编译器这是一种更简单的代码形式

    所以,让我们总结一下:

    1. 匿名方法实际上并不是那么匿名,它们最终成为命名类型,具有命名方法,只是 您不必自己命名这些东西
    2. 引擎盖下有很多编译器的魔力可以移动事物,因此您不必这样做
    3. 表达式和委托是查看某些相同事物的两种方式
    4. 表达式适用于希望了解代码的作用和方式的框架,以便它们可以使用这些知识来优化流程(例如编写 SQL 语句)
    5. 委托适用于只关心能够调用方法的框架

    脚注:

    1. 这样一个简单表达式的.... 部分表示您从表达式中获得的返回值的类型。 () =&gt; ... simple expression ... 只允许表达式,即返回值的东西,不能是多个语句。因此,一个有效的表达式类型是这样的:Expression&lt;Func&lt;int&gt;&gt;,基本上,表达式是一个返回整数值的函数(方法)。

      请注意,“返回值的表达式”是Expression&lt;...&gt; 参数或类型的限制,而不是委托。如果Test 的参数类型是Action,这是完全合法的代码:

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

      显然,Debug.WriteLine("Test") 不会返回任何内容,但这是合法的。但是,如果 Test 方法需要 表达式,则不需要,因为表达式必须返回值。

    【讨论】:

    • 术语“匿名”实际上是一个非常好的术语。考虑一下是否有人为一个好的事业捐了一大笔钱。显然那个人有名字,只是得到钱的人不知道。在这种情况下,您不知道该方法的名称,但它确实有一个名称。
    • 优秀的论述,但在尝试将这些作为表达式处理时,除了语法糖之外还有一个细微的差别。如果您尝试将匿名委托传递给只接受 Expression&lt;Func&lt;T&gt;&gt; 的方法会发生什么?
    • 你不能那样做。您收到编译器错误,“无法将匿名方法表达式转换为表达式树”。当方法被声明为public void Test(Expression&lt;Func&lt;int&gt;&gt; expr) 时,这是来自Test(delegate { return 10; });。请注意,我并不是说没有我没有触及的东西,所以如果你能找到一些我很乐意扩展这些,但是将委托传递给需要表达式的方法是不行的去吧。
    • @LasseV.Karlsen 准确地说。这就是匿名委托和 lambda 表达式之间的区别。请注意,您也不能将多行 lambda 作为表达式传递。
    • 查看 .NET CORE 代码,lambda 函数显然在其传递它的词法范围内捕获方法,并将它们绑定到传递它的实例。对吗?
    【解决方案3】:

    您应该注意一个细微的差别。考虑以下查询(使用众所周知的 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&lt;Func&lt;T&gt;&gt;。因此,在第一种情况下,Where 扩展方法的最佳匹配是扩展 IEnumerable 的方法,而不是需要 Expression&lt;Func&lt;T, bool&gt;&gt; 的 IQueryable 版本。

    此时,匿名委托几乎没有用处。它更冗长,更不灵活。一般来说,我建议始终使用 lambda 语法而不是匿名委托语法,并同时提高可解析性和语法简洁性。

    【讨论】: