什么是匿名方法?真的是匿名的吗?它有名字吗?所有好的问题,让我们从它们开始,并逐步进行lambda表达式的开发。
执行此操作时:
public void TestSomething()
{
Test(delegate { Debug.WriteLine("Test"); });
}
实际发生了什么?
编译器首先决定采用该方法的“主体”,即:
Debug.WriteLine("Test");
并将其分离为一种方法。
编译器现在必须回答两个问题:
我应该在哪里放置方法?
该方法的签名应该是什么样的?
第二个问题很容易回答。
delegate {
部分回答了这一问题。该方法不带任何参数(
delegate
和
{
之间没有任何内容),并且由于我们不在乎其名称(因此“匿名”部分),因此可以这样声明该方法:
public void SomeOddMethod()
{
Debug.WriteLine("Test");
}
但是为什么要这样做呢?
让我们看一下委托(delegate),例如
Action
到底是什么。
如果我们暂时忽略.NET中的委托(delegate)实际上是多个单个“委托(delegate)”的链接列表的事实,则代表是对两件事的引用(指针):
对象实例
该对象实例上的方法
因此,有了这些知识,第一段代码实际上可以这样重写:
public void TestSomething()
{
Test(new Action(this.SomeOddMethod));
}
private void SomeOddMethod()
{
Debug.WriteLine("Test");
}
现在,问题在于,编译器无法知道
Test
对委托(delegate)的实际作用,并且由于委托(delegate)的一半是对要在其上调用该方法的实例的引用,因此
this
在在上面的示例中,我们不知道将引用多少数据。
例如,考虑上面的代码是否是一个非常大的对象的一部分,但是只是一个临时存在的对象。还应考虑到
Test
会将该委托(delegate)存储在可以长期保存的地方。这种“长时间”也将自己与那个巨大物体的生命周期联系在一起,并且长期保持对该物体的引用,这可能不是很好。
因此,编译器不仅可以创建方法,还可以创建一个类来保存它。这回答了第一个问题,我应该放在哪里?
上面的代码可以这样重写:
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)
这样就足够了。它说:“我需要一个委托(delegate),它是Action类型的(不带参数,不返回值)”。
但是,Microsoft还添加了另一种方法来定义此方法:
public void Test(Expression<Func<....>> expr)
请注意,我在此处放了一部分,即
....
部分,让我们回到第一部分。
此代码与此调用配对:
Test(() => x + 10);
实际上不会传递委托(delegate),也不会传递任何(立即)调用的内容。取而代之的是,编译器将将此代码重写为以下代码类似(但完全不一样)的代码:
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以对数据库执行。
如果我们仍将其委托(delegate)给包含IL的方法,则它将永远无法做到。它只能这样做有以下几点原因:
适用于Expression<Func<...>>
的lambda表达式中允许的语法是受限制的(无语句等)。
不带大括号的lambda语法,它告诉编译器这是一种更简单的代码
因此,让我们总结一下:
匿名方法实际上并非全部都是匿名的,它们最终会成为命名类型和命名方法,只有您不必自己为那些东西命名
引擎盖下的很多编译器魔术使事情四处移动,因此您不必
表达式和代理是查看某些相同事物的两种方法
表达式是为希望了解代码的功能和方式的框架而设计的,以便它们可以使用该知识来优化流程(例如编写SQL语句)。
委托(delegate)用于仅关注能够调用方法的框架
脚注:
这样一个简单表达式的....
部分用于表示您从表达式中获得的返回值的类型。 () => ... simple expression ...
仅允许表达式,即返回值的东西,并且不能为多个语句。因此,有效的表达式类型是:Expression<Func<int>>
,基本上,该表达式是返回整数值的函数(方法)。
请注意,“返回值的表达式”是Expression<...>
参数或类型的限制,但不是委托(delegate)的限制。如果Test
的参数类型是Action
,则这是完全合法的代码:
Test(() => Debug.WriteLine("Test"));
显然,Debug.WriteLine("Test")
不返回任何内容,但这是合法的。但是,如果Test
方法需要一个表达式,则不是,因为表达式必须返回一个值。
我是一名优秀的程序员,十分优秀!