65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET Framework 内部探秘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (78投票s)

2012年7月8日

CPOL

49分钟阅读

viewsIcon

213056

downloadIcon

1554

深入 .NET Framework 的核心,一窥 IL 的奥秘!

1. 目录  

  1. 目录 
  2. 引言 
  3. IL 简介
  4. 日常 .NET 代码的内部运作 
  5. 使用 VB 或 C# 发出 IL
  6. 使用表达式树生成 IL 
  7. F# 的奇妙案例 
  8. 后记

2. 引言  

一年多以前,当我刚接触 .NET 和编程不久时,我听了一场 Bart de Smet 的讲座,题为“10 个 C# 语言特性的幕后揭秘”。当时我对大多数特性都不熟悉,对 C# 也不熟悉(我是一名 VB 程序员),我甚至从未听说过中间语言(IL,它无处不在!),即使是经验丰富的 C# 专家也觉得这场讲座很难。可想而知,我度过了愉快的时光!虽然那天我几乎没听懂 Bart 说的什么,但这激发了我。它激发我去探索我编写的代码之外的东西,也激发我写下这篇文章。上周我再次回顾 Channel9 上的讲座时,我终于明白了 Bart 的意思。

以下是该讲座视频的链接: Bart de Smet - 10 个 C# 语言特性的幕后揭秘

在本文中,我将引用此视频,以帮助您更好地理解我正在讨论的主题。不用说,我强烈建议您观看整个视频,但这不是理解本文的必要条件。本文是视频的文字版吗?绝对不是!虽然视频和本文有重叠之处,但我希望它们大部分是互补的。

另一个灵感和知识的来源是我最近为 Manning Publications 评论的一本书,名为《 Metaprogramming in .NET by Kevin Hazzard and Jason Bock》。不幸的是,这本书尚未出版,但将于今年秋季发行。入门章节是免费提供的,所以我建议您阅读。同样,阅读这本书对于理解本文不是必需的,但我可能会建议您阅读某个章节,以帮助您更好地理解我正在讨论的主题(这样我就不必在书出版后编辑我的文章了)。更新:Metaprogramming 书已于上个月出版,非常棒。绝对必读!

再次更新
另一本最近出版的书也讨论了我文章中描述的许多内容。如果您想了解更多关于 C# 编译器背后发生了什么,这是一本很好的补充读物。而且远不止于此。本书由备受尊敬的 CP 成员 Mohammad Rahman 撰写。所以一定要阅读它:《 Expert C# 5.0 With The .NET 4.5 Framework》。我已更新本文中的引用,在适用时引用了本书。

那么,理解本文需要什么呢?首先,需要一点毅力和善意。这篇文章篇幅很长,我明白这一点。此外,本文讨论的主题并不容易,但我会确保您能掌握。对中间语言(IL)有一些了解会很有帮助。如果您从未听说过中间语言(或 IL),或者您知道它并且觉得它非常可怕(我完全理解),请不要放弃,我稍后会解释。此外,我们还将看到一些 .NET 结构,您可能熟悉或不熟悉,例如自动属性、匿名类型、Lambda 表达式和迭代器方法。同样,不用担心,这听起来比实际要难。

我建议您不要一次性阅读完这篇文章。时不时地休息一下,将其加入书签,第二天晚上继续阅读。让新学到的知识在您的大脑中沉淀下来,然后再继续。希望您阅读愉快。那么,我们准备好了吗?开始吧! 

3. IL 简介 

正如我所说,我们将要看 IL(中间语言)。这 IL 是什么?你可以说它是梦想的源泉!您编写的所有 .NET 代码都将被编译成 IL。这意味着您用 .NET 编写的代码(C#、VB、F# 等)将被转换成这种语言。然后,这些 IL 会被转换成机器码,以便您的软件能够执行。您点击了那个链接吗?请注意,IL 指的是一个更广泛的概念,并非 .NET 特有?那是因为当我提到 IL 时,我实际上指的是 CIL MSIL!所以,实际上是我们今天要看的是这种 Common IL 或 Microsoft IL。我们为什么要看 IL 呢?从我们的 VB 或 C# 代码中,我们难道看不出发生了什么吗?当然,但真正发生的事情只有在 IL 层面(甚至汇编器层面,但我们暂且不去那里)才能看出来。例如,VB 中的 For Each 或 C# 中的 foreach 不能直接转换为 IL。实际上发生的是对 IEnumerable IEnumerator 接口的一些方法调用,正如我们将在本文后面的示例中看到的。

在我们看 IL 示例之前,让我告诉你为什么要学习 IL。首先,学习一门新语言可以很有趣,任何你学到的新语言都会让你更容易学习下一门语言。其次,IL 不同于 VB 或 C# 这样的高级语言。学习其他语言如何处理事物会让你思考如何用自己的语言进行编码。无论你是否利用这些新知识,都取决于你。至少你知道有其他的选择。学习 IL 的另一个好原因是,它能让你更深入地理解 .NET 在底层是如何工作的。无论你是否重视这些知识,都取决于你,但至少你可以向同事吹嘘。更实际的学习 IL 的原因是因为你可以使用 Reflection.Emit 在运行时编写、编译和执行自己的 IL。这样做可能很有用,因为使用 IL,你可以使用 VB 或 C# 中不可用的语言构造。作为奖励,Reflection.Emit 比你找到的任何其他动态代码生成都要快。我们将在本文末尾看到一个例子。我听到你在想,你以前从未需要过这个,现在为什么需要?事实是,你可能不需要,但了解可用的选项对你很有好处。

所以,你现在一定很想看一些 IL!让我们先看一个简单的“Hello World”示例(是的,真的)。打开 Visual Studio,用 VB 或 C# 创建一个新的控制台应用程序。将以下代码粘贴到(无参数的)Main 方法中。

Sub Main()
   Dim s As String = "Hello IL!"
   Console.WriteLine(s)
   Console.ReadKey()
End Sub 
static void Main()
{
   string s = "Hello IL!";
   Console.WriteLine(s);
   Console.ReadKey();
}

无论你能否看到,当你构建时,IL 都被发出了。你可以通过使用中间语言反汇编器(简称为 IL DASM)来查看程序集的 IL。它包含在 Microsoft SDK 中,如果你安装了 Visual Studio 2010,应该很容易找到它(你可以直接使用“查找”工具搜索 ILDASM)。如果你找不到它,你可以在这里下载一个旧版本的 ILDASM。所以,让我们启动它,你应该会看到一个窗口,看起来像这样。

现在尝试打开你刚刚创建的控制台应用程序。转到“文件”->“打开”,然后选择 ConsoleApplication(确保你已保存并构建了项目)。它应该在 bin\debug 文件夹中。现在你应该会看到一个包含命名空间、类和方法的树形视图。

你可以双击任何方法来查看它的 IL。无论你是用 VB 还是 C# 创建控制台应用程序,结果都一样。IL 将基本相同。我们将要查看的 IL 部分如下,对于 VB 和 C# 来说,它应该是相同的。

  .locals init ([0] string s)
  IL_0000:  nop
  IL_0001:  ldstr      "Hello IL!"
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000d:  nop
  IL_000e:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_0013:  pop
  IL_0014:  ret
} // end of method Program::Main

糟糕!这代码真是太可怕了!不,事实并非如此。我们将逐行查看。但在那之前,你有一件事需要了解 IL。IL 是一种基于堆栈的语言。这意味着变量只能推送到堆栈上(你可以把它想象成一个值的堆栈),并且必须按照它们被推入堆栈的顺序被“消耗”。所以,让我们看看代码示例。在第一行,我们看到 .locals init ([0] string s)。这还需要解释吗?这只是我们在程序中声明的 string s 的 IL 声明。下一行是 nop,这准确地描述了它的作用,即“无操作”。我们将忽略遇到的任何 nop 操作码。我刚才说操作码了吗?是的,因为我们在这里看到的是一个操作码,即操作码(OPeration CODE),它告诉机器该做什么。基本上,IL 中看到的一切都是一个操作码,所以它比听起来要容易得多。让我们继续下一行代码。这才是真正有趣的地方!ldstr "Hello IL!"ldstr 操作码意味着一个 string 将被推送到堆栈上。在这种情况下,该字符串是 "Hello IL!"。下一行,stloc.0 将这个 string 存储到局部变量 0 中(基本上,它将堆栈上的第一个项,即 string Hello IL!,存储到局部变量 0 中)。那么局部变量 0 是什么?再看第一行,.locals init ([0] string s)。答案就在那里,就是 string s

下一行是 ldloc.0。你能猜出它做什么吗?它将局部变量 0 的值加载到堆栈上。你会看到很多以 st 或 ld 开头的操作码。可以肯定地说,它们通常表示 STore(存储)和 LoaD(加载),记住这一点会很有帮助。那么,当 "Hello IL!" 回到堆栈上时,IL 将做什么呢?它将调用 System.Console.Writeline,该方法接受一个 string 作为参数。此时,堆栈上的 string 被消耗,堆栈再次变空。Console.Writeline 中发生了什么对我们来说是未知的(如果你想查找,请随意)。每当代码从 Console.Writeline 返回时,IL 都会调用 System.Console.ReadKey,该方法返回一个 System.ConsoleKeyInfo 值类型并将其放在堆栈上。由于我们不使用这个 ConsoleKeyInfo,下一条指令是 popPop 只是从堆栈上取走第一个值。这就结束了示例,并且发出了 ret,表示返回到调用代码。

这有那么难吗?我认为没有。本文中还会看到更多操作码,但你已经掌握了 IL 的基本知识,一种基于堆栈的语言。

深入阅读 
ILDASM.exe 教程 
OpCodes 类操作码列表 
《Metaprogramming in .NET》一书的第 5 章
《Expert C# 5.0》一书的第 15 章
nop 操作码的用途? 

4. 日常 .NET 代码的内部运作 

既然你已经看到了一个简单的“Hello World”程序,让我们来看看一些更有趣的 IL。实际上,让我们来看看一些你可能没有从代码中预料到的 IL。打开本文顶部可下载的示例应用程序之一。UnderTheHoodVBUnderTheHoodCSharp 都可以。暂时忽略 TheCuriousCaseOfFSharp。打开解决方案后,你会看到两个项目。一个项目包含一些 Windows Forms,另一个项目包含一些被其他项目中的使用的或不使用的 WinForms。事实是我们不会运行部分代码,它们只是用于理论分析。我们将运行的代码主要是为了演示代码确实如我所说的那样工作。你也可以打开 ILDASM 并打开 UnderTheHoodVB.Examples.dllUnderTheHoodCSharp.Examples.dll,因为这正是我们将主要关注的。这些 dll 文件可以在各自项目文件夹的 bin\debug 文件夹中找到。那么,准备好了吗?让我们来看第一个 IL 示例!

4.1. 属性的案例
在 CP 的 QA 部分我经常看到一个问题:“公共字段和属性有什么区别?”或者“为什么我应该使用属性而不是 get 和 set 函数?”让我们先来看看第一个问题。在你的解决方案中,打开 AutoPropertyClassPropertyClassGetterSetterClass(这三个都位于 PropertyExample 文件夹下)。你会发现以下代码。
Public Class AutoPropertyClass
   Public Property Text As String
   Public Property Number As Integer
End Class 
 
Public Class PropertyClass
 
   Private _Text As String
   Public Property Text() As String
      Get
         Return _Text
      End Get
      Set(ByVal value As String)
         _Text = value
      End Set
   End Property
 
   Private _Number As Integer
   Public Property Number() As Integer
      Get
         Return _Number
      End Get
      Set(ByVal value As Integer)
         _Number = value
      End Set
   End Property
 
End Class
 
Public Class GetterSetterClass
 
   Private _Text As String
   Private _Number As Integer
 
   Public Function get_Text() As String
      Return _Text
   End Function
 
   Public Sub set_Text(ByVal value As String)
      _Text = value
   End Sub
 
   Public Function get_Number() As Integer
      Return _Number
   End Function
 
   Public Sub set_Number(ByVal value As Integer)
      _Number = value
   End Sub
 
End Class
class AutoPropertyClass
{
   public string Text { get; set; }
   public int Number { get; set; }
}
 
class PropertyClass
{
   private string _Text;
   public string Text
   {
      get { return _Text; }
      set { _Text = value; }
   }
 
   private int _Number;
   public int Number
   {
      get { return _Number; }
      set { _Number = value; }
   }
}
 
 
class GetterSetterClass
{
   private string _Text;
   private int _Number;
 
   public string get_Text()
   {
      return _Text;
   }
 
   public void set_Text(string value)
   {
      _Text = value;
   }
 
   public int get_Number()
   {
      return _Number;
   }
 
