更新:这个问题是我在2010年2月4日的博客主题。感谢提出了这个好问题!
让我为您解释一下。最基本的意义上,编译器是一个“两遍编译器”,因为编译器经历的阶段是:
- 生成元数据。
- 生成IL。
元数据是描述代码结构的所有“顶层”内容。命名空间、类、结构体、枚举、接口、委托、方法、类型参数、形式参数、构造函数、事件、属性等等。基本上,除了方法体之外的一切都是元数据。
IL包含的是方法体中的所有内容 - 实际的命令式代码,而不是关于代码结构的元数据。
第一个阶段实际上是通过对源码进行多次通行来实现的。它超过了两次。
我们要做的第一件事就是将源代码的文本分割成一系列的标记流。也就是说,我们进行了词法分析,以确定
class c : b { }
是类、标识符、冒号、标识符、左大括号、右大括号。
然后我们进行“顶层解析”,验证令牌流是否定义了一个语法正确的C#程序。但是,我们跳过解析方法体。当我们遇到方法体时,我们只需快速通过令牌,直到找到匹配的闭合大括号。稍后我们会回来处理它;此时我们只关心获取足够的信息以生成元数据。
然后我们进行“声明”遍历,记录程序中每个命名空间和类型声明的位置。
然后我们进行遍历,验证所有声明的类型在其基类型中没有循环。我们需要首先执行此操作,因为在随后的每个遍历中,我们需要能够沿着类型层次结构向上行走,而不必处理循环。
然后我们进行遍历,验证泛型类型上的所有泛型参数约束也是无环的。
然后我们进行遍历,检查每个类型的每个成员——类的方法、结构体的字段、枚举值等——是否一致。没有枚举中的循环,每个重写方法都重写了实际虚拟的内容等。此时,我们可以计算所有接口、具有虚拟方法的类等的“虚函数表”布局。
然后我们进行遍历,计算所有“常量”字段的值。
此时,我们已经有足够的信息来生成几乎所有此程序集的元数据。我们仍然没有有关迭代器/匿名函数闭包或匿名类型的元数据信息;我们稍后处理这些。
现在我们可以开始生成IL代码。对于每个方法体(以及属性、索引器、构造函数等),我们将词法分析器倒回到方法体开始的位置,并解析该方法体。
一旦方法体被解析,我们就进行了初始的“绑定”处理,在这个过程中,我们尝试确定每个语句中每个表达式的类型。然后我们对每个方法体进行了一整堆的处理。
首先,我们运行一个处理循环转换为goto和标签的过程。
(接下来的几个步骤寻找不良内容。)
然后我们运行一个检查使用已弃用类型的警告的过程。
然后我们运行一个搜索我们尚未为其发出元数据的匿名类型使用的过程,并发出这些警告。
然后我们运行一个搜索表达式树的错误使用情况的过程。例如,在表达式树中使用++运算符。
然后我们运行一个查找在主体中定义但未使用的所有局部变量的过程,以报告警告。
然后我们运行一个查找迭代器块内部非法模式的过程。
然后我们运行可达性检查器,以给出有关无法访问的代码的警告,并在您忘记在非void方法的末尾返回时通知您。
然后我们运行一个验证每个goto目标合理的标签的过程,并且每个标签都是由可达的goto目标。
然后我们运行一个检查所有局部变量在使用之前都被明确分配的过程,记录哪些局部变量是匿名函数或迭代器的外部变量,并且哪些匿名函数在可达代码中。 (此过程做得太多了。我一直想重构它。)
此时,我们已经完成了寻找不良内容的工作,但是在睡觉之前我们还有更多的处理要做。
接下来我们运行一个程序,检测COM对象调用中缺少的引用参数并进行修复。(这是C# 4中的新功能。)
然后,我们运行一个程序,查找"new MyDelegate(Foo)"形式的内容,并将其重写为对CreateDelegate的调用。
接着,我们运行一个程序,将表达式树转换为必须在运行时创建表达式树的工厂方法调用序列。
然后,我们运行一个程序,将所有可空算术转换为测试HasValue的代码,等等。
然后,我们运行一个程序,查找所有base.Blah()形式的引用,并将它们重写为调用基类方法的非虚拟调用代码。
然后,我们运行一个程序,查找对象和集合初始化程序,并将它们转换为适当的属性设置,等等。
接下来,我们运行一个程序,查找动态调用(C# 4中),并将它们重写为使用DLR的动态调用站点。
然后,我们运行一个程序,查找已删除的方法的调用。(也就是说,没有实际实现的部分类方法,或者没有定义条件编译符号的条件方法。)它们被转换为no-op(no operation)。
然后,我们查找无法访问的代码并从树中删除它。没有必要将它编译成IL。
接下来,我们运行一个优化程序,重写微不足道的"is"和"as"运算符。
然后,我们运行一个优化程序,查找switch(constant)并将其重写为直接跳转到正确case的分支。
然后,我们运行一个程序,将字符串连接转换为对String.Concat正确重载版本的调用。
(啊,回忆。当我加入编译器团队时,这两个步骤是我首先要处理的事情。)
然后我们运行一个步骤,将命名和可选参数的用法重写为调用,其中副作用按正确顺序发生。
然后我们运行一个步骤来优化算术运算;例如,如果我们知道 M() 返回 int,并且我们有 1 * M(),那么我们只需将其转换为 M()。
然后我们首先生成此方法使用的匿名类型的代码。
然后我们将此体中的匿名函数转换为闭包类的方法。
最后,我们将迭代器块转换为基于 switch 的状态机。
然后我们发出刚刚计算出的转换树的 IL。
简单易懂!