什么是匿名方法?它真的是匿名的吗?它有名称吗?这些都是很好的问题,让我们从这些问题开始,并随着进展逐步了解lambda表达式。
当你这样做时:
public void TestSomething()
{
Test(delegate { Debug.WriteLine("Test"); });
}
实际发生了什么?
编译器首先决定采取方法的“body”,即:
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(不带参数,不返回值)”。
然而,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的方法,它绝不能做到这一点。它之所以能够做到这一点,是因为有几件事情:
Expression<Func<...>>
中适用于 lambda 表达式的语法受限(无语句等)。
- 省略花括号的 lambda 语法告诉编译器这是一种更简单的代码形式。
因此,让我们总结一下:
- 匿名方法实际上并不是完全匿名的,它们最终会成为一个有名称的类型,具有一个命名的方法,只是您不必自己命名这些内容
- 在幕后有很多编译器魔法来移动东西,以便您不必这样做
- 表达式和委托是查看某些相同事物的两种方式
- 表达式用于希望知道代码执行情况及其方式的框架,以便他们可以利用该知识来优化过程(例如编写SQL语句)
- 委托用于仅关注能够调用方法的框架
脚注:
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.