匿名方法与lambda表达式
任何人都可以提供匿名方法和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
类型的参数,但匿名方法的想法对我来说相当新。
什么是匿名方法? 它真的是匿名的吗? 它有名字吗? 所有好的问题,让我们从他们开始,随着我们的进展,逐步完成lambda表达式。
当你这样做:
public void TestSomething() { Test(delegate { Debug.WriteLine("Test"); }); }
究竟发生了什么?
编译器首先决定采用方法的“主体”,即:
Debug.WriteLine("Test");
并将其分离成一种方法。
编译器现在必须回答两个问题:
- 我应该把方法放在哪里?
- 该方法的签名应该是什么样的?
第二个问题很容易回答。 delegate {
部分答案。 该方法不带任何参数( delegate
和{
之间没有),因为我们不关心它的名称(因此是“匿名”部分),我们可以这样声明方法:
public void SomeOddMethod() { Debug.WriteLine("Test"); }
但为什么要这样做呢?
让我们来看看代理是什么,比如Action
真的是什么。
委托是,如果我们暂时忽略.NET中的委托实际上是多个单个“委托”的链表,这是两个事物的引用(指针):
- 一个对象实例
- 该对象实例上的方法
因此,有了这些知识,第一段代码实际上可以重写为:
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.b__0); IL_000E: ldarg.0 IL_000F: ldloc.0 // CS$<>8__locals2 IL_0010: ldftn UserQuery+<>c__DisplayClass1. 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. 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之外不生成任何内容。
小于号和大于号的符号可确保您不会偶然创建与编译器为您生成的类型和/或方法匹配的类型和/或方法。
所以这基本上就是匿名方法。
那这是什么?
Test(() => Debug.WriteLine("Test"));
嗯,在这种情况下它是相同的,它是生成匿名方法的捷径。
你可以用两种方式写这个:
() => { ... code here ... } () => ... single expression here ...
在第一种forms中,您可以编写在普通方法体中执行的所有代码。 在第二种forms中,您可以编写一个表达式或语句。
但是 ,在这种情况下,编译器将对此进行处理:
() => ...
与此相同:
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> expr)
请注意,我在那里放了一个部分, ....
部分,让我们回到那个1 。
此代码与此调用配对:
Test(() => x + 10);
实际上不会传递一个委托,也不会传递任何可以立即调用的内容。 相反,编译器会将此代码重写为类似 (但完全不同)以下代码:
var operand1 = new VariableReferenceOperand("x"); var operand2 = new ConstantOperand(10); var expression = new AdditionOperator(operand1, operand2); Test(expression);
基本上,编译器将构建一个Expression
对象,其中包含对变量,文字值,使用的运算符等的引用,并将该对象树传递给该方法。
为什么?
好吧,考虑上面的db.Customers.Where(...)
部分。
如果不是将所有客户(及其所有数据)从数据库下载到客户端,循环遍历所有客户,找出哪个客户具有正确的名称等,代码实际上会要求数据库,这不是很好吗一次找到那个单一,正确的客户?
这就是表达背后的目的。 entity framework,Linq2SQL或任何其他此类支持LINQ的数据库层将采用该表达式,对其进行分析,将其拆开,并编写要针对数据库执行的格式正确的SQL。
如果我们仍然给它代表包含IL的方法,它就永远不会这样做。 它只能做这件事,因为有几件事:
- 适用于
Expression
的lambda表达式中允许的语法是有限的(没有语句等)> - 没有大括号的lambda语法,告诉编译器这是一种更简单的代码forms
那么,让我们总结一下:
- 匿名方法实际上不是所有的匿名方法,它们最终都是命名类型,使用命名方法,只有你自己不必命名这些东西
- 这是引擎盖下的很多编译器魔法,所以你不需要这样做
- 表达式和代表是两种查看某些相同内容的方法
- 表达式适用于想要知道代码的作用和方式的框架,以便他们可以使用该知识来优化流程(比如编写SQL语句)
- 代表用于仅关注能够调用该方法的框架
脚注:
-
这个简单表达式的
....
部分是指从表达式获得的返回值的类型。() => ... simple expression ...
只允许表达式 ,即返回值的东西,它不能是多个语句。 因此,有效的表达式类型是这样的:Expression
,基本上,表达式是返回整数值的函数(方法)。> 请注意,“返回值的表达式”是
Expression<...>
参数或类型的限制,但不是委托的限制。 如果Test
的参数类型是Action
那么这是完全合法的代码:Test(() => Debug.WriteLine("Test"));
显然,
Debug.WriteLine("Test")
不会返回任何内容,但这是合法的。 如果方法Test
需要一个表达式 ,那么它就不会,因为表达式必须返回一个值。
你应该注意一个微妙的区别。 考虑以下查询(使用众所周知的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
包装。 作为第一种情况的结果,Where扩展方法的最佳匹配是扩展IEnumerable而不是需要Expression
的IQueryable版本。
在这一点上,匿名代表几乎没有用处。 它更冗长,更不灵活。 一般来说,我建议总是使用lambda语法而不是匿名委托语法,并提取可解析性和语法简洁性。
确切地说,你所谓的“匿名委托”实际上是一种匿名方法。
嗯,lambdas和匿名方法都只是语法糖。 编译器将为您生成至少一个“正常”方法,尽管有时(在闭包的情况下)它将生成一个嵌套类,其中包含不再使用匿名的方法。