Skip to content

源生成器原理

📋 文档信息

难度: 🟢 简单
预计阅读时间: 20 分钟
前置知识:

  • C# 基础语法
  • 基本的编译原理概念

适合人群:

  • 想要了解源生成器工作原理的开发者
  • 需要理解 Roslyn 编译器平台的开发者
  • 准备开发源生成器的初学者

📋 快速导航

章节难度阅读时间链接
Roslyn 编译器平台🟢5 分钟查看
语法树和语义模型🟢10 分钟查看
编译管道🟡5 分钟查看

🎯 概览

本文档介绍 C# 源生成器的核心原理,包括 Roslyn 编译器平台、语法树、语义模型和编译管道。理解这些基础概念对于开发高效的源生成器至关重要。

本文档涵盖:

  • Roslyn 编译器平台的架构和组件
  • 语法树和语义模型的概念和区别
  • 编译管道流程和源生成器的执行时机
  • 增量生成器的管道模型和缓存机制

典型应用场景:

  • 理解源生成器如何与编译器交互
  • 选择合适的 API 层次进行开发
  • 优化生成器性能

Roslyn 编译器平台

什么是 Roslyn?

Roslyn 是 .NET 编译器平台的代号,它是 C# 和 Visual Basic 的开源编译器。与传统的"黑盒"编译器不同,Roslyn 将编译器作为一个平台开放,提供了丰富的 API 供开发者使用。

Roslyn 的核心组件

┌─────────────────────────────────────────────────────────────┐
│                    Roslyn 编译器平台                          │
├─────────────────────────────────────────────────────────────┤
│  1. 语法分析器 (Parser)                                       │
│     输入: 源代码文本                                          │
│     输出: 语法树 (SyntaxTree)                                 │
│     功能: 将文本转换为结构化的语法树                           │
├─────────────────────────────────────────────────────────────┤
│  2. 语义分析器 (Semantic Analyzer)                            │
│     输入: 语法树 + 引用                                       │
│     输出: 语义模型 (SemanticModel)                            │
│     功能: 类型检查、符号解析、绑定                             │
├─────────────────────────────────────────────────────────────┤
│  3. 代码生成器 (Code Generator)                               │
│     输入: 语义模型                                            │
│     输出: IL 代码                                             │
│     功能: 生成中间语言代码                                     │
├─────────────────────────────────────────────────────────────┤
│  4. 源生成器 API (Source Generator API)                       │
│     位置: 在语义分析之后,代码生成之前                         │
│     功能: 分析代码并生成新的源文件                             │
└─────────────────────────────────────────────────────────────┘

Roslyn 的三层 API 架构

  1. 编译器 API (Compiler APIs)

    • 最底层的 API
    • 提供完整的编译功能
    • 用于构建编译器、分析器和源生成器
  2. 工作区 API (Workspace APIs)

    • 中间层 API
    • 管理项目、解决方案和文档
    • 用于构建 IDE 功能和重构工具
  3. 脚本 API (Scripting APIs)

    • 最高层 API
    • 提供动态代码执行
    • 用于 REPL 和脚本场景

源生成器使用的是编译器 API 层,主要涉及:

  • Microsoft.CodeAnalysis - 核心抽象
  • Microsoft.CodeAnalysis.CSharp - C# 特定实现

语法树和语义模型

语法树 (Syntax Tree)

定义

语法树是源代码的结构化表示,它将代码文本转换为树形结构,每个节点代表代码的一个语法元素。

语法树的特点

  • 完整性: 包含所有源代码信息(包括空格、注释)
  • 不可变性: 一旦创建就不能修改
  • 红绿树结构: 使用红绿树优化内存和性能

语法树的三种节点类型

  1. SyntaxNode(语法节点)

    • 表示声明、语句、表达式等
    • 例如:ClassDeclarationSyntax, MethodDeclarationSyntax
    • 可以有子节点
  2. SyntaxToken(语法标记)

    • 表示关键字、标识符、运算符等
    • 例如:public, class, {, }
    • 叶子节点,没有子节点
  3. SyntaxTrivia(语法琐碎内容)

    • 表示空格、注释、预处理指令等
    • 附加在 Token 上
    • 不影响程序语义

示例代码的语法树

csharp
public class Person
{
    public string Name { get; set; }
}

对应的语法树结构:

