源生成器原理
📋 文档信息
难度: 🟢 简单
预计阅读时间: 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 架构
编译器 API (Compiler APIs)
- 最底层的 API
- 提供完整的编译功能
- 用于构建编译器、分析器和源生成器
工作区 API (Workspace APIs)
- 中间层 API
- 管理项目、解决方案和文档
- 用于构建 IDE 功能和重构工具
脚本 API (Scripting APIs)
- 最高层 API
- 提供动态代码执行
- 用于 REPL 和脚本场景
源生成器使用的是编译器 API 层,主要涉及:
Microsoft.CodeAnalysis- 核心抽象Microsoft.CodeAnalysis.CSharp- C# 特定实现
语法树和语义模型
语法树 (Syntax Tree)
定义
语法树是源代码的结构化表示,它将代码文本转换为树形结构,每个节点代表代码的一个语法元素。
语法树的特点
- 完整性: 包含所有源代码信息(包括空格、注释)
- 不可变性: 一旦创建就不能修改
- 红绿树结构: 使用红绿树优化内存和性能
语法树的三种节点类型
SyntaxNode(语法节点)
- 表示声明、语句、表达式等
- 例如:
ClassDeclarationSyntax,MethodDeclarationSyntax - 可以有子节点
SyntaxToken(语法标记)
- 表示关键字、标识符、运算符等
- 例如:
public,class,{,} - 叶子节点,没有子节点
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)
定义
语义模型提供代码的语义信息,包括类型信息、符号解析、类型检查等。它建立在语法树之上,提供更深层次的代码理解。
语义模型的核心概念
符号 (ISymbol)
- 表示代码中的命名实体
- 类型:
INamedTypeSymbol,IMethodSymbol,IPropertySymbol,IFieldSymbol等 - 提供类型信息、可访问性、修饰符等
类型 (ITypeSymbol)
- 表示类型信息
- 包括:类、接口、结构、枚举、委托等
- 提供继承关系、成员信息等
绑定 (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 语义模型对比
| 特性 | 语法树 | 语义模型 |
|---|---|---|
| 信息类型 | 结构信息 | 语义信息 |
| 性能 | 快速 | 较慢 |
| 依赖 | 仅源代码 | 需要引用和编译 |
| 用途 | 语法分析、格式化 | 类型检查、重构 |
| 示例 | 找到所有类声明 | 找到实现某接口的类 |
在源生成器中的使用建议
优先使用语法树过滤
csharp// ✅ 好:先用语法快速过滤 predicate: static (node, _) => node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0仅在必要时使用语义模型
csharp// ✅ 好:只对候选节点使用语义分析 transform: static (ctx, _) => { var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node); return symbol?.GetAttributes().Any(a => a.AttributeClass?.Name == "MyAttribute") == true ? symbol : null; }避免过度使用语义模型
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)源生成器的执行时机
源生成器在编译管道中有两个关键执行点:
初始化阶段 (Initialization)
- 时机:编译器加载生成器时
- 调用:
Initialize(GeneratorInitializationContext context) - 用途:注册回调、设置管道
生成阶段 (Generation)
- 时机:所有源文件解析完成后
- 调用:
Execute(GeneratorExecutionContext context)或增量生成器的管道 - 用途:分析代码、生成新源文件
增量生成器的管道模型
┌─────────────────────────────────────────────────────────────┐
│ 增量生成器管道 │
└─────────────────────────────────────────────────────────────┘
输入源 (Input Source)
↓
语法提供者 (SyntaxProvider)
├── CreateSyntaxProvider
│ ├── Predicate (语法过滤) ← 快速,无语义信息
│ └── Transform (语义转换) ← 较慢,有语义信息
│
└── ForAttributeWithMetadataName (.NET 7+)
└── 内置优化的特性查找
↓
转换操作 (Transformations)
├── Select (映射)
├── Where (过滤)
├── Combine (组合)
└── Collect (收集)
↓
输出注册 (Output Registration)
└── RegisterSourceOutput
└── 生成代码管道的缓存机制
增量生成器使用智能缓存来提高性能:
输入缓存
- 如果输入没有变化,跳过整个管道
- 使用
IEquatable<T>比较输入
中间结果缓存
- 管道的每个步骤都可以缓存
- 只重新计算受影响的部分
输出缓存
- 如果输出没有变化,不重新写入文件
- 减少 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 ─────────────→│
│ │
├─ 调用生成器 ───────────────────→│
│ ├─ 分析代码
│ ├─ 生成新源文件
│ │
│←─ 返回生成的源文件 ─────────────┤
│ │
├─ 将新源文件加入编译 │
│ │
├─ 继续编译流程 │
│ │
└─ 输出程序集 │💡 关键要点
Roslyn 是开放的编译器平台
- 提供三层 API:编译器 API、工作区 API、脚本 API
- 源生成器使用编译器 API 层
语法树提供结构信息
- 快速、轻量级
- 适合初步过滤和语法分析
- 包含所有源代码信息(包括注释和空格)
语义模型提供语义信息
- 较慢、需要完整编译上下文
- 适合类型检查和符号解析
- 应该在语法过滤后使用
增量生成器使用管道模型
- 两阶段过滤:语法过滤 + 语义转换
- 智能缓存机制
- 性能提升 10-100 倍
源生成器在编译管道中执行
- 在语义分析之后、IL 生成之前
- 生成的代码会被重新编译
- 不能修改现有代码,只能添加新代码
🔗 相关文档
- Roslyn API 介绍 - 详细的 API 参考
- 语法树 API - 语法树操作详解
- 语义模型 API - 语义模型详解
- 最佳实践 - 性能优化建议
- 返回学习指南
📚 下一步
学习完源生成器原理后,建议继续学习:
- Roslyn API 介绍 - 了解核心 API 和生成器接口
- 语法树 API - 深入学习语法树操作
- 语义模型 API - 掌握语义分析技巧