01-09-运行时-Roslyn深度-从语法树到IL的编译魔法

Roslyn 深度:从语法树到 IL 的编译魔法

系列:C#与常用数据结构源码剖析 · 运行时底层剖析
阅读时间:约 45 分钟
前置知识:C# 语法、IL 基础


一、引言

你写下 var list = new List<int>() { 1, 2, 3 },Roslyn 编译器把它变成 IL。但中间发生了什么?编译器不是黑盒——它有一套精密的四层流水线,每一层都在做不同的事情。

理解 Roslyn 的编译过程,你就能精确知道你的 C# 代码在编译时被做了什么。哪些语法糖被展开了,哪些代码被优化掉了,哪些额外的代码被自动生成了。你看到的 foreach,编译后可能变成了 while 加枚举器。你写的 record,编译器悄悄给你加了一整套 EqualsGetHashCodeToStringDeconstruct

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>();

在上面的代码中:

  • var token 的前导 Trivia:换行 + // 这是一条注释
  • new token 的前导 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>() 时的步骤:

  1. 看到 var,根据上下文判断它是一个隐式类型声明——list 的类型由右侧初始化表达式推断
  2. 看到 new List<int>(),查找名为 List 的类型——在 System.Collections.Generic 命名空间中找到 List<T>,或用 using 指令
  3. 看到 int 类型参数,绑定到 System.Int32
  4. 查找 List<T> 的构造函数——找到 public List()(无参构造函数)
  5. 看到初始化器 { 1, 2, 3 },验证是否存在 Add(int) 方法——找到 List<T>.Add(T) 方法
  6. 最终确定 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 类中。它的核心逻辑:

  1. 收集所有候选方法——参数数量匹配、名称匹配、可访问
  2. 从候选中排除不可用的——泛型参数推断失败、约束不满足
  3. 应用"更好性"规则——参数类型更具体的胜出,非泛型优于泛型
  4. 如果有且只有一个最优候选,选它;否则报歧义错误

对于 List<int> 的初始化器,重载解析找到 Add(T) 方法,因为 int 与 T 匹配(当 T = int 时)。如果集合有多个重载的 Add 方法,相同算法适用。

类型推断(Type Inference)

var x = ... 的本质是类型推断。Roslyn 的推断算法比大多数人想象的复杂得多——它需要处理泛型类型参数之间的约束关系。

类型推断分三个阶段:

  1. 第一遍:从参数到参数类型的推断(输入推断)
  2. 第二遍:从参数类型到其他类型参数的推断(输出推断)
  3. 第三遍:修正阶段——锁定所有自由的类型参数

对于一个调用如 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 开头:BoundExpressionStatementBoundCallBoundLocalBoundObjectCreationExpression。它们比语法节点"更低级"——语法中的 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 Currentforeach 直接调用它们而不检查 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 行。更重要的是,这些生成的方法基于字段的定义自动更新——如果你添加一个新字段 MiddleNameEqualsGetHashCodeToStringDeconstruct 全都自动更新。

对于 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 structwith 表达式的实现不同——直接复制值、修改字段、返回新实例。不需要克隆。

集合表达式的 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 一一对应的基本操作:callbrfalsestlocldlocnewobjldfldstfld

IL 发射器

IL 发射由 ILEmit 类(在 Microsoft.CodeAnalysis.CSharp.Emit 中)完成。它遍历 BoundTree,对每个节点调用对应的 IL 生成方法:

  • BoundCall → call 或 callvirt 指令
  • BoundLocal → ldloc / stloc
  • BoundObjectCreationExpression → newobj + 构造函数参数
  • BoundBinaryOperator → addsubceq 等算术指令
  • BoundReturnStatement → ret
  • BoundBranch → 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# 语言复杂度的必然产物。

  1. SyntaxTree 让你看到代码的"骨架"——词法和语法分析的结果,完全保留文本信息
  2. SemanticModel 给你的代码注入"含义"——类型绑定、重载解析、类型推断
  3. BoundTree + Lowering 是编译器的"魔法车间"——foreach 变成 whileusing 变成 try-finallyasync 变成状态机,record 的一行变成六十行
  4. IL Emit 把打扫干净的 BoundTree 变成字节码和元数据

理解这些层,你就能精确地知道编译器对你的代码做了什么。当你看到 record 产生的 IL 字节码比你手写的快,这不是什么黑魔法——那是编译器替你做了你本来应该做但可能做得不够好的事情。


下一篇AOT编译 vs JIT:泛型、反射、代码生成的命运分野

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

淡海水

感谢支持 共同进步 好运++

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值