   public void set_Number(int value)
   {
      _Number = value;
   }
}

三个看起来完全不同的。然而,如果你打开 ILDASM 并查看生成的代码,你会发现这些类实际上几乎是相同的!AutoPropertyClassPropertyClass 甚至完全相同。编译器实际上为自动属性生成了后备字段,以及 get 和 set 函数,也就是你在 GetterSetterClass 中看到的那些。我们还看到了什么?Property 仅仅是 get 和 set 函数的包装器(这些是 ILDASM 中的红色三角形,在 GetterSetterClass 的 IL 中不存在)。

那么,当我们获取或设置Property的值时会发生什么?这可以在 PropertyUser 类中看到。它包含三个方法,一个获取和设置 AutoPropertyClass 中的Property,一个对 PropertyClass 执行相同操作,还有一个调用 GetterSetterClass 中的 get 和 set 函数。生成的 IL?对于所有这三个方法来说,它都完全相同!

  .locals init ([0] int32 n,
           [1] class UnderTheHoodVB.Examples.PropertyExample.PropertyClass p,
           [2] string t)
  IL_0000:  nop
  IL_0001:  newobj     instance void UnderTheHoodVB.Examples.PropertyExample.PropertyClass::.ctor()
  IL_0006:  stloc.1
  IL_0007:  ldloc.1
  IL_0008:  ldstr      "Hello"
  IL_000d:  callvirt   instance void UnderTheHoodVB.Examples.PropertyExample.PropertyClass::set_Text(string)
  IL_0012:  nop
  IL_0013:  ldloc.1
  IL_0014:  ldc.i4.s   42
  IL_0016:  callvirt   instance void UnderTheHoodVB.Examples.PropertyExample.PropertyClass::set_Number(int32)
  IL_001b:  nop
  IL_001c:  ldloc.1
  IL_001d:  callvirt   instance string UnderTheHoodVB.Examples.PropertyExample.PropertyClass::get_Text()
  IL_0022:  stloc.2
  IL_0023:  ldloc.1
  IL_0024:  callvirt   instance int32 UnderTheHoodVB.Examples.PropertyExample.PropertyClass::get_Number()
  IL_0029:  stloc.0
  IL_002a:  nop
  IL_002b:  ret
} // end of method PropertyUser::UseTheProperties 

在“Hello World”示例之后,你应该能够很好地阅读这段代码。我们看到了一些新的操作码,例如 newobj,这很自明。ldc.i4.s 42 可能需要解释。 Ldc.i4 将一个指定的 Int32 推送到堆栈。 .s 表示它将指定值视为 Int16 而不是 Int32,这可能是正确的,因为 42 既可以装入 Int16 也可以装入 Int32。那么 callvirt 操作码呢?它用于以多态方式调用可重写的方法。也就是说,即使对象的设计时类型是其基类,callvirt 也会调用超类上的函数,而不是基类上的函数(如果提供了超类)。听起来很难?别担心。在这种情况下,只需假定 callvirtcall 的作用相同。那么我们在上面的 IL 中看到了什么?没有任何东西被称为 Property,它们都是 get 和 set 方法!那么我们为什么还要使用 Properties 呢?首先,它们在编码时提供了直观的 API。而不是寻找正确的函数来获取或设置某个值,我们只需使用一个 Property 来获取或设置相同的值。为什么不使用 Public field 呢?嗯,我希望那显而易见。Properties 通过 get 和 set 方法提供封装,并允许你在获取或设置 Property 的值时编写额外的代码。

这结束了我们的属性示例。它是否符合你的预期?让我们来看看另一个 VB 和 C# 结构,看看 IL 会怎么处理它。 

深入阅读
《Expert C# 5.0》一书的第 5 章  

4.2. With 语句的案例

你是否曾想过在使用 VB 中的 With 关键字时会发生什么?它允许你在初始化一个 Object 后设置一些 Properties,而无需引用该 Object。 C# 也知道这个相同的结构,但没有像 VB 那样提供关键字。让我们看看代码。你可以在 WithExample 文件夹下找到它。

Public Sub ExampleWithoutWith()
   Dim p As New Person
   p.FirstName = "Fu"
   p.LastName = "Bar"
   p.Age = 50
End Sub
 
Public Sub ExampleUsingWith()
   Dim p As New Person With {.FirstName = "Fu", .LastName = "Bar", .Age = 50}
End Sub
public void ExampleWithoutWith()
{
   Person p = new Person();
   p.FirstName = "Fu";
   p.LastName = "Bar";
   p.Age = 50;
}
 
public void ExampleUsingWith()
{
   Person p = new Person
   {
      FirstName = "Fu",
      LastName = "Bar",
      Age = 50
   };
} 

我想现在你可以猜出 ExampleWithoutWith 的 IL 看起来是什么样子了,所以我不打算讨论它。 但是当我们使用那个 With 关键字时会发生什么?这是 IL。

  .locals init ([0] class UnderTheHoodVB.Examples.Person p,
           [1] class UnderTheHoodVB.Examples.Person VB$t_ref$S0)
  IL_0000:  nop
  IL_0001:  newobj     instance void UnderTheHoodVB.Examples.Person::.ctor()
  IL_0006:  stloc.1
  IL_0007:  ldloc.1
  IL_0008:  ldstr      "Fu"
  IL_000d:  callvirt   instance void UnderTheHoodVB.Examples.Person::set_FirstName(string)
  IL_0012:  nop
  IL_0013:  ldloc.1
  IL_0014:  ldstr      "Bar"
  IL_0019:  callvirt   instance void UnderTheHoodVB.Examples.Person::set_LastName(string)
  IL_001e:  nop
  IL_001f:  ldloc.1
  IL_0020:  ldc.i4.s   50
  IL_0022:  callvirt   instance void UnderTheHoodVB.Examples.Person::set_Age(int32)
  IL_0027:  nop
  IL_0028:  ldloc.1
  IL_0029:  stloc.0
  IL_002a:  nop
  IL_002b:  ret
} // end of method WithExample::ExampleUsingWith

你应该看到的第一个事情是额外初始化的局部变量。它有一个奇怪的名字(通过这个名字你可以看出我正在使用 VB 生成的 IL),这个名字在常规的 VB 或 C# 代码中是无效的。然后我们看到创建一个新的 Person,但它没有被赋值给 Person p,而是赋值给了那个额外的、奇怪的变量。从那时起,一切都相当正常,所有的 Properties 都设置在那个额外的变量上。之后,在 IL_0028,那个奇怪的变量被赋值给我们的变量 p。如果你查看 C# 生成的 IL,你会看到完全相同的内容,只是那个额外的变量名称不同。看,编译器又在玩弄我们了!

在我们开始处理更难的内容之前,让我们来看另一个相当简单的示例。

4.3. For Each 循环的案例 

我最喜欢但又简单的编译器技巧之一就是它如何处理 For Each... Next 语句(C# 中的 foreach)。你可以在 ForEachExamples 文件夹中打开示例。它包含一个单独的 ,里面有四个方法。 两个方法用于迭代 IEnumerable,两个方法用于迭代 IEnumerable(Of T)(C# 中的 IEnumerable<T>)。第一个方法直接使用 For Each 关键字,第二个方法使用编译器发出的代码(如 IL 中所示)。所以,让我们看看使用 For EachIEnumerable 的代码。

Public Shared Sub ForEach(ByVal l As IEnumerable, ByVal handler As Action(Of Object))
   For Each obj As Object In l
      handler.Invoke(obj)
   Next
End Sub
public static void ForEach(IEnumerable l, Action<object> handler)
{
   foreach (object obj in l)
   {
      handler.Invoke(obj);
   }
}

这些方法看起来相当直接,这很可能是你以前做过无数次的事情。生成的 IL 代码对于 C# 和 VB 来说略有不同。 所以,让我们看看 VB 的 IL 代码。你有空时可以自行看看 C# 的 IL。

.method public static void  ForEach(class [mscorlib]System.Collections.IEnumerable l,
                                    class [mscorlib]System.Action`1<object> 'handler') cil managed
{
  // Code size       82 (0x52)
  .maxstack  2
  .locals init ([0] object obj,
           [1] class [mscorlib]System.Collections.IEnumerator VB$t_ref$L0,
           [2] bool VB$CG$t_bool$S0)
  IL_0000:  nop
  IL_0001:  nop
  .try
  {
    IL_0002:  ldarg.0
    IL_0003:  callvirt   instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.IEnumerable::GetEnumerator()
    IL_0008:  stloc.1
    IL_0009:  br.s       IL_0025
    IL_000b:  ldloc.1
    IL_000c:  callvirt   instance object [mscorlib]System.Collections.IEnumerator::get_Current()
    IL_0011:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
    IL_0016:  stloc.0
    IL_0017:  ldarg.1
    IL_0018:  ldloc.0
    IL_0019:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
    IL_001e:  callvirt   instance void class [mscorlib]System.Action`1<object>::Invoke(!0)
    IL_0023:  nop
    IL_0024:  nop
    IL_0025:  ldloc.1
    IL_0026:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_002b:  stloc.2
    IL_002c:  ldloc.2
    IL_002d:  brtrue.s   IL_000b
    IL_002f:  nop
    IL_0030:  leave.s    IL_0050
  }  // end .try
  finally
  {
    IL_0032:  ldloc.1
    IL_0033:  isinst     [mscorlib]System.IDisposable
    IL_0038:  ldnull
    IL_0039:  ceq
    IL_003b:  ldc.i4.0
    IL_003c:  ceq
    IL_003e:  stloc.2
    IL_003f:  ldloc.2
    IL_0040:  brfalse.s  IL_004e
    IL_0042:  ldloc.1
    IL_0043:  isinst     [mscorlib]System.IDisposable
    IL_0048:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_004d:  nop
    IL_004e:  nop
    IL_004f:  endfinally
  }  // end handler
  IL_0050:  nop
  IL_0051:  ret
} // end of method ForEachExamples::ForEach 