CompilationUnit
└── ClassDeclaration (Person)
    ├── Modifiers: [public]
    ├── Identifier: "Person"
    └── Members
        └── PropertyDeclaration (Name)
            ├── Modifiers: [public]
            ├── Type: string
            ├── Identifier: "Name"
            └── AccessorList
                ├── GetAccessor
                └── SetAccessor

遍历语法树的方法

csharp
// 方法 1: 使用 DescendantNodes() 遍历所有节点
var classes = syntaxTree.GetRoot()
    .DescendantNodes()
    .OfType<ClassDeclarationSyntax>();

// 方法 2: 使用 Visitor 模式
public class MyVisitor : CSharpSyntaxWalker
{
    public override void VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        // 处理类声明
        base.VisitClassDeclaration(node);
    }
}

// 方法 3: 使用 Rewriter 模式(修改语法树)
public class MyRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        // 修改并返回新节点
        return base.VisitClassDeclaration(node);
    }
}

语义模型 (Semantic Model)

定义

语义模型提供代码的语义信息,包括类型信息、符号解析、类型检查等。它建立在语法树之上,提供更深层次的代码理解。

语义模型的核心概念

  1. 符号 (ISymbol)

    • 表示代码中的命名实体
    • 类型:INamedTypeSymbol, IMethodSymbol, IPropertySymbol, IFieldSymbol
    • 提供类型信息、可访问性、修饰符等
  2. 类型 (ITypeSymbol)

    • 表示类型信息
    • 包括:类、接口、结构、枚举、委托等
    • 提供继承关系、成员信息等
  3. 绑定 (Binding)

    • 将语法节点与符号关联
    • 解析名称引用
    • 类型推断

语义模型的主要方法

csharp
// 获取符号信息
ISymbol symbol = semanticModel.GetDeclaredSymbol(syntaxNode);
ITypeSymbol typeSymbol = semanticModel.GetTypeInfo(expression).Type;

// 获取符号的完整名称
string fullName = symbol.ToDisplayString();

// 检查类型关系
bool isAssignable = compilation.ClassifyConversion(sourceType, targetType).IsImplicit;

// 查找所有引用
var references = await SymbolFinder.FindReferencesAsync(symbol, solution);

语法树 vs 语义模型对比

特性语法树语义模型
信息类型结构信息语义信息
性能快速较慢
依赖仅源代码需要引用和编译
用途语法分析、格式化类型检查、重构
示例找到所有类声明找到实现某接口的类

在源生成器中的使用建议

  1. 优先使用语法树过滤

    csharp
    // ✅ 好:先用语法快速过滤
    predicate: static (node, _) => 
        node is ClassDeclarationSyntax c && 
        c.AttributeLists.Count > 0
  2. 仅在必要时使用语义模型

    csharp
    // ✅ 好:只对候选节点使用语义分析
    transform: static (ctx, _) => {
        var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node);
        return symbol?.GetAttributes().Any(a => 
            a.AttributeClass?.Name == "MyAttribute") == true 
            ? symbol : null;
    }
  3. 避免过度使用语义模型

    csharp
    // ❌ 坏:对所有节点都使用语义分析
    predicate: static (node, _) => true,
    transform: static (ctx, _) => {
        var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node);
        // 这会非常慢!
    }

编译管道

完整的编译流程

┌─────────────────────────────────────────────────────────────┐
│                      编译管道流程                             │
└─────────────────────────────────────────────────────────────┘

1. 源代码文件 (.cs)

2. 词法分析 (Lexical Analysis)
   输出: Token 流

3. 语法分析 (Syntax Analysis)
   输出: 语法树 (SyntaxTree)

4. 声明分析 (Declaration Analysis)
   输出: 符号表 (Symbol Table)

5. 绑定 (Binding)
   输出: 绑定树 (Bound Tree)

6. ⭐ 源生成器执行点 ⭐
   输入: 编译信息 (Compilation)
   输出: 新的源文件

7. 重新编译新生成的源文件
   (回到步骤 2-5)

8. IL 代码生成 (IL Emit)
   输出: 中间语言代码

9. 元数据生成 (Metadata Emit)
   输出: 程序集 (.dll 或 .exe)

源生成器的执行时机

源生成器在编译管道中有两个关键执行点:

  1. 初始化阶段 (Initialization)

    • 时机:编译器加载生成器时
    • 调用:Initialize(GeneratorInitializationContext context)
    • 用途:注册回调、设置管道
  2. 生成阶段 (Generation)

    • 时机:所有源文件解析完成后
    • 调用:Execute(GeneratorExecutionContext context) 或增量生成器的管道
    • 用途:分析代码、生成新源文件

