匿名方法与lambda表达式

时间:2013-07-17 19:53:02

标签: c# lambda delegates anonymous-methods

任何人都可以在匿名方法和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类型的参数,但匿名方法的想法对我来说是新的。

3 个答案:

答案 0 :(得分:37)

一个匿名方法?它真的是匿名的吗?它有名字吗?所有好的问题,所以让我们从他们开始,随着我们的进展,逐步完成lambda表达式。

执行此操作时:

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

实际发生了什么?

编译器首先决定采用" body"方法,这是:

Debug.WriteLine("Test");

并将其分解为方法。

编译器现在必须回答两个问题:

  1. 我应该把方法放在哪里?
  2. 该方法的签名应该是什么样的?
  3. 第二个问题很容易回答。 delegate {部分回答了这个问题。该方法不使用任何参数(delegate{之间没有任何参数),因为我们不关心它的名称(因此"匿名"部分),我们可以声明这样的方法:

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

    但为什么要这么做呢?

    让我们看看代理人是什么,例如Action

    如果我们暂时忽略了一个事实,即.NET中的委托实际上是多个单个"委托"的链接列表这一事实,那么一个委托是指向两个的引用(指针)事情:

    1. 对象实例
    2. 该对象实例上的方法
    3. 因此,有了这些知识,第一段代码实际上可以重写为:

      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); });
      }
      

      编译器创建一个类,将该方法所需的所有变量提升到该类中,并重写对局部变量的所有访问权限,以访问匿名类型的字段。

      班级名称和方法有点奇怪,让我们问LINQPad它会是什么:

      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输出该程序的IL(中级语言),我明白了:

      void Main()
      {
          int x = 10;
          Test(delegate { Debug.WriteLine("x=" + x); });
      }
      
      public void Test(Action action)
      {
          action();
      }
      

      您可以在此处看到该类的名称为// 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。我编写了生成此代码的C#代码,LINQPad除了以上示例中的IL之外没有产生任何内容。

      小于号和大于号的符号确保您不会偶然创建与编译器为您生成的类型和/或方法相匹配的类型和/或方法。

      这基本上就是匿名方法。

      那是什么?

      <Main>b__0

      嗯,在这种情况下,它是相同的,它是生成匿名方法的捷径。

      你可以用两种方式写出来:

      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 { ... }等重写表达式,并且它们看起来更加时髦。因此添加了lambda语法,以便在编写上述代码时让我们的程序员更轻松。

      那么表达式是什么?

      到目前为止,我还没有说明return ...的定义方式,但让我们为上述代码定义Test

      Test

      这应该足够了。它说&#34;我需要一个委托,它是Action类型(不带参数,不返回值)&#34;。

      但是,Microsoft还添加了一种不同的方法来定义此方法:

      public void Test(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<Func<...>>部分。

      如果不是将所有客户(及其所有数据)从数据库下载到客户端,循环遍历所有客户,找出哪个客户拥有正确的名称等,那么这样做会不会很好。代码会实际上要求数据库立即找到那个单一,正确的客户?

      表达背后的目的。实体框架,Linq2SQL或任何其他支持LINQ的数据库层将采用该表达式,分析它,将其拆开,并编写一个格式正确的SQL以对数据库执行。

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

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

      3. 所以,让我们总结一下:

        1. 匿名方法实际上不是所有匿名方法,它们最终都是命名类型,使用命名方法,只有您不必自己命名
        2. 引擎盖下的很多编译魔术让你不必
        3. 表达式和代表是两种查看某些相同内容的方法
        4. 表达式适用于想要了解代码的作用以及的框架,以便他们可以使用该知识来优化流程(如编写SQL语句)
        5. 代表适用于仅关注能够调用方法的框架

        6. 脚注:

          1. 这个简单表达式的Expression<Func<...>>部分是指从表达式获得的返回值的类型。 ....只允许表达式,即返回值的东西,它不能是多个语句。因此,有效的表达式类型为:() => ... simple expression ...,基本上,表达式是返回整数值的函数(方法)。

            请注意&#34;表达式返回值&#34;是Expression<Func<int>>参数或类型的限制,但不是委托的限制。如果Expression<...>的参数类型为Test

            ,则这是完全合法的代码
            Action

            显然,Test(() => Debug.WriteLine("Test")); 并没有返回任何内容,但这是合法的。如果方法Debug.WriteLine("Test")需要表达式,那么它就不会,因为表达式必须返回一个值。

答案 1 :(得分:5)

你应该注意一个微妙的区别。考虑以下查询(使用众所周知的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而不是需要Expression<Func<T, bool>>的IQueryable版本。

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

答案 2 :(得分:1)

准确地说,你所谓的'匿名委托'实际上是一种匿名方法。

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