Roslyn 深度:从语法树到 IL 的编译魔法
系列:C#与常用数据结构源码剖析 · 运行时底层剖析
阅读时间:约 45 分钟
前置知识:C# 语法、IL 基础
一、引言
你写下 var list = new List<int>() { 1, 2, 3 },Roslyn 编译器把它变成 IL。但中间发生了什么?编译器不是黑盒——它有一套精密的四层流水线,每一层都在做不同的事情。
理解 Roslyn 的编译过程,你就能精确知道你的 C# 代码在编译时被做了什么。哪些语法糖被展开了,哪些代码被优化掉了,哪些额外的代码被自动生成了。你看到的 foreach,编译后可能变成了 while 加枚举器。你写的 record,编译器悄悄给你加了一整套 Equals、GetHashCode、ToString、Deconstruct。
Roslyn 的完整代码在 dotnet/roslyn 仓库中。核心编译管线入口在 Microsoft.CodeAnalysis.Compilation 类,Lowering 实现在 Microsoft.CodeAnalysis.CSharp.Lowering 命名空间下。本文从编译原理的高度,带你看懂这张蓝图。
二、Roslyn 的四层编译流水线
Roslyn 的编译过程分为四个阶段,每个阶段都产生独特的数据结构。
2.1 第一层:SyntaxTree(语法树)
词法分析(Lexer)
源代码文本进入编译器的第一站是 Lexer。词法分析器逐字符扫描,将文本流切割为 token 序列:关键字(var)、标识符(list)、运算符(=)、字面量(1)、标点符号(;)。每个 token 携带位置信息——它在源文件中的起始行、列、长度。
Roslyn 的 Lexer 实现在 Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer 中。它不跳过空白和注释——这些被保留为 SyntaxTrivia,后面会讲。
"var list = new List<int>() { 1, 2, 3 }"
→
[
Keyword("var"), // 上下文关键字,此处作为隐式类型声明
Identifier("list"),
EqualsToken("="),
Keyword("new"),
Identifier("List"),
OpenAngle("<"),
Keyword("int"),
CloseAngle(">"),
OpenParen("("),
CloseParen(")"),
OpenBrace("{"),
NumericLiteral("1"),
Comma(","),
NumericLiteral("2"),
Comma(","),
NumericLiteral("3"),
CloseBrace("}"),
Semicolon(";")
]
语法分析(Parser)
token 序列进入 Parser,根据 C# 语法规则构建抽象语法树(AST)。Roslyn 的 Parser 是递归下降解析器,手写的(不是用 Yacc/Bison 生成的),这意味着它的解析错误恢复能力极强——即使遇到语法错误,Parser 也能尽量多地继续解析,给出更友好的错误信息。
对于 var list = new List<int>() { 1, 2, 3 },Parser 生成一棵这样的树:
LocalDeclarationStatementSyntax
├── VariableDeclarationSyntax
│ ├── IdentifierNameSyntax "var"
│ └── VariableDeclaratorSyntax
│ ├── IdentifierToken "list"
│ └── EqualsValueClauseSyntax
│ └── ObjectCreationExpressionSyntax
│ ├── GenericNameSyntax "List<int>"
│ │ ├── IdentifierToken "List"
│ │ └── TypeArgumentListSyntax
│ │ └── PredefinedTypeSyntax "int"
│ ├── ArgumentListSyntax "()"
│ └── InitializerExpressionSyntax "{ 1, 2, 3 }"
│ ├── LiteralExpressionSyntax "1"
│ ├── LiteralExpressionSyntax "2"
│ └── LiteralExpressionSyntax "3"
└── SemicolonToken ";"
Green Tree vs Red Tree
Roslyn 语法树有两层表示,这是一个值得专门讲的设计决策。
Green Tree(绿色树):不可变、不包含位置信息、不包含父引用。它在解析时构建,所有 token 都是 InternalSyntaxNode 类型。因为是纯不可变的,Green Tree 可以被安全地缓存和复用——在 IDE 中,当你修改了一个字符,Roslyn 不会重建整棵树,而是复用大部分节点。Green Tree 驻留在 Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax 命名空间中。
Red Tree(红色树):在 Green Tree 之上包了一层。每个 Red 节点包含对父节点的引用、位置信息(从 Green 节点计算得到)、以及 Directives(预处理指令)。Red Tree 是 Roslyn 对外暴露的 API——你在代码分析器中操作的 IfStatementSyntax 就是 Red 节点。红色树是"按需构建"的:当且仅当某个节点被访问时,Roslyn 才创建对应的 Red 包装。
这种设计的精妙之处在于:IDE 中的语法高亮、大纲显示、代码补全都依赖这棵树,但编译器进程是常驻的。如果每次编辑都新建整棵树,内存开销不可接受。Green/Red 分离让 Roslyn 可以在不丢失父链信息的前提下,大量复用底层节点。
SyntaxTrivia:不被遗忘的角落
C# 代码中的空白、换行、注释、预处理指令(#if、#region)在 Roslyn 中被统一表示为 SyntaxTrivia。Trivia 附着在 token 上——每个 token 都有前导 Trivia(Leading Trivia)和后置 Trivia(Trailing Trivia)。
// 这是一条注释
var list = /* 内联注释 */ new List<int>();
在上面的代码中:
vartoken 的前导 Trivia:换行 +// 这是一条注释newtoken 的前导 Trivia:空格 +/* 内联注释 */ + 空格
保留 Trivia 的意义在于:Roslyn 的格式化器(CodeFormatter)和代码修复器(CodeFix)在修改代码后,需要重新生成和之前风格一致的输出。如果丢弃了注释和空白,格式化后的代码会丢失有用信息。
2.2 第二层:SemanticModel(语义模型)
语法树告诉你"代码长什么样",语义模型告诉你"代码是什么意思"。
Bind 过程
var list = new List<int>() 中的 var 是什么意思?语义上它不是一个具体的类型名——它需要被绑定到实际的类型。Binder 负责这件事。
Roslyn 的绑定过程由 Binder 类体系完成。每个语法节点对应一个或多个 Binder:
GlobalBinder:最外层,处理命名空间级别的绑定LocalBinder:方法体内部的绑定,处理局部变量MemberBinder:类型成员级别的绑定WithQueryBinder:处理 LINQ 查询表达式的特殊绑定
Binder 在绑定 var list = new List<int>() 时的步骤:
- 看到
var,根据上下文判断它是一个隐式类型声明——list的类型由右侧初始化表达式推断 - 看到
new List<int>(),查找名为List的类型——在System.Collections.Generic命名空间中找到List<T>,或用using指令 - 看到
int类型参数,绑定到System.Int32 - 查找
List<T>的构造函数——找到public List()(无参构造函数) - 看到初始化器
{ 1, 2, 3 },验证是否存在Add(int)方法——找到List<T>.Add(T)方法 - 最终确定
list的类型是List<int>
符号解析(Symbol Resolution)
SemanticModel 通过 GetSymbolInfo() 方法暴露绑定结果。你可以向它问任何节点的语义信息:
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var node = ... // 得到语法树中的某个 IdentifierNameSyntax
SymbolInfo info = semanticModel.GetSymbolInfo(node);
// info.Symbol → 这个标识符引用的符号
Symbol 的类层次结构反映了 C# 的类型系统:
ISymbol
├── ITypeSymbol (类、结构体、接口、委托)
│ ├── INamedTypeSymbol (有名字的泛型或非泛型类型)
│ └── IArrayTypeSymbol (数组类型)
├── IMethodSymbol (方法,包括构造函数、运算符)
├── IPropertySymbol (属性)
├── IFieldSymbol (字段)
├── IEventSymbol (事件)
├── ILocalSymbol (局部变量)
├── IParameterSymbol (参数)
├── ITypeParameterSymbol (类型参数 T)
└── INamespaceSymbol (命名空间)
重载解析(Overload Resolution)
对于方法调用,Binder 执行重载解析。C# 的重载解析算法由 C# 语言规范 §7.5 定义,Roslyn 的实现在 OverloadResolution 类中。它的核心逻辑:
- 收集所有候选方法——参数数量匹配、名称匹配、可访问
- 从候选中排除不可用的——泛型参数推断失败、约束不满足
- 应用"更好性"规则——参数类型更具体的胜出,非泛型优于泛型
- 如果有且只有一个最优候选,选它;否则报歧义错误
对于 List<int> 的初始化器,重载解析找到 Add(T) 方法,因为 int 与 T 匹配(当 T = int 时)。如果集合有多个重载的 Add 方法,相同算法适用。
类型推断(Type Inference)
var x = ... 的本质是类型推断。Roslyn 的推断算法比大多数人想象的复杂得多——它需要处理泛型类型参数之间的约束关系。
类型推断分三个阶段:
- 第一遍:从参数到参数类型的推断(输入推断)
- 第二遍:从参数类型到其他类型参数的推断(输出推断)
- 第三遍:修正阶段——锁定所有自由的类型参数
对于一个调用如 M<string, int>("hello", 42),类型推断非常简单。但对于 var result = list.Select(x => x.Length),推断器需要先确定 list 的类型是 List<string>,然后确定 Select 的泛型参数,然后从 lambda 推断 x 的类型是 string,然后确定 x.Length 的返回类型是 int,最后确定 result 是 IEnumerable<int>。这全都发生在编译期,零运行时开销。
2.3 第三层:BoundTree 与 Lowering
语义绑定之后,Roslyn 产生一棵新的中间表示——BoundTree。这不是 C# 语法树,而是一种更接近 IL 的中间数据结构。
BoundTree 的节点类型以 Bound 开头:BoundExpressionStatement、BoundCall、BoundLocal、BoundObjectCreationExpression。它们比语法节点"更低级"——语法中的 foreach 在 BoundTree 阶段已经被展开为 while 循环,?? 操作符被展开为条件分支。
BoundTree 为什么存在?因为 C# 有大量语法糖,而 IL 没有这些糖。Roslyn 不能直接把 foreach 翻译成 IL——它要先把它降级为 IL 能表示的基本结构。
Lowering 的实现分布在 Microsoft.CodeAnalysis.CSharp.Lowering 命名空间中,主要有:
LocalRewriter / BoundTreeRewriter
├── ForEachRewriter (foreach → while)
├── UsingRewriter (using → try-finally)
├── LockRewriter (lock → Monitor.Enter/Exit)
├── AsyncRewriter (async/await → 状态机)
├── AwaitRewriter (await → 状态机挂起/恢复)
├── YieldRewriter (yield → 状态机)
├── WithExpressionRewriter (with → Clone$ + setter)
├── TupleRewriter (元组 → ValueTuple 类型)
├── CollectionExpressionRewriter (C#12 集合表达式)
├── QueryRewriter (LINQ 查询表达式 → 方法链)
├── PatternRewriter (模式匹配 → switch)
└── SwitchRewriter (switch → 跳转表或 if 链)
foreach 的 Lowering:完整分析
// 源代码
foreach (var item in list) {
Console.WriteLine(item);
}
Lowering 后的 BoundTree 等价于:
{
List<int>.Enumerator enumerator = list.GetEnumerator();
try {
while (enumerator.MoveNext()) {
int item = enumerator.Current;
Console.WriteLine(item);
}
} finally {
if (enumerator != null) {
((IDisposable)enumerator).Dispose();
}
}
}
但这只是个简化版本。实际的 Lowering 细节更多:
数组的特殊路径:如果 list 是数组(int[]),Roslyn 走另一条完全不同的路径。C# 语言规范(§12.9.5)规定数组的 foreach 不使用枚举器模式,而是生成索引循环:
for (int i = 0; i < array.Length; i++) {
int item = array[i];
Console.WriteLine(item);
}
为什么?性能。数组索引访问比调用 MoveNext()/Current 快得多——没有虚方法调用、没有边界检查的开销(JIT 可以消除数组边界检查)。
Span 和 ReadOnlySpan 的特殊路径:从 C# 7.2 开始,foreach 还能遍历 Span<T> 和 ReadOnlySpan<T>。Roslyn 对它们生成更高效的代码——直接使用索引器 span[i],不使用枚举器。因为 Span<T> 的索引器是一个内部方法,JIT 可以内联并消除边界检查。
模式匹配扩展(C# 9+):如果集合类型有 public int GetEnumerator() 返回 Enumerator 类型,且 Enumerator 有 public bool MoveNext() 和 public T Current,foreach 直接调用它们而不检查 IDisposable。这让 struct 枚举器可以避免装箱。
using 的 Lowering
// using 声明(C# 8+)
using var file = File.OpenRead("data.txt");
降级为:
FileStream file = File.OpenRead("data.txt");
try {
// 使用 file
} finally {
if (file != null) {
((IDisposable)file).Dispose();
}
}
如果 file 的值类型(比如 struct 实现了 IDisposable),Dispose 调用不会装箱——Roslyn 在编译期就知道具体类型,直接调用 Dispose 方法,不做虚分派。
lock 的 Lowering
lock (syncRoot) {
list.Add(item);
}
降级为:
bool lockTaken = false;
try {
Monitor.Enter(syncRoot, ref lockTaken);
list.Add(item);
} finally {
if (lockTaken) {
Monitor.Exit(syncRoot);
}
}
注意 lockTaken 变量——这是 C# 4 引入的改进模式。之前是 Monitor.Enter(syncRoot) + try-finally,存在极小的竞态窗口:如果 Enter 成功后但在进入 try 前抛出异常,锁永远不会被释放。ref lockTaken 模式修复了这个问题。
async/await 的 Lowering:状态机生成
这是 Roslyn 最复杂的 Lowering 之一。async 方法不是简单的代码展开——编译器生成一个完整的状态机结构。
public async Task<int> GetDataAsync() {
using var http = new HttpClient();
string json = await http.GetStringAsync(url);
return json.Length;
}
编译器生成的等价结构(高度简化):
[AsyncStateMachine(typeof(GetDataAsyncStateMachine))]
public Task<int> GetDataAsync() {
var stateMachine = new GetDataAsyncStateMachine();
stateMachine.builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.state = -1;
stateMachine.builder.Start(ref stateMachine);
return stateMachine.builder.Task;
}
private struct GetDataAsyncStateMachine : IAsyncStateMachine {
public int state; // -1=初始, 0=await后
public AsyncTaskMethodBuilder<int> builder;
public HttpClient http;
public string json;
public string url;
private TaskAwaiter<string> awaiter;
void IAsyncStateMachine.MoveNext() {
try {
if (state == -1) {
http = new HttpClient();
awaiter = http.GetStringAsync(url).GetAwaiter();
if (awaiter.IsCompleted) goto AWAIT_DONE;
state = 0;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
AWAIT_DONE:
json = awaiter.GetResult();
builder.SetResult(json.Length);
} catch (Exception ex) {
builder.SetException(ex);
}
}
}
关键洞察:状态机中的每个 await 都是一个可能的"挂起点"。状态机字段存储了所有在 await 之后还需要访问的局部变量。编译器分析每个局部变量的存活范围,精确决定哪些变量需要提升到状态机字段中。
record 的 Lowering:编译器生成的代码
从 C# 9 开始的 record 类型是 Roslyn 自动代码生成的最佳示例之一。
public record Person(string First, string Last, int Age);
编译器实际上生成的是:
public class Person : IEquatable<Person> {
public string First { get; init; }
public string Last { get; init; }
public int Age { get; init; }
// 主构造函数初始化
public Person(string First, string Last, int Age) {
this.First = First;
this.Last = Last;
this.Age = Age;
((object)this).<Clone>$(); // 基类初始化
}
// 解构方法
public void Deconstruct(out string First, out string Last, out int Age) {
First = this.First;
Last = this.Last;
Age = this.Age;
}
// ToString 的逐字段版本
public override string ToString() {
return $"Person {{ First = {First}, Last = {Last}, Age = {Age} }}";
}
// 逐字段相等比较
public virtual bool Equals(Person? other) {
return (object)this == other
&& EqualityComparer<string>.Default.Equals(First, other.First)
&& EqualityComparer<string>.Default.Equals(Last, other.Last)
&& EqualityComparer<int>.Default.Equals(Age, other.Age);
}
// 逐字段哈希
public override int GetHashCode() {
return Combine(
EqualityComparer<string>.Default.GetHashCode(First),
EqualityComparer<string>.Default.GetHashCode(Last),
EqualityComparer<int>.Default.GetHashCode(Age)
);
}
// Clone 方法,用于 with 表达式
protected virtual Person <Clone>$() {
return (Person)base.MemberwiseClone();
}
// == 和 != 运算符重载
public static bool operator ==(Person left, Person right) => left.Equals(right);
public static bool operator !=(Person left, Person right) => !left.Equals(right);
}
所有这些代码都是编译器自动生成的。你只写了第一行,编译器生成了大约 60 行。更重要的是,这些生成的方法基于字段的定义自动更新——如果你添加一个新字段 MiddleName,Equals、GetHashCode、ToString、Deconstruct 全都自动更新。
对于 record struct,生成的代码类似但不产生 Clone$ 方法(结构体是值类型,按值复制)。
with 表达式的 Lowering
var olderPerson = person with { Age = 31 };
编译为:
Person olderPerson = person.<Clone$>();
olderPerson.Age = 31;
<Clone$> 调用基类的 MemberwiseClone(一个原生方法,按位复制所有字段)。然后 init 属性设置器被调用——这个设置器只能在构造时或在 with 表达式中调用。编译器验证这一点:init setter 在 IL 中标记为 initonly,在反射和 Roslyn 分析阶段都被特殊对待。
对于 record struct,with 表达式的实现不同——直接复制值、修改字段、返回新实例。不需要克隆。
集合表达式的 Lowering(C# 12)
C# 12 引入了 [1, 2, 3] 集合表达式语法。Roslyn 的 Lowering 根据目标类型选择策略:
// 目标为数组
int[] arr = [1, 2, 3];
// 生成:new int[] { 1, 2, 3 }
// 目标为 Span<int>
Span<int> span = [1, 2, 3];
// 生成:用 stackalloc 或者 new int[] 然后转 Span
// 目标为 List<int>
List<int> list = [1, 2, 3];
// 生成:使用 CollectionBuilder 特性或展开
// var tmp = new List<int>();
// tmp.Add(1); tmp.Add(2); tmp.Add(3);
// 目标为自定义集合类型
// 如果类型有 [CollectionBuilder(typeof(MyBuilder), "Create")] 特性
// 生成:MyBuilder.Create(ReadOnlySpan<int>.From([1, 2, 3]))
// 分散为集合表达式(spread)
int[] more = [..arr, 4, 5, 6];
// 生成:创建一个更大的数组,复制 arr 的内容,再添加 4, 5, 6
// 或使用 CollectionBuilder 的 CreateRange 方法
集合表达式的多目标派发是 Roslyn 编译期重载解析的一个应用——编译器根据目标类型和 CollectionBuilderAttribute 选择最佳的生成路径。
Lambda 和闭包的 Lowering
var list = new List<int> { 1, 2, 3 };
int threshold = 10;
var filtered = list.Where(x => x > threshold);
x => x > threshold 这个 lambda 捕获了外部变量 threshold。Roslyn 的 Lowering:
// 编译器生成一个闭包类
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0 {
public int threshold;
internal bool <Main>g__Lambda_0(int x) {
return x > threshold;
}
}
// 原始代码被替换为:
<>c__DisplayClass0_0 closure = new <>c__DisplayClass0_0();
closure.threshold = 10;
var filtered = list.Where(new Func<int, bool>(closure.<Main>g__Lambda_0));
如果 lambda 不捕获任何变量,编译器生成一个静态缓存的委托实例,避免每次分配——这是 C# 9+ 的隐式静态 lambda 优化:
list.Where(static x => x > 0); // 显式静态
list.Where(x => x > 0); // 编译器自动判断不捕获
对于不捕获的 lambda,编译器在类型初始化器中缓存委托实例——Func<int, bool> cachedDelegate。每次调用时复用同一个委托对象,零分配。
2.4 第四层:IL Emit
Lowering 完成后,Roslyn 得到一棵纯粹的 BoundTree——其中所有 C# 特有的高阶构造都被消除,只剩下与 IL 一一对应的基本操作:call、brfalse、stloc、ldloc、newobj、ldfld、stfld。
IL 发射器
IL 发射由 ILEmit 类(在 Microsoft.CodeAnalysis.CSharp.Emit 中)完成。它遍历 BoundTree,对每个节点调用对应的 IL 生成方法:
BoundCall→call或callvirt指令BoundLocal→ldloc/stlocBoundObjectCreationExpression→newobj+ 构造函数参数BoundBinaryOperator→add、sub、ceq等算术指令BoundReturnStatement→retBoundBranch→br/brfalse/brtrue
Roslyn 的 IL 发射器会做局部指令选择优化。例如,加载常量时有多种方式:
ldc.i4.0 → 1 字节(无参数,压入 0)
ldc.i4.1 → 1 字节(压入 1)
ldc.i4.s → 2 字节(压入 -128 ~ 127 的任意值)
ldc.i4 → 5 字节(压入任意 32 位整数)
Roslyn 会选择最短的编码。类似地,加载局部变量:
ldloc.0 → 1 字节(加载索引 0 的变量)
ldloc.1 → 1 字节(加载索引 1 的变量)
ldloc.s → 2 字节(加载索引 0~255 的变量)
ldloc → 4 字节(加载任意索引的变量)
元数据表的生成
IL 只是编译产物的一个部分。Roslyn 同时生成:
- 元数据表:类型定义表(TypeDef)、方法定义表(MethodDef)、字段定义表(FieldDef)、成员引用表(MemberRef)、泛型参数表(GenericParam)等
- 程序集清单:程序集名称、版本、公钥标记、引用的程序集列表
- 资源:嵌入的 .resources 文件
- PDB(Program Database):调试信息——将 IL 指令映射回源代码的行号
对于 List<int>,元数据表中有:
- TypeDef 条目:
List<T>的元数据标记 - TypeSpec 条目:
List<int>(泛型实例化) - MethodDef 条目:
.ctor、.Add等 - MemberRef 条目:对
System.Int32的引用
PDB 调试信息的生成
Roslyn 生成可移植 PDB(Portable PDB)格式。它记录了每个 IL 偏移量对应的源代码行号范围:
IL_0000-IL_0014 → ListTest.cs 第 10 行(var list = ...)
IL_0015-IL_0028 → ListTest.cs 第 11 行(list.Add(1))
这也是为什么调试器能精确知道当前执行到哪行。PDB 还记录了局部变量的名称和作用域——所以你在调试时可以看到变量名和值。
三、Source Generators:编译时代的代码注入
C# 9 引入的 Source Generator 在编译期间作为"插件"运行。它不是 Lowering(编译器内置),而是外部代码在编译时介入。
工作方式
Source Generator 必须实现 ISourceGenerator:
[Generator]
public class MyGenerator : ISourceGenerator {
public void Initialize(GeneratorInitializationContext context) {
context.RegisterForSyntaxNotifications(() => new MyReceiver());
}
public void Execute(GeneratorExecutionContext context) {
// context.Compilation → 当前编译单元
// context.AddSource("GeneratedFile.cs", sourceText);
// 可以访问 SyntaxTree、SemanticModel
}
}
与 Lowering 的不同
- Lowering 是编译器内置的,不可扩展
- Source Generator 是用户代码,在编译时运行,可以注入任意源代码
- Source Generator 生成的代码和其他手动编写的代码一起被编译和 Lowering
典型应用
- System.Text.Json 源生成器:将 JSON 序列化器从运行时反射替换为编译期生成的序列化代码。性能提升 40-50%,零运行时元数据依赖
- MediatR 源生成器:自动注册请求处理器,替代反射扫描程序集的操作
- Regex 源生成器:
[GeneratedRegex(@"\d+")]在编译期生成优化过的正则表达式匹配代码,运行时零启动开销
对数据结构开发者的意义
如果你在创造自定义集合类型,Source Generator 可以自动生成:
IEquatable<T>实现(如 record 但更灵活)- 自定义序列化/反序列化代码
- LINQ 操作符的集合专有重载
[CollectionBuilder]特性的配套方法
四、追踪一条完整路径:从 var x = new List<int>() 到 IL
让我们追踪这一条代码如何穿过四层流水线。
语法树阶段:代码被解析为 LocalDeclarationStatementSyntax → VariableDeclarationSyntax → ObjectCreationExpressionSyntax → GenericNameSyntax("List<int>")。
语义绑定阶段:Binder 查找 List<int>,找到 System.Collections.Generic.List<T> 并将 T 绑定到 int。确定 x 的类型是 List<int>。匹配构造函数 List<T>()。
BoundTree/Lowering 阶段:这个简单的表达式没有需要 Lowering 的语法糖。它被转化为 BoundLocalDeclaration → BoundObjectCreationExpression。但请注意:如果这是一条 var x = new List<int> { 1, 2, 3 }(带初始化器),Lowering 会把它展开为 var tmp = new List<int>(); tmp.Add(1); tmp.Add(2); tmp.Add(3); var x = tmp;。
IL Emit 阶段:生成类似以下的 IL:
// 方法 Prolog
.maxstack 2
.locals init (class [System.Runtime]System.Collections.Generic.List`1<int32> V_0)
// newobj 创建 List<int> 实例并调用构造函数
IL_0000: newobj instance void class [System.Runtime]System.Collections.Generic.List`1<int32>::.ctor()
// stloc.0 将引用存入局部变量 0
IL_0005: stloc.0
// 方法 Epilog - ret
IL_0006: ret
最终输出的是 .dll 文件,包含 IL 字节码、元数据表、程序集清单。
五、Roslyn 源码地图
在 dotnet/roslyn 仓库中,关键路径如下:
src/Compilers/CSharp/Portable/
├── Syntax/
│ ├── SyntaxParser.cs ← 手写的递归下降解析器
│ ├── InternalSyntax/ ← Green Tree 定义
│ └── SyntaxTree.cs ← Red Tree 包装
├── Binding/
│ ├── Binder.cs ← 绑定核心
│ ├── OverloadResolution.cs ← 重载解析
│ └── TypeInferrer.cs ← 类型推断
├── Lowering/
│ ├── LocalRewriter.cs ← Lowering 总入口
│ ├── ForEachRewriter.cs ← foreach 展开
│ ├── AsyncRewriter.cs ← async 状态机生成
│ ├── LockRewriter.cs ← lock 展开
│ └── WithExpressionRewriter.cs
│ └── CollectionExpressionRewriter.cs
└── Emit/
├── ILEmit.cs ← IL 字节码发射
├── MetadataWriter.cs ← 元数据表写入
└── PDBWriter.cs ← 调试符号写入
六、总结
Roslyn 的四层流水线不是设计上的奢侈——它是 C# 语言复杂度的必然产物。
- SyntaxTree 让你看到代码的"骨架"——词法和语法分析的结果,完全保留文本信息
- SemanticModel 给你的代码注入"含义"——类型绑定、重载解析、类型推断
- BoundTree + Lowering 是编译器的"魔法车间"——
foreach变成while,using变成try-finally,async变成状态机,record的一行变成六十行 - IL Emit 把打扫干净的 BoundTree 变成字节码和元数据
理解这些层,你就能精确地知道编译器对你的代码做了什么。当你看到 record 产生的 IL 字节码比你手写的快,这不是什么黑魔法——那是编译器替你做了你本来应该做但可能做得不够好的事情。

329

被折叠的 条评论
为什么被折叠?