增量生成器的管道模型

┌─────────────────────────────────────────────────────────────┐
│                  增量生成器管道                               │
└─────────────────────────────────────────────────────────────┘

输入源 (Input Source)

语法提供者 (SyntaxProvider)
   ├── CreateSyntaxProvider
   │   ├── Predicate (语法过滤) ← 快速,无语义信息
   │   └── Transform (语义转换) ← 较慢,有语义信息

   └── ForAttributeWithMetadataName (.NET 7+)
       └── 内置优化的特性查找

转换操作 (Transformations)
   ├── Select (映射)
   ├── Where (过滤)
   ├── Combine (组合)
   └── Collect (收集)

输出注册 (Output Registration)
   └── RegisterSourceOutput
       └── 生成代码

管道的缓存机制

增量生成器使用智能缓存来提高性能:

  1. 输入缓存

    • 如果输入没有变化,跳过整个管道
    • 使用 IEquatable<T> 比较输入
  2. 中间结果缓存

    • 管道的每个步骤都可以缓存
    • 只重新计算受影响的部分
  3. 输出缓存

    • 如果输出没有变化,不重新写入文件
    • 减少 IDE 刷新

示例:增量生成器的执行流程

csharp
[Generator]
public class MyIncrementalGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 步骤 1: 创建输入管道
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                // 阶段 A: 语法过滤(快速)
                predicate: static (node, _) => 
                    node is ClassDeclarationSyntax c && 
                    c.AttributeLists.Count > 0,
                
                // 阶段 B: 语义转换(较慢)
                transform: static (ctx, _) => 
                    GetClassInfo(ctx))
            
            // 步骤 2: 过滤空结果
            .Where(static m => m is not null);
        
        // 步骤 3: 注册输出
        context.RegisterSourceOutput(classDeclarations,
            static (spc, classInfo) => GenerateCode(classInfo, spc));
    }
}

性能优化原理

场景:修改一个文件中的一个类

传统生成器 (ISourceGenerator):
  ✗ 重新分析所有文件
  ✗ 重新生成所有代码
  时间: O(n) - n 是文件总数

增量生成器 (IIncrementalGenerator):
  ✓ 只分析修改的文件
  ✓ 只重新生成受影响的代码
  ✓ 其他结果从缓存读取
  时间: O(1) - 常数时间

编译器与生成器的交互

编译器                          源生成器
  │                                │
  ├─ 加载生成器 ──────────────────→│
  │                                │
  ├─ 调用 Initialize() ───────────→│
  │                                ├─ 注册回调
  │                                │
  ├─ 解析所有源文件                 │
  │                                │
  ├─ 构建 Compilation ─────────────→│
  │                                │
  ├─ 调用生成器 ───────────────────→│
  │                                ├─ 分析代码
  │                                ├─ 生成新源文件
  │                                │
  │←─ 返回生成的源文件 ─────────────┤
  │                                │
  ├─ 将新源文件加入编译              │
  │                                │
  ├─ 继续编译流程                   │
  │                                │
  └─ 输出程序集                     │

💡 关键要点

  1. Roslyn 是开放的编译器平台

    • 提供三层 API:编译器 API、工作区 API、脚本 API
    • 源生成器使用编译器 API 层
  2. 语法树提供结构信息

    • 快速、轻量级
    • 适合初步过滤和语法分析
    • 包含所有源代码信息(包括注释和空格)
  3. 语义模型提供语义信息

    • 较慢、需要完整编译上下文
    • 适合类型检查和符号解析
    • 应该在语法过滤后使用
  4. 增量生成器使用管道模型

    • 两阶段过滤:语法过滤 + 语义转换
    • 智能缓存机制
    • 性能提升 10-100 倍
  5. 源生成器在编译管道中执行

    • 在语义分析之后、IL 生成之前
    • 生成的代码会被重新编译
    • 不能修改现有代码,只能添加新代码

🔗 相关文档


📚 下一步

学习完源生成器原理后,建议继续学习:

  1. Roslyn API 介绍 - 了解核心 API 和生成器接口
  2. 语法树 API - 深入学习语法树操作
  3. 语义模型 API - 掌握语义分析技巧

基于 MIT 许可发布