哇!这么一小段代码却有这么多 IL!我在这里粘贴了全部 IL 代码,因为有两个输入参数。你看到了不少于两个由编译器创建的额外局部变量。一个是 IEnumerator,另一个是 Boolean (IL 和 C# 中是 bool)。 此外,我们还看到了一个 Try Finally 块,我实际上并没有在代码中添加它。正如你所看到的,第一件要做的事情是对 IEnumerable 调用 GetEnumerator 方法(它由 ldarg.0(或“加载参数 0”,其中参数是传递给方法的参数)推送到堆栈上)。 然后我们看到一个奇怪的操作码, br.s。 每当你看到一个以 br 开头的操作码时,它通常表示 BRanch(分支)。后面跟着一个地址,如 IL_0025。Br_s 意味着代码将在指定地址继续执行(如果你愿意,可以看作是跳转或 GoTo)。所以,如果我们遵循这个路径,我们会看到对 Enumerator 调用 MoveNext。 结果,一个 Boolean(C# 中是 bool)被存储在局部变量 2 中。 现在我们应该能猜到 brtrue.s 的意思了。如果为 TRUE,则 BRanch(分支)到指定地址。我们找到地址 IL_000b 并回到我们所在的位置。调用 IEnumerator.Current 属性(一个Property!)的 getter。 我们将忽略下一行,它将 Object 装箱。VB 生成的 IL 在这一点上与 C# 生成的 IL 不同。C# 从不调用 GetObjectValue。接下来,我们将调用我们传递给方法的委托Invoke(来自调用 get_CurrentObject 位于堆栈上并被传递给 Invoke 方法。再次调用 MoveNext,循环重新开始。 如果 MoveNext 返回 false,我们就进入 finally 块。 在这里,IEnumerator 被检查是否为 IDisposable 类型。 isinst 操作码将一个 Object 转换为指定的类型。如果 IEnumerator 实现 IDisposable,则调用 Dispose(以清理资源),方法执行结束。

既然我们已经逐行分析了 IL,我们应该就能将其翻译回 VB 或 C# 了!这正是我所做的。请看下面的代码,并注意 VB 和 C# 之间的细微差别。

Public Shared Sub ForEachRewritten(ByVal l As IEnumerable, ByVal handler As Action(Of Object))
   Dim e As IEnumerator
   Try
      Dim obj As Object
      e = l.GetEnumerator
      Do While e.MoveNext
         obj = e.Current
         handler.Invoke(obj)
      Loop
   Finally
      If TryCast(e, IDisposable) IsNot Nothing Then
         DirectCast(e, IDisposable).Dispose()
      End If
   End Try
End Sub
public static void ForEachRewritten(IEnumerable l, Action<object> handler)
{
   IEnumerator e = l.GetEnumerator();
   try
   {
      object obj;
      while (e.MoveNext())
      {
         obj = e.Current;
         handler.Invoke(obj);
      }
   }
   finally
   {
      IDisposable disposable = e as IDisposable;
      if (disposable != null)
      {
         disposable.Dispose();
      }
   }
}

现在比较 ForEach 函数和 ForEachRewritten 函数生成的 IL,它们完全相同!这真是太棒了,不是吗?有很多事情在你不知道的情况下发生了!谁能想到呢?

我对 GenericForEachGenericForEachRewritten(它们使用 IEnumerable(Of T))(C# 中的 IEnumerable<T>)也做了同样的事情。你可以自行探索它们各自的 IL。通过运行应用程序并点击“For each”按钮,你可以检查不同的函数是否真的产生相同的输出。你会看到一个带有四个按钮的 Form,每个按钮执行一个 ForEach 函数并将它们的输出打印到 TextBoxes 中。

深入阅读
《Expert C# 5.0》一书的第 9 章 

4.4. Lambda 表达式的案例  

4.4.1. 简单级别示例
现在你应该已经掌握窍门了。让我们来看看 IL 如何生成与你所输入的代码截然不同的另一个示例。Lambda 表达式(一种匿名函数+)是编译器功能的一个绝佳示例!在解决方案中打开 LambdaExamples 文件夹,找到 EasyLambdaButtonFactory。我们要做的就是创建一组 Button,并将一个 lambda 表达式分配给 Button.Click 事件。看看下面的代码。

Public Function GenerateButtons() As System.Collections.Generic.IEnumerable(Of System.Windows.Forms.Button) Implements IButtonFactory.GenerateButtons
   Dim list As New List(Of Button)
      For i As Integer = 1 To 10
         Dim btn As New Button
         btn.Text = "1"
         AddHandler btn.Click,
             Sub(sender, e)
                Dim senderBtn As Button = DirectCast(sender, Button)
                senderBtn.Text = (Convert.ToInt32(senderBtn.Text) + 1).ToString
             End Sub
         list.Add(btn)
      Next
   Return list
End Function
IEnumerable<System.Windows.Forms.Button> IButtonFactory.GenerateButtons()
{
   List<Button> list = new List<Button>();
   for (int i = 1; i <= 10; i++)
   {
      Button btn = new Button();
      btn.Text = "1";
      btn.Click += (sender, e) =>
      {
         Button senderBtn = sender as Button;
         senderBtn.Text = (Convert.ToInt32(senderBtn.Text) + 1).ToString();
      };
      list.Add(btn);
   }
   return list;
}

那么我们在这里看到了什么?在一个循环中,我们创建了十个 Buttons。我们将 "1" 这个 string 值赋给每个 ButtonText Property。每当 Button 被点击时,我们就将 sender 转换为 Button,将 ButtonText Property 转换为 Integer,给它加 1,然后将新值赋给 ButtonText Property。结果应该是,你每次点击一个 Button,它的 Text 都会增加一。你可以通过启动应用程序在 Easy lambda form 上亲眼看到这一点。
让我们再看看 IL。实际上,即使只看 ILDASM 中的,我们也能看到一些奇怪的事情发生了。它多了一个我们没有实现的额外方法!

那么这个额外的共享(C# 中的静态)函数是从哪里来的?那就是我们的 lambda 表达式!看看那个东西的 IL。

.method private specialname static void  _Lambda$__4(object sender,
                                                     class [mscorlib]System.EventArgs e) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       38 (0x26)
  .maxstack  3
  .locals init ([0] class [System.Windows.Forms]System.Windows.Forms.Button senderBtn,
           [1] int32 VB$t_i4$S0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  castclass  [System.Windows.Forms]System.Windows.Forms.Button
  IL_0007:  stloc.0
  IL_0008:  ldloc.0
  IL_0009:  ldloc.0
  IL_000a:  callvirt   instance string [System.Windows.Forms]System.Windows.Forms.ButtonBase::get_Text()
  IL_000f:  call       int32 [mscorlib]System.Convert::ToInt32(string)
  IL_0014:  ldc.i4.1
  IL_0015:  add.ovf
  IL_0016:  stloc.1
  IL_0017:  ldloca.s   VB$t_i4$S0
  IL_0019:  call       instance string [mscorlib]System.Int32::ToString()
  IL_001e:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.ButtonBase::set_Text(string)
  IL_0023:  nop
  IL_0024:  nop
  IL_0025:  ret
} // end of method EasyLambdaButtonFactory::_Lambda$__4

它获取参数 Object senderEventArgs e,将 sender 转换为 Button,获取 Text 并将其转换为 Integer,然后(使用 add_ovf 操作码)加 1,并将其赋值给 ButtonText Property。 这绝非巧合!
那么,我们在我们实现的方法中是如何调用这个东西的呢?
好吧,看这里。

IL_001d:  ldftn      void UnderTheHoodVB.Examples.LambdaExamples.EasyLambdaButtonFactory::_Lambda$__4(object,
                                                                                                        class [mscorlib]System.EventArgs)
IL_0023:  newobj     instance void [mscorlib]System.EventHandler::.ctor(object,
                                                                          native int)
IL_0028:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::add_Click(class [mscorlib]System.EventHandler)

指向生成函数的指针被推送到堆栈上(使用 ldftn 操作码)。创建一个新的 EventHandler 委托实例,并将指针传递给构造函数。 然后,EventHandler 被添加到 ButtonClick Event 的侦听器列表中。我们也可以自己编写 Shared(C# 中的 static)函数并使用 AddressOf(C# 中的 += 运算符)。
那么,我们为什么不这样做呢?嗯,首先,在我们的 中有许多只在某个地方使用的 Shared functions,这并不容易阅读,其次,因为我们可以使用 lambda 表达式做一些非常巧妙的事情,正如我们将在下一个示例中看到的。

顺便说一下,你可能已经注意到 C# 编译器还创建了一个名为 CachedAnonymousDelegate 的字段。想知道这是怎么回事吗? 这是一个小的性能优化。为了防止创建多个委托,C# 编译器只创建一个,将其存储起来,并在以后重用,而不是每次都创建一个新的委托。

4.4.2. 中等级别示例
所以,是时候打开 MediumLambdaButtonFactory 了。这个 lambda 比第一个只是稍微不同。让我们看看。

AddHandler btn.Click,
   Sub(sender, e)
      btn.Text = (Convert.ToInt32(btn.Text) + 1).ToString
   End Sub
btn.Click += (sender, e) =>
{
   btn.Text = (Convert.ToInt32(btn.Text) + 1).ToString();
};

诀窍是什么?我们在处理程序中使用了 btn 变量,即使它超出了 lambda 表达式的范围(如果我们创建了一个 Shared function,我们就无法访问 btn 变量)! 所以,让我们再次检查 ILDASM。
 
我的天!编译器创建了一个名为 _Closure$__5 的全新类型!那是什么?它将函数范围之外的 Button 作为一个字段来保存,并有一个名为 _Lambda$__9 的函数。我认为没有必要查看那个 _Lambda$__9 函数的 IL。它只是做了上一个 lambda 示例所做的,只是这次它没有将 sender 转换为 Button,而是使用了 btn 字段。有趣的是查看 GenerateButtons 的 IL。不幸的是,我无法在此处发布它,因为发出的操作码行对于普通显示器来说太宽了。然而,我们在 IL 中看到的是创建了一个 _Closure$__5 的新实例,并将 Button 赋值给了 _Closure$__5.$VB$Local_btn。为了将函数赋值给 Button.Click Event,发出的代码与上面示例中的相同,只是 delegate 现在指向 _Closure$__5._Lambda$__9
那么,编译器为什么要为这个函数创建一个内部类型呢?正如我所说,btn 变量超出了函数的范围。在这种情况下,btn 变量实际上将在下一个 For Loop 开始时超出范围,但是由我们的 lambda 表达式创建的函数将一直存在,只要 btn 变量指向的 Button 存在,或者直到 Click Handler 被移除。因此,编译器必须找到一种方法来使 btn 变量在 delegate 存在期间保持有效。它通过将 btn 变量包装在一个新的 Type 中,并通过 Button.Click Event 保持对该 Type 实例的引用来实现。所以,虽然这个示例与上一个示例做了完全相同的事情(你可以在 medium lambda form 中查看),但生成的 IL 却大不相同!

4.4.3. 困难级别示例
你可能已经猜到了,但是如果在同一个 lambda 表达式中使用了来自多个作用域的变量,会发生什么?编译器实际上为每个作用域级别创建了一个内部类型!所以,让我们看看下一个示例,其中我们将引入一个在 For Loop 之外的计数器,该计数器被所有 Button Click Events 共享。

Public Function GenerateButtons() As System.Collections.Generic.IEnumerable(Of System.Windows.Forms.Button) Implements IButtonFactory.GenerateButtons
   Dim list As New List(Of Button)
   Dim counter As Integer = 1
   For i As Integer = 1 To 10
      Dim btn As New Button
      btn.Text = counter.ToString
      AddHandler btn.Click,
         Sub(sender, e)
            counter += 1
            btn.Text = counter.ToString
         End Sub
      list.Add(btn)
   Next
   Return list
End Function
IEnumerable<System.Windows.Forms.Button> IButtonFactory.GenerateButtons()
{
   List<Button> list = new List<Button>();
   int counter = 1;
   for (int i = 1; i <= 10; i++)
   {
      Button btn = new Button();
      btn.Text = counter.ToString();
      btn.Click += (sender, e) =>
      {
         counter++;
         btn.Text = counter.ToString();
      };
      list.Add(btn);
   }
   return list;
}

所以我们看到这里,btn 变量仍然在 For Loop 的作用域内,但是 counterFor Loop 的作用域之外,因此被所有 Buttons 共享。这意味着,如果你点击一个按钮,它的 Text 会变成“2”,然后如果你再点击另一个按钮,它的 Text 会变成“3”(因为第一个按钮已经增加了 counter)。 你可以在 hard lambda form 中看到这个效果。
再次,我们将查看 ILDASM,看看编译器为我们创建了什么。
 
为你创建了一个类型中的类型中的类型……最里面的类型(_Closure$__2)保存了对其外部类型(_Closure$__1)的引用。为什么呢?嗯,lambda 需要一个对 btn 变量的引用,该变量对每个 Event Handler 都是唯一的,以及对 counter 变量的引用,该变量在所有 Event Handlers 之间共享。所以,每个 ButtonsClick EventHandler 都将持有一个对 _Closure$__2 的唯一实例的引用,而这些实例都将持有一个对 _Closure$__1 的相同实例的引用,_Closure$__1 包含了 counter 变量。如果你用 C# 项目查看 ILDASM,你会看到没有内部-内部类型,只有两个内部类型。除了这个小区别之外,其他所有内容对于 C# 来说仍然是真的。再次,我不会展示任何 IL 代码,因为它不适合页面。你可以自己去看看。这不少,但不要气馁!只需逐行阅读,你就会明白。我们将在稍后看 VB 和 C# 的对应版本,如果现在还不清楚,在下一个示例中就会清楚了。

4.4.4. 极难级别示例
别让这个标题吓到你。使这个示例比前一个示例稍微困难一些的唯一原因是在 lambda 中增加了一个新的作用域级别。我已经在 InsaneLambdaButtonFactory 中创建了一个额外的计数器,名为 _outerCounter,作为其中的一个字段。

Private _outerCounter As Integer = 1
 
Public Function GenerateButtons() As System.Collections.Generic.IEnumerable(Of System.Windows.Forms.Button) Implements IButtonFactory.GenerateButtons
   Dim list As New List(Of Button)
   Dim counter As Integer = 1
   For i As Integer = 1 To 10
      Dim btn As New Button
      btn.Text = _outerCounter.ToString + " - " + counter.ToString
      AddHandler btn.Click,
         Sub(sender, e)
            counter += 1
            If counter Mod 10 = 0 Then
               _outerCounter += 1
            End If
            btn.Text = _outerCounter.ToString + " - " + counter.ToString
         End Sub
      list.Add(btn)
   Next
   Return list
End Function
private int _outerCounter = 1;
 
IEnumerable<System.Windows.Forms.Button> IButtonFactory.GenerateButtons()
{
   List<Button> list = new List<Button>();
   int counter = 1;
   for (int i = 1; i <= 10; i++)
   {
      Button btn = new Button();
      btn.Text = _outerCounter.ToString() + " - " + counter.ToString();
      btn.Click += (sender, e) =>
      {
         counter++;
         if (counter % 10 == 0)
         {
            _outerCounter++;
         }
         btn.Text = _outerCounter.ToString() + " - " + counter.ToString();
      };
      list.Add(btn);
   }
   return list;
}

所以,我们看到 _outerCounter 变量在 lambda 中被使用,并且被所有 Buttons 共享(就像 counter 变量一样)。但有一个区别。_outerCounter 可能会被非按钮点击事件的其他事物改变。那么,另一个作用域对应另一个内部类型吗?不!在这种情况下,第一个内部类型持有对其创建者实例的引用。
 
很聪明,不是吗?_Closure$__4 中的函数通过对 _Closure$__3 的引用,可以访问创建它的 ButtonFactory 的实例。这样,ButtonFactory 和 lambda 函数都查看同一个 _outerCounter。当 counter(仅由 Buttons 共享)增加十次时,_outerCounter 就会增加一。如果你想了解它是如何工作的,请打开 insane lambda form,然后点击你想要的任何按钮二十次。

所以,这很不错,但如果能在 VB 或 C# 中看到其中一些内容,不是更清楚吗?嗯,你今天运气真好!我研究了 insane lambda 示例的 IL,并创建了一个 ,它生成完全相同的 IL(除了名称更改)。去看看 InsaneLambdaButtonFactoryRewritten,并将生成的 IL 与 InsaneLambdaButtonFactory 的 IL 进行比较。还要比较 VB 版本和 C# 版本,以发现一些细微的差别。

Public Class InsaneLambdaButtonFactoryRewritten
   Implements IButtonFactory
 
   Private outerCounter As Integer = 1
 
   Public Function GenerateButtons() As _
          System.Collections.Generic.IEnumerable(Of System.Windows.Forms.Button) _
          Implements IButtonFactory.GenerateButtons
      Dim iLambda As New InnerLambda
      iLambda.Field_Me = Me
      Dim list As New List(Of Button)
      Dim iILambda As InnerLambda.InnerInnerLambda
      iLambda.Local_counter = 1
      For i As Integer = 1 To 10
         iILambda = New InnerLambda.InnerInnerLambda(iILambda)
         iILambda.NonLocal_Inner_InnerLambda = iLambda
         iILambda.Local_btn = New Button
         iILambda.Local_btn.Text = outerCounter.ToString + " - " + iLambda.Local_counter.ToString
         AddHandler iILambda.Local_btn.Click, AddressOf iILambda.EventHandler
         list.Add(iILambda.Local_btn)
      Next
      Return list
   End Function
 
   Public Class InnerLambda
 
      Public Local_counter As Integer
      Public Field_Me As InsaneLambdaButtonFactoryRewritten
 
      Public Sub New()
      End Sub
 
      Public Sub New(ByVal innerLambda As InnerLambda)
         If Not innerLambda Is Nothing Then
            Field_Me = innerLambda.Field_Me
            Local_counter = innerLambda.Local_counter
         End If
      End Sub
 
      Public Class InnerInnerLambda
 
         Public Local_btn As Windows.Forms.Button
         Public NonLocal_Inner_InnerLambda As InnerLambda
 
         Public Sub New()
         End Sub
 
         Public Sub New(ByVal innerInnerLambda As InnerInnerLambda)
            If Not innerInnerLambda Is Nothing Then
               Local_btn = innerInnerLambda.Local_btn
            End If
         End Sub
 
         Public Sub EventHandler(ByVal sender As Object, ByVal e As EventArgs)
            NonLocal_Inner_InnerLambda.Local_counter = NonLocal_Inner_InnerLambda.Local_counter + 1
            If NonLocal_Inner_InnerLambda.Local_counter Mod 10 = 0 Then
               NonLocal_Inner_InnerLambda.Field_Me.outerCounter = _
                        NonLocal_Inner_InnerLambda.Field_Me.outerCounter + 1
            End If
            Local_btn.Text = NonLocal_Inner_InnerLambda.Field_Me.outerCounter.ToString + _
                             " - " + NonLocal_Inner_InnerLambda.Local_counter.ToString
         End Sub
 
      End Class
 
   End Class
 
End Class
public class InsaneLambdaFactoryRewritten : IButtonFactory
{
   private int _outerCounter = 1;
   IEnumerable<System.Windows.Forms.Button> IButtonFactory.GenerateButtons()
   {
      InnerClass1 lambda1 = new InnerClass1();
      lambda1.field_this = this;
      List<Button> list = new List<Button>();
      lambda1.counter = 1;
      for (int i = 1; i <= 10; i++)
      {
         InnerClass2 lambda2 = new InnerClass2();
         lambda2.locals = lambda1;
         lambda2.btn = new Button();
         lambda2.btn.Text = _outerCounter.ToString() + " - " + lambda1.counter.ToString();
         lambda2.btn.Click += lambda2.EventHandler;
         list.Add(lambda2.btn);
      }
      return list;
   }
 
   class InnerLambda1
   {
      public int counter;
      public InsaneLambdaFactoryRewritten field_this;
   }
 
   class InnerLambda2
   {
      public InnerClass1 locals;
      public Button btn;
 
      public void EventHandler(Object sender, EventArgs e)
      {
         locals.counter++;
         if (locals.counter % 10 == 0)
         {
            locals.field_this._outerCounter++;
         }
         btn.Text = locals.field_this._outerCounter.ToString() + " - " + locals.counter.ToString();
      }
   }
}

正如你所看到的,没有 lambda 表达式的痕迹。你应该能够调试这段代码并了解它的作用。lambda 部分已移至 InnerInnerLambda(C# 中的 InnerLambda2)中的函数。它还包含 btn 变量和对 InnerLambda(C# 中的 InnerLambda1)的引用。InnerLambda 负责 counter 变量,并为 _outerCounter 持有对 ButtonFactory 的引用。所有变量都在创建按钮的原始函数中设置。事实上,你可以看到所有变量都已从该函数中移除,并被 InnerLambdaInnerInnerLambda 调用所取代。

你可以通过运行应用程序并打开 insane lambda rewritten Form 来检查 InsaneLambdaButtonFactoryRewritten 是否真的与 InsaneLambdaButtonFactory 执行相同的操作。

你也可以自己尝试一下,尝试使用嵌套的 For Each 循环If Then Else 语句来进一步嵌套。现在你知道它是如何工作的了!

深入阅读
Lambda 表达式 (Visual Basic) - MSDN
匿名函数 (C# 编程指南) - MSDN

《Expert C# 5.0》一书的第 4 章(也解释了“扩展方法”!) 

一些关于该主题的博客
匿名方法,第 1 部分
匿名方法作为事件处理程序 - 第 1 部分 
C# 中匿名方法的实现及其后果(第一部分) 

我现在鼓励你看看 Bart de Smet 的讲座。他也讨论了 lambda 表达式,并进一步解释了它们如何可能导致内存泄漏,如果你不小心的话。这实际上是他讲座的第一个主题,所以你可以直接开始播放视频,坐下来放松。
Bart de Smet - 10 个 C# 语言特性的幕后揭秘

4.5. 匿名类型的案例

让我们继续来看我为你们准备的下一个 VB 和 C# 结构,匿名类型。你可以在解决方案的 AnonymousTypeExamples 文件夹中找到示例。我们将要做的是创建一个 Person 对象集合,然后选择一个 Properties 的子集,这将创建一个所谓的匿名类型。所以,让我们来看看第一个示例。查看 FormalNamePeopleFactoryNickNamePeopleFactory。先看哪个都无所谓,所以我先看 FormalNamePeopleFactory。这是它的代码。

Public Function GeneratePeople() As System.Collections.IList Implements IPeopleFactory.GeneratePeople
   Return PeopleHelper.GetPeople.Select(Function(p) New With {.FullName = p.LastName + ", " + p.FirstName, .Age = p.Age}).ToList
End Function
System.Collections.IList IPeopleFactory.GeneratePeople()
{
   return PeopleHelper.GetPeople().Select(p => new { FullName = p.LastName + ", " + p.FirstName, Age = p.Age }).ToList();
}

代码量不多,但有很多你不知道的事情(但很快就会知道了)。首先,让我们看看这段代码实际上做了什么。PeopleHelper.GetPeople 只是创建了一个 Person 对象集合。然后我们调用 IEnumerable(Of T).Select 方法,这是一个在 IEnumerable(Of T)(C# 中的 IEnumerable<T>)上的扩展方法。你可以看到我们正在创建一个新的 Object,因为 VB 和 C# 示例都有 New 关键字。然而,我们没有定义一个 Type,例如 New Person,而是再次使用了那个 With 关键字(参见本文前面的 With 示例)。然后我们定义了一组不存在的 Properties 并给它们赋值。

让我们看看为这个函数生成的 IL。

.method private specialname static class VB$AnonymousType_0`2<string,int32> 
        _Lambda$__2(class UnderTheHoodVB.Examples.Person p) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       38 (0x26)
  .maxstack  3
  .locals init ([0] class VB$AnonymousType_0`2<string,int32> _Lambda$__2,
           [1] class VB$AnonymousType_0`2<string,int32> VB$t_ref$S0)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance string UnderTheHoodVB.Examples.Person::get_LastName()
  IL_0006:  ldstr      ", "
  IL_000b:  ldarg.0
  IL_000c:  callvirt   instance string UnderTheHoodVB.Examples.Person::get_FirstName()
  IL_0011:  call       string [mscorlib]System.String::Concat(string,
                                                              string,
                                                              string)
  IL_0016:  ldarg.0
  IL_0017:  callvirt   instance int32 UnderTheHoodVB.Examples.Person::get_Age()
  IL_001c:  newobj     instance void class VB$AnonymousType_0`2<string,int32>::.ctor(!0,
                                                                                     !1)
  IL_0021:  stloc.0
  IL_0022:  br.s       IL_0024
  IL_0024:  ldloc.0
  IL_0025:  ret
} // end of method FormalNamePeopleFactory::_Lambda$__2

当然,我们使用了一个 lambda 表达式,所以我们应该查看在生成的 _Lambda$__2 函数中的 IL。 正如你所看到的,这是一个返回 VB$AnonymousType_0`2<string, int32> 的函数(它在最上面的那一行)。我们看到完整的名称被推到堆栈上,p.LastName", "p.FirstName 被连接起来。然后,连接后的 FullNamep.Age 被推到堆栈上,并创建一个 AnonymousType(Of T1, T2)(C# 中的 AnonymousType<T1, T2>)的新实例,其中 T1 是一个 stringFullName Property),T2 是一个 int32Age Property)。那么,这个 AnonymousType(Of T1, T2) 是从哪里来的?为什么它是泛型的?
当你检查 ILDASM 时,你实际上可以在 Global Namespace 中看到 AnonymousType
 

所以编译器实际上为你创建了一个新类型(使得匿名类型在底层不那么匿名了)。这解释了 AnonymousType 的来源,但没有解释为什么它是 Generic 的,为什么它位于 Global Namespace 而不是紧挨着使用它的函数(甚至可能作为一个内部类型)。
第二部分可以通过查看另一个示例 NickNamePeopleFactory 来解释。所以,让我们看看代码。

Public Function GeneratePeople() As System.Collections.IList Implements IPeopleFactory.GeneratePeople
   Return PeopleHelper.GetPeople.Select(Function(p) New With {.FullName = p.FirstName + " " + p.LastName, .Age = p.Age}).ToList
End Function
System.Collections.IList IPeopleFactory.GeneratePeople()
{
   return PeopleHelper.GetPeople().Select(p => new { FullName = p.FirstName + " " + p.LastName, Age = p.Age }).ToList();
}

如你所见,这个函数做了几乎完全相同的事情,只是 FullName Property 的格式稍有不同。由于这是另一个 中的另一个函数,你可能会期望编译器创建一个新的匿名类型(毕竟,它对 lambda 也是这么做的)。然而,事实并非如此,当我们查看这个函数的 IL 代码时,我们可以看到以下内容。

  .locals init ([0] class VB$AnonymousType_0`2<string,int32> _Lambda$__3,
           [1] class VB$AnonymousType_0`2<string,int32> VB$t_ref$S0)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance string UnderTheHoodVB.Examples.Person::get_FirstName()
  IL_0006:  ldstr      " "
  IL_000b:  ldarg.0
  IL_000c:  callvirt   instance string UnderTheHoodVB.Examples.Person::get_LastName()
  IL_0011:  call       string [mscorlib]System.String::Concat(string,
                                                              string,
                                                              string)
  IL_0016:  ldarg.0
  IL_0017:  callvirt   instance int32 UnderTheHoodVB.Examples.Person::get_Age()
  IL_001c:  newobj     instance void class VB$AnonymousType_0`2<string,int32>::.ctor(!0,
                                                                                     !1)
  IL_0021:  stloc.0
  IL_0022:  br.s       IL_0024
  IL_0024:  ldloc.0
  IL_0025:  ret
} // end of method NickNamePeopleFactory::_Lambda$__3

它看起来很像前一个方法生成的 IL,尽管我们可以看到 FullName Property 的格式有所不同。但这真的是我们看到的唯一区别!同一个匿名类型被用于这个函数!那么,如果这个匿名类型在 Private Inner Class 中使用,并且匿名类型本身是 Private Class 的另一个子类型呢?那么 NickNamePeopleFactory 显然就无法访问它了,并且必须生成一个新的 AnonymousType。显然,编译器生成一个新的 AnonymousType 比重用一个已经存在的类型花费的时间更长。

那么,为什么它是 Generic 的呢?因为 AnonymousType 位于 Global Namespace,它无法访问任何 Private Types,但因为 AnonymousTypeGeneric 的,它实际上从不引用任何 Private Types,因此可以与你可能想到的任何类型重用。让我们看看 BuggedPeopleFactory

Public Function GeneratePeople() As System.Collections.IList Implements IPeopleFactory.GeneratePeople
   Return PeopleHelper.GetPeople.Select(Function(p) New With {.FullName = p.Age, .Age = p.FirstName}).ToList
End Function
System.Collections.IList IPeopleFactory.GeneratePeople()
{
   return PeopleHelper.GetPeople().Select(p => new { FullName = p.Age, Age = p.FirstName }).ToList();
}

如你所见,某个低级程序员(在本例中是我)交换了 AgeFullName,所以 Age 现在显示 FullName,而 FullName 显示 Age!这意味着 FullName 不再是 stringAge 也不再是 int32。然而,当我们查看生成的 IL 时,我们可以看到使用了相同的 AnonymousType

.method private specialname static class VB$AnonymousType_0`2<int32,string> 
        _Lambda$__1(class UnderTheHoodVB.Examples.Person p) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       22 (0x16)
  .maxstack  2
  .locals init ([0] class VB$AnonymousType_0`2<int32,string> _Lambda$__1,
           [1] class VB$AnonymousType_0`2<int32,string> VB$t_ref$S0)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance int32 UnderTheHoodVB.Examples.Person::get_Age()
  IL_0006:  ldarg.0
  IL_0007:  callvirt   instance string UnderTheHoodVB.Examples.Person::get_FirstName()
  IL_000c:  newobj     instance void class VB$AnonymousType_0`2<int32,string>::.ctor(!0,
                                                                                     !1)
  IL_0011:  stloc.0
  IL_0012:  br.s       IL_0014
  IL_0014:  ldloc.0
  IL_0015:  ret
} // end of method BuggedPeopleFactory::_Lambda$__1

怎么样?它只是返回相同的 AnonymousType,但具有不同的 Generic 参数。现在,如果我们使用了另一个使用相同 AnonymousType 的方法无法访问的 Private Type 呢?它仍然可以使用相同的 AnonymousType,只是具有不同的 Generic 参数!这确实是一件精妙的作品!

但为什么以及何时会重用匿名类型?当匿名类型上的属性的数量、名称和顺序相同时,它们就会被重用。例如,尝试在其中一个函数中交换 FullNameAge 的位置,你就会在 ILDASM 中看到第二个匿名类型被创建。你也可以在一个函数中将 FulName 拼写为单个 L,同样会看到一个新的匿名类型被生成。它们被重用的原因是,你可以有两个匿名类型列表,它们代表相同的 Object,并且你仍然可以比较它们(如果它们是完全不同的 Types,则比较总是返回 False)。

顺便说一下,你有没有注意到示例中的所有函数都返回一个 IList?这是为了能够绑定到函数返回的匿名类型。你可以在标有“Anonymous type examples”的 GroupBox 中的 Forms 中看到这一点。

深入阅读
匿名类型 (Visual Basic) - MSDN
匿名类型 (C# 编程指南) - MSDN
为什么匿名类型是泛型的? 

再次,我也想向你推荐 Bart de Smet 的讲座。他在大约 45:30 分钟时解释了关于匿名类型的一些内容。
Bart de Smet - 10 个 C# 语言特性的幕后揭秘

4.6. Case 语句的案例

我接下来要谈论的是 Select Case 语句(C# 中的 switch 语句)。 这里发生了一些神奇的事情, Bart de Smet 在大约 13:40 分钟处对此进行了非常好的解释。 我建议您在继续之前观看这一部分。此时,对于阅读本文的 C# 开发者,我有一个坏消息。下一节仅适用于 VB(当然,您也非常欢迎阅读)。为什么仅限 VB?VB 有一种特殊的 Select Case,它颠倒了许多事情。常规的 Select Case 将一个值与另一个值进行比较,并在两个值相同(或 Else)时执行 Case。然而,在这种 Select Case 中,第一个返回 True 的语句所在的 Case 将被执行。 让我们来看一个常规 VB Case 的示例。你可以在 VB 解决方案的 SelectCaseExample 下找到代码。
Public Sub DoACase()
   Dim i As Integer = 10
 
   Select Case i
      Case 1
         Console.WriteLine("i = 1")
      Case 2
         Console.WriteLine("i = 2")
      Case 3
         Console.WriteLine("i = 3")
      Case Else
         Console.WriteLine("i is something else.")
   End Select
End Sub

如你所见,i 与 1、2 和 3 进行比较,只有当 Case 返回 True 时,case 中的代码才会执行。现在,让我们换个方式。

Public Sub DoATrueCase()
   Dim i As Integer = 10
 
   Select Case True
      Case i = 1
         Console.WriteLine("i = 1")
      Case i = 2
         Console.WriteLine("i = 2")
      Case i = 3
         Console.WriteLine("i = 3")
      Case Else
         Console.WriteLine("i is something else.")
   End Select
End Sub

如你所见,在这个 Select Case 中,我们测试了几条语句(只要它返回 Boolean 即可),并将其结果与 True 进行比较。然而,让我们看看它们各自生成的 IL。

.method public instance void  DoACase() cil managed
{
  // Code size       86 (0x56)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 VB$t_i4$L0,
           [2] int32 VB$CG$t_i4$S0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  nop
  IL_0005:  ldloc.0
  IL_0006:  ldc.i4.1
  IL_0007:  sub
  IL_0008:  stloc.2
  IL_0009:  ldloc.2
  IL_000a:  switch     ( 
                        IL_001d,
                        IL_002b,
                        IL_0039)
  IL_001b:  br.s       IL_0047
  IL_001d:  nop
  IL_001e:  ldstr      "i = 1"
  IL_0023:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0028:  nop
  IL_0029:  br.s       IL_0053
  IL_002b:  nop
  IL_002c:  ldstr      "i = 2"
  IL_0031:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0036:  nop
  IL_0037:  br.s       IL_0053
  IL_0039:  nop
  IL_003a:  ldstr      "i = 3"
  IL_003f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0044:  nop
  IL_0045:  br.s       IL_0053
  IL_0047:  nop
  IL_0048:  ldstr      "i is something else."
  IL_004d:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0052:  nop
  IL_0053:  nop
  IL_0054:  nop
  IL_0055:  ret
} // end of method SelectCaseExample::DoACase

你可以清楚地看到正在执行一个 Select Case(这是 switch 操作码)。所以,让我们看看第二个示例,其中 Select Case 不比较值,而是检查给定语句是否返回 True。

.method public instance void  DoATrueCase() cil managed
{
  // Code size       97 (0x61)
  .maxstack  3
  .locals init ([0] int32 i,
           [1] bool VB$t_bool$L0,
           [2] bool VB$CG$t_bool$S0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  nop
  IL_0005:  ldc.i4.1
  IL_0006:  stloc.1
  IL_0007:  nop
  IL_0008:  ldloc.1
  IL_0009:  ldloc.0
  IL_000a:  ldc.i4.1
  IL_000b:  ceq
  IL_000d:  ceq
  IL_000f:  stloc.2
  IL_0010:  ldloc.2
  IL_0011:  brfalse.s  IL_0020
  IL_0013:  ldstr      "i = 1"
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001d:  nop
  IL_001e:  br.s       IL_005e
  IL_0020:  nop
  IL_0021:  ldloc.1
  IL_0022:  ldloc.0
  IL_0023:  ldc.i4.2
  IL_0024:  ceq
  IL_0026:  ceq
  IL_0028:  stloc.2
  IL_0029:  ldloc.2
  IL_002a:  brfalse.s  IL_0039
  IL_002c:  ldstr      "i = 2"
  IL_0031:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0036:  nop
  IL_0037:  br.s       IL_005e
  IL_0039:  nop
  IL_003a:  ldloc.1
  IL_003b:  ldloc.0
  IL_003c:  ldc.i4.3
  IL_003d:  ceq
  IL_003f:  ceq
  IL_0041:  stloc.2
  IL_0042:  ldloc.2
  IL_0043:  brfalse.s  IL_0052
  IL_0045:  ldstr      "i = 3"
  IL_004a:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_004f:  nop
  IL_0050:  br.s       IL_005e
  IL_0052:  nop
  IL_0053:  ldstr      "i is something else."
  IL_0058:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_005d:  nop
  IL_005e:  nop
  IL_005f:  nop
  IL_0060:  ret
} // end of method SelectCaseExample::DoATrueCase

好吧,好吧,好吧!找不到一个 switch 操作码!我们这里有很多比较和 BRanch(分支)操作码。你看到的是,它实际上看起来像是生成了一个 If Then ElseIf ElseIf Else 语句。比较上一示例的 IL 与 DoAnIfThenElseIf 方法的 IL。你会看到一些相似之处。你可能也明白了为什么 C# 不支持它。它不是一个 switch 语句,但它也不如 If Then ElseIf Else 简洁。至于可读性,我留给你自己判断。

4.7. 迭代器的案例

虽然上一个示例是为 VB 阅读者准备的,但这个示例实际上是为 C# 用户准备的。迭代器方法 Iterator methods)在 C# 中已经存在了一段时间。它在 VS Async CTP 版本中出现在 VB 中,并且将默认包含在 VB11 中(至少我是这么被告知的)。我不会详细解释这一部分,因为 Bart de Smet 也对此做了很好的解释。我只想指出一些内容。
让我们先来看一个使用 Iterator 的代码示例。它可以在你的 C# 解决方案的 IteratorExample 文件夹中找到。

public static string UseTheItator()
{
   StringBuilder sb = new StringBuilder();
   foreach (string s in EnumeratorFunction())
   {
      sb.AppendLine(s);
   }
   return sb.ToString();
}
 
private static IEnumerable<string> EnumeratorFunction()
{
   string hello = "Hello";
   yield return hello;
   hello += " people!";
   yield return "Iterator!";
}

你认为 UseTheIterator 中的 StringBuilder 会返回什么?"Hello people! Iterator"?在 Main form 上按下 Iterator button 来找出答案。你会看到返回的文本是 "Hello Iterator"。这很奇怪,因为这意味着 hello 变量在 " people!" 被追加到它之前就已经返回到调用方法了,但 "Iterator!" 也被返回了。这正是 Iterator 的作用。yield 关键字 yield Keyword)告诉函数返回到调用方法,然后回来继续执行。它到底是怎么做到的呢?ILDASM 有答案。

天哪,IL!C# 编译器生成了一个实现 IEnumerable<string>IEnumerator<string>Type!如果我们查看原始 EnumeratorFunction 的 IL,我们会看到它只是返回这个生成类型的实例。

  .locals init ([0] class UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0' V_0,
           [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<string> V_1)
  IL_0000:  ldc.i4.s   -2
  IL_0002:  newobj     instance void UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::.ctor(int32)
  IL_0007:  stloc.0
  IL_0008:  ldloc.0
  IL_0009:  stloc.1
  IL_000a:  br.s       IL_000c
  IL_000c:  ldloc.1
  IL_000d:  ret
} // end of method IteratorExample::EnumeratorFunction

那么,返回 "Hello",追加 " people!" 等的逻辑在哪里呢?你可以在生成类型的 MoveNext 方法中找到所有这些。

.method private hidebysig newslot virtual final 
        instance bool  MoveNext() cil managed
{
  .override [mscorlib]System.Collections.IEnumerator::MoveNext
  // Code size       142 (0x8e)
  .maxstack  3
  .locals init ([0] bool CS$1$0000,
           [1] int32 CS$4$0001)
  IL_0000:  ldarg.0
  IL_0001:  ldfld      int32 UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<>1__state'
  IL_0006:  stloc.1
  IL_0007:  ldloc.1
  IL_0008:  switch     ( 
                        IL_001f,
                        IL_001b,
                        IL_001d)
  IL_0019:  br.s       IL_0021
  IL_001b:  br.s       IL_004d
  IL_001d:  br.s       IL_0080
  IL_001f:  br.s       IL_0023
  IL_0021:  br.s       IL_0088
  IL_0023:  ldarg.0
  IL_0024:  ldc.i4.m1
  IL_0025:  stfld      int32 UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<>1__state'
  IL_002a:  nop
  IL_002b:  ldarg.0
  IL_002c:  ldstr      "Hello"
  IL_0031:  stfld      string UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<hello>5__1'
  IL_0036:  ldarg.0
  IL_0037:  ldarg.0
  IL_0038:  ldfld      string UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<hello>5__1'
  IL_003d:  stfld      string UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<>2__current'
 // Etc...
  IL_0080:  ldarg.0
  IL_0081:  ldc.i4.m1
  IL_0082:  stfld      int32 UnderTheHoodCSharp.Examples.IteratorExample.IteratorExample/'<EnumeratorFunction>d__0'::'<>1__state'

那么我们在这里看到了什么? 每次调用 MoveNext 时,_state 字段就会增加一,并且根据 _state 字段的值,MoveNext 会执行另一段代码。感觉有点“脏”,不是吗?无论如何,编译器在将如此困难的事情隐藏起来不让程序员看到方面做得非常出色。这真是一件艺术品!

深入阅读
《Expert C# 5.0》一书的第 9 章对迭代器有非常详细的解释! 

正如我所说,你真的应该看看 Bart de Smet 关于 Iterators 的演讲。他大约在 36:30 分钟后开始讲 Iterators
Bart de Smet - 10 个 C# 语言特性的幕后揭秘

5. 使用 VB 或 C# 发出 IL 

正如我之前提到的,我们可以使用 VB 或 C# 发出自己的操作码并即时生成 IL!这正是我们在这里要做的事情。但是,我们不会生成任何方法,而是要生成一个使用 Try... Fault Block 的方法!这个功能在 VB 或 C# 中不可用,但在 IL 中可用。Try... Fault 块看起来像一个 Try... Catch,不同之处在于 Fault 块实际上并不捕获 Exception。它只是执行一些代码,但仅在抛出 Exception 时执行(并且总是如果抛出 Exception,就像 Try... Finally Block 一样)。这并不像听起来那么难,真的。打开你解决方案的 EmitExamples 文件夹中的 TypeFactory Class。 当你打开它时,你会看到一个 Public Shared(C# 中的 static)函数,它返回一个 Type。然而,这个 Type 是在函数第一次被调用时生成的。让我们看看 Type 是如何创建的。

首先,我们必须创建一个 Assembly(或 dll)来存放类型,我们可以使用 AssemblyBuilder 来实现。之后,我们创建一个 Module,使用 ModuleBuilder,它实际保存我们要创建的 Type。通过 Module,我们可以获得一个 TypeBuilder,它构建 Type 并让我们访问 MethodBuilders 来在 Type 上定义新方法。这些都很简单,对吧?让我们看看代码,你就会明白了。

' Create a new Assembly using an AssemblyBuilder.
Dim domain As AppDomain = System.Threading.Thread.GetDomain()
Dim assmName As New AssemblyName("DynamicAssembly")
Dim dynamicAssmBuilder As AssemblyBuilder = domain.DefineDynamicAssembly(assmName, AssemblyBuilderAccess.RunAndSave)
 
' Create a new Module using a ModuleBuilder.
Dim dynamicModule As ModuleBuilder = dynamicAssmBuilder.DefineDynamicModule("DynamicModule", "DynamicModule.dll")
 
' Create a new type using a TypeBuilder.
Dim dynamicTypeBuilder As TypeBuilder = dynamicModule.DefineType("DynamicType", TypeAttributes.Public)
// Create a new Assembly using an AssemblyBuilder.
AppDomain domain = System.Threading.Thread.GetDomain();
AssemblyName assmName = new AssemblyName("DynamicAssembly");
AssemblyBuilder dynamicAssmBuilder = domain.DefineDynamicAssembly(assmName, AssemblyBuilderAccess.RunAndSave);
 
// Create a new Module using a ModuleBuilder.
ModuleBuilder dynamicModule = dynamicAssmBuilder.DefineDynamicModule("DynamicModule", "DynamicModule.dll");
 
// Create a new type using a TypeBuilder.
TypeBuilder dynamicTypeBuilder = dynamicModule.DefineType("DynamicType", TypeAttributes.Public);

此时,我们已经完成了实现自己的类型方法所需的所有工作。这真的只需要几行代码!那么我们如何获得我们的方法呢? 我们可以在 GenerateILMethod 方法中看到一个例子。

Dim internalILMethod As MethodBuilder = typeBuilder.DefineMethod("InternalILMethod", _
      MethodAttributes.Private Or MethodAttributes.Static, Nothing, methodParams)
MethodBuilder internalILMethod = typeBuilder.DefineMethod("InternalILMethod", 
             MethodAttributes.Private | MethodAttributes.Static, null, methodParams);

如你所见,我们调用 TypeBuilder 上的 DefineMethod 方法,该方法返回一个 MethodBuilder。该方法将命名为 "InternalILMethod",并且是 PrivateShared(C# 中的 static)。它还需要两个参数,在本例中是 Action(Of String)(C# 中的 Action<string>)和一个 Boolean。既然我们有了 MethodBuilder,我们就想给它一些主体。我们想发出一些操作码,以便当我们以后调用该方法时,它能实际做些什么。我们通过调用 MethodBuilder 上的 GetILGenerator 来实现。GetILGenerator 返回当前方法的 ILGenerator 对象。然后,我们可以使用 ILGenerator 发出操作码。这实际上很简单,正如你将看到的。

' Use an ILGenerator to emit opcodes.
Dim internalILGen As ILGenerator = internalILMethod.GetILGenerator()
 
' A directive for the compiler.
Dim skipThrow As Label = internalILGen.DefineLabel
' Begin a try-finally block.
internalILGen.BeginExceptionBlock()
' Begin a try-fault block.
internalILGen.BeginExceptionBlock()
' Load the first argument (index 0), this is an Action(Of String).
' Be aware that if this were not Shared the first argument would be
' the object on who the method was called.
internalILGen.Emit(OpCodes.Ldarg_0)
' Push a string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Entered Emit method.")
' Invoke invoke on the Action(Of String) (it is first on the stack)
' and pass the string as a parameter (which is second on the stack).
internalILGen.Emit(OpCodes.Call, invokeInfo)
' Push the second parameter (index 1) on the stack, this is a boolean.
internalILGen.Emit(OpCodes.Ldarg_1)
' Check if the boolean is true by pushing a 1 on the stack and comparing it.
internalILGen.Emit(OpCodes.Ldc_I4_1)
' Compare the two values.
internalILGen.Emit(OpCodes.Ceq)
' If the boolean is false (does not equal 1) go to skipThrow.
internalILGen.Emit(OpCodes.Brfalse, skipThrow)
' If the boolean was not equal to 0 push a string (exception message) on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Well, that's it for you!")
' Create a new Exception. The exception message is on the stack and will be passed to the constructor.
internalILGen.Emit(OpCodes.Newobj, exType.GetConstructor(stringType))
' Throw the Exception.
internalILGen.Emit(OpCodes.Throw)
' If the third parameter to the method was False (0) then the code will skip the previous lines and continue here.
internalILGen.MarkLabel(skipThrow)
' Load the second argument again.
internalILGen.Emit(OpCodes.Ldarg_0)
' Push a string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Emit method finished successfully.")
' Invoke the invoke method on the Action(Of String) and pass the string as a parameter.
internalILGen.Emit(OpCodes.Call, invokeInfo)
 
' Begin the illusive Fault block!
' This code ONLY runs when an Exception was thrown, but does NOT catch the Exception.
internalILGen.BeginFaultBlock()
' Load the Action(Of T) again.
internalILGen.Emit(OpCodes.Ldarg_0)
' Push another string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Emit method finished unsuccessfully.")
' Again invoke the invoke method and pass the string (on the stack) as a parameter.
internalILGen.Emit(OpCodes.Call, invokeInfo)
' End the try-fault block.
internalILGen.EndExceptionBlock()
 
' Begin a finally block.
internalILGen.BeginFinallyBlock()
' Once again invoke the invoke method on the Action(Of String) (first parameter)
' with the string that was pushed onto the stack.
internalILGen.Emit(OpCodes.Ldarg_0)
internalILGen.Emit(OpCodes.Ldstr, "Leaving Emit method.")
internalILGen.Emit(OpCodes.Call, invokeInfo)
' End the try-finally block.
internalILGen.EndExceptionBlock()
 
' Return.
internalILGen.Emit(OpCodes.Ret)
// Use an ILGenerator to emit opcodes.
ILGenerator internalILGen = internalILMethod.GetILGenerator();
 
// A directive for the compiler.
Label skipThrow = internalILGen.DefineLabel();
// Begin a try-finally block.
internalILGen.BeginExceptionBlock();
// Begin a try-fault block.
internalILGen.BeginExceptionBlock();
// Load the first argument (index 0), this is an Action(Of String).
// Be aware that if this were not Shared the first argument would be
// the object on who the method was called.
internalILGen.Emit(OpCodes.Ldarg_0);
// Push a string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Entered Emit method.");
// Invoke invoke on the Action(Of String) (it is first on the stack)
// and pass the string as a parameter (which is second on the stack).
internalILGen.Emit(OpCodes.Call, invokeInfo);
// Push the second parameter (index 1) on the stack, this is a boolean.
internalILGen.Emit(OpCodes.Ldarg_1);
// Check if the boolean is true by pushing a 1 on the stack and comparing it.
internalILGen.Emit(OpCodes.Ldc_I4_1);
// Compare the two values.
internalILGen.Emit(OpCodes.Ceq);
// If the boolean is false (does not equal 1) go to skipThrow.
internalILGen.Emit(OpCodes.Brfalse, skipThrow);
// If the boolean was not equal to 0 push a string (exception message) on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Well, that's it for you!");
// Create a new Exception. The exception message is on the stack and will be passed to the constructor.
internalILGen.Emit(OpCodes.Newobj, exType.GetConstructor(stringType));
// Throw the Exception.
internalILGen.Emit(OpCodes.Throw);
// If the third parameter to the method was False (0) then
// the code will skip the previous lines and continue here.
internalILGen.MarkLabel(skipThrow);
// Load the second argument again.
internalILGen.Emit(OpCodes.Ldarg_0);
// Push a string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Emit method finished successfully.");
// Invoke the invoke method on the Action(Of String) and pass the string as a parameter.
internalILGen.Emit(OpCodes.Call, invokeInfo);
 
// Begin the illusive Fault block!
// This code ONLY runs when an Exception was thrown, but does NOT catch the Exception.
internalILGen.BeginFaultBlock();
// Load the Action(Of T) again.
internalILGen.Emit(OpCodes.Ldarg_0);
// Push another string on the stack.
internalILGen.Emit(OpCodes.Ldstr, "Emit method finished unsuccessfully.");
// Again invoke the invoke method and pass the string (on the stack) as a parameter.
internalILGen.Emit(OpCodes.Call, invokeInfo);
// End the try-fault block.
internalILGen.EndExceptionBlock();
 
// Begin a finally block.
internalILGen.BeginFinallyBlock();
// Once again invoke the invoke method on the Action(Of String) (first parameter)
// with the string that was pushed onto the stack.
internalILGen.Emit(OpCodes.Ldarg_0);
internalILGen.Emit(OpCodes.Ldstr, "Leaving Emit method.");
internalILGen.Emit(OpCodes.Call, invokeInfo);
// End the try-finally block.
internalILGen.EndExceptionBlock();
 
// Return.
internalILGen.Emit(OpCodes.Ret);

所以,我们现在有了一个方法,它看起来像这样:

Private Shared Sub InternalILMethod(ByVal a As Action(Of String), ByVal b As Boolean)
   Try
      a.Invoke("Entered Emit method.")
      If b = True Then
         Throw New Exception("Well, that's it for you!")
      End If
      a.Invoke("Emit method finished successfully.")
   Fault ' Not possible in VB!
      a.Invoke("Emit method finished unsuccessfully.")
   Finally
      a.Invoke("Leaving Emit method.")
   End Try
End Sub
private static void InternalILMethod(Action<string> a, bool b)
{
   try
   {
      a.Invoke("Entered Emit method.");
      if (b == true)
      {
         throw new Exception("Well, that's it for you!");
      }
      a.Invoke("Emit method finished successfully.");
   }
   fault // Not possible in C#!
   {
      a.Invoke("Emit method finished unsuccessfully.");
   }
   finally
   {
       a.Invoke("Leaving Emit method.");
   {
}

你应该记住一个小技巧,如果你需要像这样自己发出操作码。首先编写并构建你实际想要发出的代码,然后检查 IL,查看发出的 IL。这样做将极大地简化像这样的 IL 编码。所以,现在我们有了我们的方法,它使用一个 delegate 来向调用者传递一些消息,并且如果提供的 BooleanTrue,则可能抛出 Exception。如果抛出 Exception,代码将进入 Fault 块并发送消息 "Emit method finished unsuccessfully."。代码将始终执行 Finally 块中的部分。不过,我们现在遇到了一个小问题。我将动态调用这个方法,而 Exception 不会被我捕获,而是被动态调用者捕获,它将把异常包装到另一个 Exceptions InnerException 中,然后抛回给我。我更希望动态调用一个不抛出 Exception 的方法。所以,我们要做的是从另一个我们将要创建的方法来调用我们刚刚创建的方法。这个额外的方法将为我们捕获 Exception,并将 Exception MessageStackTrace 传递给 delegate

' Create another method using a MethodBuilder.
' This method calls the previous method.
' If the previous method throws an Exception this method will Catch it and write the message using a delegate.
' If the Exception is not caught in this code then the InvokeMember method
' will wrap the Exception in a TargetInvocationException.
Dim execILMethod As MethodBuilder = typeBuilder.DefineMethod("ExecuteILMethod", _
                 MethodAttributes.Public Or MethodAttributes.Static, Nothing, methodParams)
' Use an ILGenerator to emit opcodes.
Dim execILGen As ILGenerator = execILMethod.GetILGenerator
 
' Begin a try-catch block.
execILGen.BeginExceptionBlock()
' Load the Action(Of String) on the stack.
execILGen.Emit(OpCodes.Ldarg_0)
' Load the Boolean on the stack.
execILGen.Emit(OpCodes.Ldarg_1)
' Invoke the WriteInternal method on the current
' type and pass the delegate and Boolean as parameters.
execILGen.Emit(OpCodes.Call, internalILMethod)
 
' Begin a catch block.
execILGen.BeginCatchBlock(exType)
' Create a local variable, which will hold the caught Exception.
Dim ex As LocalBuilder = execILGen.DeclareLocal(exType)
' At this point the Exception is on the stack.
' Store it in the local variable we just created.
execILGen.Emit(OpCodes.Stloc_0)
' Load the delegate.
execILGen.Emit(OpCodes.Ldarg_0)
' Load the Exception.
execILGen.Emit(OpCodes.Ldloc_0)
' Get the getter Function of the Message Property on the Exception.
execILGen.Emit(OpCodes.Call, exType.GetProperty("Message").GetGetMethod)
' Pass the exception message to the Invoke method on the delegate.
execILGen.Emit(OpCodes.Call, invokeInfo)
 
' Load the delegate again.
execILGen.Emit(OpCodes.Ldarg_0)
' Load the Exception again.
execILGen.Emit(OpCodes.Ldloc_0)
' Get the getter Function of the StackTrace Property on the Exception.
execILGen.Emit(OpCodes.Call, exType.GetProperty("StackTrace").GetGetMethod)
' Pass the stacktrace to the Invoke method on the delegate.
execILGen.Emit(OpCodes.Call, invokeInfo)
 
' End the try-catch block.
execILGen.EndExceptionBlock()
 
' Return.
execILGen.Emit(OpCodes.Ret)
// Create another method using a MethodBuilder.
// This method calls the previous method.
// If the previous method throws an Exception this method will Catch it and write the message using a delegate.
// If the Exception is not caught in this code then the InvokeMember method will wrap the Exception in a TargetInvocationException.
MethodBuilder execILMethod = typeBuilder.DefineMethod("ExecuteILMethod", MethodAttributes.Public | MethodAttributes.Static, null, methodParams);
// Use an ILGenerator to emit opcodes.
ILGenerator execILGen = execILMethod.GetILGenerator();
 
// Begin a try-catch block.
execILGen.BeginExceptionBlock();
// Load the Action(Of String) on the stack.
execILGen.Emit(OpCodes.Ldarg_0);
// Load the Boolean on the stack.
execILGen.Emit(OpCodes.Ldarg_1);
// Invoke the WriteInternal method on the current type and pass the delegate and Boolean as parameters.
execILGen.Emit(OpCodes.Call, internalILMethod);
 
// Begin a catch block.
execILGen.BeginCatchBlock(exType);
// Create a local variable, which will hold the caught Exception.
LocalBuilder ex = execILGen.DeclareLocal(exType);
// At this point the Exception is on the stack.
// Store it in the local variable we just created.
execILGen.Emit(OpCodes.Stloc_0);
// Load the delegate.
execILGen.Emit(OpCodes.Ldarg_0);
// Load the Exception.
execILGen.Emit(OpCodes.Ldloc_0);
// Get the getter Function of the Message Property on the Exception.
execILGen.Emit(OpCodes.Call, exType.GetProperty("Message").GetGetMethod());
// Pass the exception message to the Invoke method on the delegate.
execILGen.Emit(OpCodes.Call, invokeInfo);
 
// Load the delegate again.
execILGen.Emit(OpCodes.Ldarg_0);
// Load the Exception again.
execILGen.Emit(OpCodes.Ldloc_0);
// Get the getter Function of the StackTrace Property on the Exception.
execILGen.Emit(OpCodes.Call, exType.GetProperty("StackTrace").GetGetMethod());
// Pass the stacktrace to the Invoke method on the delegate.
execILGen.Emit(OpCodes.Call, invokeInfo);
 
// End the try-catch block.
execILGen.EndExceptionBlock();
 
// Return.
execILGen.Emit(OpCodes.Ret);

呼,对于一个做的事情如此之少的方法来说,这代码量真够大的!嗯,这就是 IL 的本质。有一件事你应该注意到,我正在使用 MethodBody 对象来调用堆栈上的 Objects 的方法。在调用我们刚刚创建的 InternalILMethod 时,我可以直接将该方法的 MethodBuilder 作为参数传入。由于该方法是 Shared(C# 中的 static),所以我不需要对当前 Object 的引用。
所以,既然我们已经实现了两个方法,一个调用另一个,我们就必须实际创建 Type 才能使用它。幸运的是,这非常简单。我们只需调用 TypeBuilder 上的 CreateType 方法。

' This creates the type and closes it for any further notifications.
Dim dynamicType As Type = dynamicTypeBuilder.CreateType
' Save the assembly to disk so we can check out the emitted IL.
dynamicAssmBuilder.Save("DynamicModule.dll")
// This creates the type and closes it for any further notifications.
Type dynamicType = dynamicTypeBuilder.CreateType();
// Save the assembly to disk so we can check out the emitted IL.
dynamicAssmBuilder.Save("DynamicModule.dll"); 

现在我需要做的就是将 Type 返回给调用者,并调用我们刚刚生成的方法。您可以在 EmitForm 中看到它是如何完成的。您也可以从 Main form 打开 Emit form,看看当方法被调用(包括抛出 Exception 和不抛出 Exception 的情况)时会发生什么。您实际上可以在 StackTrace 中看到,我们确实创建了一个新方法,该方法在我们动态创建的 Type 中调用另一个方法!是不是很神奇!?

深入阅读

如果您想了解更多关于 Reflection.Emit 类的信息,我实际上推荐您阅读 MSDN 文档。例如, ILGenerator.BeginExceptionBlock ILGenerator.BeginCatchBlock 提供了相当不错的、经过精心设计的动态创建方法的示例。
我还可以推荐阅读《.NET 元编程》(Metaprogramming in .NET) 一书中第 5 章的第二部分。

奖励
CP 成员 Pieter van Parys 向我介绍了一个名为 'BLToolkit'(业务逻辑工具包)的工具。它包含一些很酷的功能,包括一些类,可以帮助您生成动态代码。特别是 EmitHelper,对于任何想使用 VB 或 C# 发出自己的操作码的人来说,都应该特别感兴趣!
感谢 Pieter van Parys 指出这一点 竖起大拇指 | <img src=   

6. 使用表达式树生成 IL

幸运的是,.NET Framework 提供了一种更短的发出 IL 的方法。它被称为 表达式树。您可能已经注意到,我们在动态 Type 上实际创建了三个方法:两个使用 IL,一个使用 Expression Trees。什么是 Expression Tree?它是以数据形式的代码表示。这听起来相当抽象,但请相信我,它不是。Expression Trees围绕着 System.Linq.Expressions.Expression Type。所有 Expressions 都继承自这个基类,并且所有 Expressions 都可以使用该类型上的 Shared(C# 中的 static)工厂方法创建。让我们看看创建上面 InternalILMethod 的代码,但这次使用 Expression Trees

Private Shared Function GenerateInnerExpressionTree(ByVal actionParam As ParameterExpression, _
        ByVal throwExParam As ParameterExpression, ByVal invokeInfo As MethodInfo) As Expression
Return Expression.TryFinally(
   Expression.TryFault(
      Expression.Block(
         Expression.Call(actionParam, invokeInfo, Expression.Constant("Entered Expression Tree method.")),
         Expression.IfThen(Expression.Equal(throwExParam, Expression.Constant(True)),
            Expression.Throw(Expression.[New](GetType(Exception).GetConstructor({GetType(String)}),
               Expression.Constant("Well, that's it for you!")))),
         Expression.Call(actionParam, invokeInfo, Expression.Constant("Expression Tree method finished successfully."))),
      Expression.Call(actionParam, invokeInfo, Expression.Constant("Expression Tree method finished unsuccessfully."))),
   Expression.Call(actionParam, invokeInfo, Expression.Constant("Leaving Expression Tree method.")))
End Function
private static Expression GenerateInnerExpressionTree(ParameterExpression actionParam, 
               ParameterExpression throwExParam, MethodInfo invokeInfo)
{
   return Expression.TryFinally(
      Expression.TryFault(
         Expression.Block(
            Expression.Call(actionParam, invokeInfo, Expression.Constant("Entered Expression Tree method.")),
            Expression.IfThen(Expression.Equal(throwExParam, Expression.Constant(true)),
               Expression.Throw(Expression.New(typeof(Exception).GetConstructor(new[] { typeof(string) }), 
               Expression.Constant("Well, that's it for you!")))),
               Expression.Call(actionParam, invokeInfo, 
               Expression.Constant("Expression Tree method finished successfully."))),
            Expression.Call(actionParam, invokeInfo, 
            Expression.Constant("Expression Tree method finished unsuccessfully."))),
        Expression.Call(actionParam, invokeInfo, Expression.Constant("Leaving Expression Tree method.")));
}

看起来容易吗?不完全是,主要是因为我将每个 Expression 嵌套在包含的 Expression 中。您应该这样阅读:我们声明一个 TryFinally Expression,它需要一个构成 Try 块体的 Expression 和一个构成 Finally 块体的 Expression。作为 Try 块的体,我们创建一个 TryFault Expression,顾名思义,它再次需要一个 Try 块的 Expression 和一个 Fault 块的 Expression。因此,对于 Try 块,我们创建一个 Expressions Block,以一个 Call Expression 开始,该表达式调用 Action(Of String)(C# 中的 Action<string>)参数上的 Invoke,并将 Constant Expression "Entered Expression Tree Method." 作为参数传递。我们 Block Expression 中的下一个 Expression 是一个 IfThenExpression,它当然需要一个 If 和一个 Then Expression。所以对于 If,我们创建一个 EqualExpression 并将 Boolean 参数与 ConstantTrue 进行比较。在 Then 块中,我们放一个 ThrowExpression,其中我们放一个 NewExpression 来创建 Exception。现在我们退出了 IfThenExpression,回到了 BlockExpression,我们在其中放入了对委托输入参数的 Invoke 方法的另一个调用。这构成了 Try 块,现在我们进入 Fault 块,在那里我们再次调用 Invoke。然后我们退出 Fault 块,进入 Finally 块,在那里我们最后一次调用 Invoke。这就构成了一个完整的 Expression。我承认这需要一些时间来适应,但一旦掌握了窍门,它就很有意义了。

这就是内部方法,现在让我们看看捕获 Exception 的方法。这里有一个问题……使用 Expression Trees 时,无法使用 MethodBuilder,就像我们使用 ILGenerator 时那样。据称,原因在于 MethodBuilder 仍然可能发生变化。我不知道为什么这个限制只适用于 Expression Trees,但这是我们必须接受的事实。因此,我没有传入方法,而是直接调用创建 Expression Tree 的函数,并在创建方法时,将 Expression Tree 与外部 Expression Tree 巧妙地组合起来。让我们看看代码中的样子。

Private Shared Sub GenerateExpressionTreeMethod(ByVal typeBuilder As TypeBuilder, ByVal methodParams As Type())
   Dim invokeInfo As MethodInfo = GetType(Action(Of String)).GetMethod("Invoke", {GetType(String)})
 
   ' Input parameters.
   Dim actionParam As ParameterExpression = Expression.Parameter(GetType(Action(Of String)), "action")
   Dim throwExParam As ParameterExpression = Expression.Parameter(GetType(Boolean), "throwEx")
 
   ' Local variable.
   Dim exParam As ParameterExpression = Expression.Parameter(GetType(Exception), "ex")
 
   Dim exp As Expression = Expression.TryCatch(
      TypeFactory.GenerateInnerExpressionTree(actionParam, throwExParam, invokeInfo),
         Expression.Catch(exParam,
            Expression.Block(
               Expression.Call(actionParam, invokeInfo,
                  Expression.Property(exParam, "Message")),
               Expression.Call(actionParam, invokeInfo,
                  Expression.Property(exParam, "StackTrace")))))
 
   Dim expTreeMethod As MethodBuilder = typeBuilder.DefineMethod("ExecuteExpressionTreeMethod", MethodAttributes.Public Or MethodAttributes.Static, Nothing, methodParams)
   Expression.Lambda(Of Action(Of Action(Of String), Boolean))(exp, actionParam, throwExParam).CompileToMethod(expTreeMethod)
End Sub
private static void GenerateExpressionTreeMethod(TypeBuilder typeBuilder, Type[] methodParams)
{
   MethodInfo invokeInfo = typeof(Action<string>).GetMethod("Invoke", new[] { typeof(string) });
 
   // Input parameters.
   ParameterExpression actionParam = Expression.Parameter(typeof(Action<string>), "action");
   ParameterExpression throwExParam = Expression.Parameter(typeof(bool), "throwEx");
 
   // Local variable.
   ParameterExpression exParam = Expression.Parameter(typeof(Exception), "ex");
 
   Expression exp = Expression.TryCatch(
      TypeFactory.GenerateInnerExpressionTree(actionParam, throwExParam, invokeInfo),
      Expression.Catch(exParam,
         Expression.Block(
            Expression.Call(actionParam, invokeInfo,
               Expression.Property(exParam, "Message")),
            Expression.Call(actionParam, invokeInfo,
               Expression.Property(exParam, "StackTrace")))));
 
   MethodBuilder expTreeMethod = typeBuilder.DefineMethod("ExecuteExpressionTreeMethod", MethodAttributes.Public | MethodAttributes.Static, null, methodParams);
   Expression.Lambda<Action<Action<string>, bool>>(exp, actionParam, throwExParam).CompileToMethod(expTreeMethod);
}

也许这段代码比其他函数更容易。此示例中值得注意的是,Expression Tree 被包装在 LambdaExpression 中,它可以被编译成新创建的方法。实际上,有两种选项可以编译 Expression Trees。一种是 CompileToMethod,它将 IL 发送到 MethodBuilder 参数。编译 Expression Tree 的另一种方法是调用 Compile 方法,该方法返回一个可以立即 Invokeddelegate。然而,由于我们使用的是 TryFault 块,这将抛出一个 NotSupportedException,因为 TryFault 块在 VB 和 C# 中不受支持。
同样,您可以通过从 Main form 打开 Expression tree form 来查看此方法的性能。现在您可以在 ExceptionStackTrace 中看到,只创建了一个方法。

还记得我们把创建的程序集保存到磁盘了吗?您可以在启动项目的 bin 文件夹中找到它。使用 ILDASM 打开它,并检查两种方法的发出的 IL。它们完全相同!这是 EmitExpression Trees 示例中 TryFault 块的 IL。

    .try
    {
      IL_0000:  ldarg.0
      IL_0001:  ldstr      "Entered Emit method."
      IL_0006:  call       instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
      IL_000b:  ldarg.1
      IL_000c:  ldc.i4.1
      IL_000d:  ceq
      IL_000f:  brfalse    IL_001f
      IL_0014:  ldstr      "Well, that's it for you!"
      IL_0019:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
      IL_001e:  throw
      IL_001f:  ldarg.0
      IL_0020:  ldstr      "Emit method finished successfully."
      IL_0025:  call       instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
      IL_002a:  leave      IL_003b
    }  // end .try
    fault
    {
      IL_002f:  ldarg.0
      IL_0030:  ldstr      "Emit method finished unsuccessfully."
      IL_0035:  call       instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
      IL_003a:  endfinally
    }  // end handler
    IL_003b:  leave      IL_004c
  }  // end .try
      .try
      {
        IL_0000:  ldarg.0
        IL_0001:  ldstr      "Entered Expression Tree method."
        IL_0006:  callvirt   instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
        IL_000b:  ldarg.1
        IL_000c:  ldc.i4.1
        IL_000d:  ceq
        IL_000f:  brfalse    IL_001f
        IL_0014:  ldstr      "Well, that's it for you!"
        IL_0019:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
        IL_001e:  throw
        IL_001f:  ldarg.0
        IL_0020:  ldstr      "Expression Tree method finished successfully."
        IL_0025:  callvirt   instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
        IL_002a:  leave      IL_003b
      }  // end .try
      fault
      {
        IL_002f:  ldarg.0
        IL_0030:  ldstr      "Expression Tree method finished unsuccessfully."
        IL_0035:  callvirt   instance void class [mscorlib]System.Action`1<string>::Invoke(!0)
        IL_003a:  endfinally
      }  // end handler
      IL_003b:  leave      IL_004c
    }  // end .try

它们看起来确实很相似!因此,可以使用 Reflection.Emit 操作码或 Expression Trees 来发出 IL。这两种方法都有其优点和缺点。Reflection.Emit 的缺点显然是您需要大量的代码才能完成工作,而且调试相当困难(但并非不可能)。优点是天空才是极限,几乎没有什么事情是 Emit 做不到的!Expression Trees 的优点是它更容易理解和调试,尤其是在您逐块编写它时(尽管在嵌套它们时您会获得良好的 IntelliSense 支持)。缺点是它实际上文档很少。MSDN 上关于 Expression Classes 的大多数页面都没有示例,甚至没有描述!此外,Expression Trees 存在一些限制,正如我们所遇到的,我们无法调用尚未存在的方法。另一个限制是,使用 Expression Trees,我们只能生成 Sharedstatic)方法。这可能是一个问题,也可能不是。

深入阅读
虽然我没有关于表达式树的实际文档,但您可以像往常一样查看 MSDN。
您应该阅读《.NET 元编程》(Metaprogramming in .NET) 一书的第 6 章。

7. F# 的奇特之处

还有两件事我想看看。函数作为一等公民和尾调用。这两者都是 Microsoft 的 函数式编程语言 F# 的特性。

7.1. 函数作为一等公民的案例

F#(和其他函数式语言)将函数视为一等公民,这意味着它们可以作为参数传递给其他函数,由函数返回,并存储在变量中。基本上,一等函数就像对待任何其他变量一样,比如 IntegerString。VB 和 C# 可以通过 委托 某种程度上模仿这种行为,但并不完全相同。您可以下载本文顶部的 TheCuriousCaseOfFSharp 示例项目。打开解决方案并查看代码。您将看到的第一个是:

// Functions as 'first class citizens'.
// This function returns a new function which takes a function as an argument.
let SomeFunc a b c d =
    let newFunc f = f (a, b) + f (c, d)
    newFunc
 
// Call SomeFunc passing in four integers and passing in an anonymous function
// that adds two integers to the function that is returned by SomeFunc.
let resultAdd = SomeFunc 1 2 3 4 (fun (a, b) -> a + b)
// Do the same as above, but multiply the integers.
let resultMult = SomeFunc 1 2 3 4 (fun (a, b) -> a * b)
 
// Print the results.
printfn "The result of adding: %d" resultAdd
printfn "The result of multiplying: %d" resultMult

嗯,这确实很容易!一个返回一个函数,该函数需要一个函数作为参数的函数。注意传递给从 SomeFunc 返回的函数的 lambda 表达式。我们已经在 VB 和 C# 中知道了这一点,它们是从 F# 继承的。那么您怎么看?编译器是否像我们在之前的 lambda 示例中看到的那样,简单地创建了 Sharedstatic)函数?

如您所见,每个函数都会创建一个新的 Type。新类型实际上 Inherits FSharpFunc(Of T, U)FSharpFunc<T, U>)。现在,每当一个 FSharpFunc 被“用作值”时,编译器就会生成一个对 FSharpFuncInvoke 方法的调用。我在这里无法显示生成的 IL,因为它不适合屏幕,但您可以自己查看。您应该在这里查看。

您应该留意这里。

IL_0074:  callvirt   instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32,class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)

我从未在代码中调用 Invoke,这就是编译器为我所做的。这就是它的精髓。函数作为一等公民!

7.2. 尾调用的案例

因此,让我们看看尾调用。函数式语言在 递归方面表现出色。递归是指一个可能调用自身的函数。如果您不注意,就会得到一个 StackOverflowException!当对同一个函数的调用次数达到一定数量时就会发生这种情况。我不确定何时发生,但它确实会发生。看看下面的 VB 和 C# 代码。

Public Function GetTenMillion(ByVal i As Integer) As Integer
   If i < 10000000 Then
      Return GetTenMillion(i + 1)
   ElseIf i > 10000000 Then
      Return GetTenMillion(i - 1)
   Else
      Return i
   End If
End Function
public int GetTenMillion(int i)
{
   if (i < 10000000)
   { return GetTenMillion(i + 1); }
   else if (i > 10000000)
   { return GetTenMillion(i - 1); }
   else
   { return i; }
}

任何有经验的程序员都知道,如果以接近 10000000 的输入参数调用此函数,它都会导致 StackOverflowException。所以如果我们用输入 1 调用它,我们的应用程序肯定会崩溃。嗯,这里的重点是,在 F# 中不会!这是相同函数以及调用代码的 F# 代码。

// Recursive 'tail call'. Eliminates the stack before calling a method.
let rec GetTenMillion i =
    if i < 10000000
    then GetTenMillion (i + 1)
    elif i > 10000000
    then GetTenMillion (i - 1)
    else i
 
// This would throw a StackOverflowException in VB or C#!
GetTenMillion 1 |> printfn "GetTenMillion from 1: %d"

那么这个 '尾调用' 是什么?好吧,每当对递归函数的调用是该函数的最后一个语句时,调用堆栈就会在调用之前被清除。所以在这个例子中,我们可以看到,如果 i 小于一千万,我们就用 i + 1 再次调用 GetTenMillion。在此示例中,i + 1 在调用 GetTenMillion 之前执行,之后什么都不发生。这会导致调用堆栈被清除。例如,如果我们会在函数执行之后加 1,它将被编译为堆栈上的另一个调用,并且可能会引发 StackOverflowException。因此,让我们看看这个函数的 IL。

.method public static int32  GetTenMillion(int32 i) cil managed
{
  // Code size       41 (0x29)
  .maxstack  4
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldc.i4     0x989680
  IL_0007:  bge.s      IL_000b
  IL_0009:  br.s       IL_000d
  IL_000b:  br.s       IL_0014
  IL_000d:  ldarg.0
  IL_000e:  ldc.i4.1
  IL_000f:  add
  IL_0010:  starg.s    i
  IL_0012:  br.s       IL_0000
  IL_0014:  ldarg.0
  IL_0015:  ldc.i4     0x989680
  IL_001a:  ble.s      IL_001e
  IL_001c:  br.s       IL_0020
  IL_001e:  br.s       IL_0027
  IL_0020:  ldarg.0
  IL_0021:  ldc.i4.1
  IL_0022:  sub
  IL_0023:  starg.s    i
  IL_0025:  br.s       IL_0000
  IL_0027:  ldarg.0
  IL_0028:  ret
} // end of method Program::GetTenMillion 

您看到了吗?没有发出一个 call 操作码!发生的是什么?如果 i 小于一千万,则将 1 添加到 i,然后我们只需分支回函数的开头。如果 i 大于一千万,则发生同样的情况,只是从 i 中减去 1。简单,但相当有效!使用 Reflection.Emit,您可以使用此技巧来创建自己的非常深的递归函数。

您可以运行 F# 应用程序,并真正看到打印出 10000000 并且没有引发 StackOverflowException。

深入阅读:
一本极大地帮助我理解 F# 的书是 Apress 出版的 Expert F# 2.0

8. 后记

好吧,这(对我而言)绝对是一大堆写作(对您而言)和阅读。正如我在引言中所说,这不是一个容易的主题。我希望我已经尽可能地简化了它,并且您在阅读它时获得了和我写作时一样的乐趣。我写的大部分内容在我开始写作之前对我来说都是新知识,所以可以说我学到了很多,希望您也能说同样的话。

我很乐意回答任何问题或评论。 

祝您编码愉快! 

© . All rights reserved.