什么是匿名方法?真的是匿名的吗?它有名字吗?都是好问题,所以让我们从它们开始,随着我们的进展逐步研究 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.<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 类型(不带参数,不返回值)”。
不过,微软也添加了不同的方式来定义这个方法:
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(...)
部分。
如果不是将所有客户(以及他们的所有数据)从数据库下载到客户端,而是遍历所有客户,找出哪个客户的姓名正确等等,代码实际上会询问,这不是很好吗?数据库来一次找到那个单一的、正确的客户?
这就是表达的目的。实体框架、Linq2SQL 或任何其他此类支持 LINQ 的数据库层将采用该表达式、对其进行分析、将其分离,并编写一个格式正确的 SQL 以针对数据库执行。
如果我们仍然将其委托给包含 IL 的方法,它可能永远不会这样做。它只能这样做,因为有几件事:
- 适用于
Expression<Func<...>>
的 lambda 表达式中允许的语法是有限的(无语句等)
- 没有大括号的 lambda 语法,它告诉编译器这是一种更简单的代码形式
所以,让我们总结一下:
- 匿名方法实际上并不是那么匿名,它们最终成为命名类型,具有命名方法,只是 您不必自己命名这些东西
- 引擎盖下有很多编译器的魔力可以移动事物,因此您不必这样做
- 表达式和委托是查看某些相同事物的两种方式
- 表达式适用于希望了解代码的作用和方式的框架,以便它们可以使用这些知识来优化流程(例如编写 SQL 语句)
- 委托适用于只关心能够调用方法的框架
脚注:
-
这样一个简单表达式的....
部分表示您从表达式中获得的返回值的类型。 () => ... simple expression ...
只允许表达式,即返回值的东西,不能是多个语句。因此,一个有效的表达式类型是这样的:Expression<Func<int>>
,基本上,表达式是一个返回整数值的函数(方法)。
请注意,“返回值的表达式”是Expression<...>
参数或类型的限制,而不是委托。如果Test
的参数类型是Action
,这是完全合法的代码:
Test(() => Debug.WriteLine("Test"));
显然,Debug.WriteLine("Test")
不会返回任何内容,但这是合法的。但是,如果 Test
方法需要 表达式,则不需要,因为表达式必须返回值。