最佳实践
源生成器开发的最佳实践和性能优化建议
📚 本文档内容
本文档提供源生成器开发的最佳实践,包括:
- 性能优化建议
- 错误处理策略
- 项目配置建议
- 代码组织建议
- 测试建议
- 性能监控
📋 文档信息
| 项目 | 信息 |
|---|---|
| 文档标题 | 最佳实践 |
| 所属系列 | 学习指南 |
| 难度级别 | ⭐⭐⭐⭐ 高级 |
| 预计阅读时间 | 50 分钟 |
| 前置知识 | 常见模式 |
| 相关文档 | 故障排除 |
🎯 学习目标
学完本文档后,你将能够:
- ✅ 优化生成器性能
- ✅ 实现正确的错误处理
- ✅ 配置项目和 NuGet 包
- ✅ 组织代码结构
- ✅ 编写高质量测试
- ✅ 监控性能指标
📑 快速导航
| 章节 | 内容概要 | 跳转链接 |
|---|---|---|
| 性能优化 | 提升生成器性能的技巧 | 查看 |
| 错误处理 | 正确的错误处理策略 | 查看 |
| 项目配置 | 项目和 NuGet 配置 | 查看 |
| 代码组织 | 代码结构和命名约定 | 查看 |
| 测试建议 | 测试覆盖和组织 | 查看 |
| 性能监控 | 性能测量和分析 | 查看 |
概览
本文档提供源生成器开发的最佳实践,帮助你编写高性能、可维护的生成器代码。
性能优化建议
1. 使用增量生成器 (IIncrementalGenerator)
为什么:
- 比传统生成器快 10-100 倍
- 支持 IDE 实时反馈
- 自动缓存和增量更新
- 只重新生成受影响的代码
对比:
csharp
// ❌ 不推荐:传统生成器
[Generator]
public class OldGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// 每次都重新分析所有文件
foreach (var tree in context.Compilation.SyntaxTrees)
{
// 处理所有文件...
}
}
}
// ✅ 推荐:增量生成器
[Generator]
public class NewGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 使用管道,自动缓存
var provider = context.SyntaxProvider.CreateSyntaxProvider(/*...*/);
context.RegisterSourceOutput(provider, /*...*/);
}
}2. 使用 ForAttributeWithMetadataName (.NET 7+)
为什么:
- 专门为基于特性的生成器优化
- 内置缓存和索引
- 比
CreateSyntaxProvider更高效
3. 避免在管道中传递大对象
为什么:
SyntaxNode和ISymbol很大- 传递它们会破坏缓存
- 增加内存压力
对比:
csharp
// ❌ 不好:传递 SyntaxNode
var provider = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => ctx.Node); // 传递整个节点
// ✅ 好:只提取需要的信息
var provider = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => {
var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol;
if (symbol == null) return null;
// 只提取必要的信息
return new ClassInfo(
symbol.Name,
symbol.ContainingNamespace.ToDisplayString());
});4. 使用值类型或实现 IEquatable
为什么:
- 增量生成器使用相等性比较来判断是否需要重新生成
record类型自动实现IEquatable
对比:
csharp
// ❌ 不好:使用 class 且不实现 IEquatable
public class ClassInfo
{
public string Name { get; set; }
// 默认使用引用相等性,总是不相等
}
// ✅ 好:使用 record(自动实现 IEquatable)
public record ClassInfo(string Name, string Namespace);5. 使用静态方法和 lambda
为什么:
- 避免闭包捕获
- 减少内存分配
- 提高性能
6. 最小化语义模型访问
为什么:
- 语义分析比语法分析慢 10-100 倍
- 先用语法快速过滤可以大幅提升性能
7. 使用 StringBuilder 生成代码
为什么:
- 字符串拼接会创建大量临时对象
StringBuilder更高效
8. 批量生成而非逐个生成
为什么:
- 减少
AddSource调用次数 - 减少文件 I/O
错误处理策略
1. 使用诊断而不是异常
为什么:
- 异常会中断编译
- 诊断可以提供更好的用户体验
对比:
csharp
// ❌ 不好:抛出异常
if (!IsValid(classInfo))
{
throw new InvalidOperationException("Invalid class");
}
// ✅ 好:报告诊断
if (!IsValid(classInfo))
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.InvalidClass,
classInfo.Location,
classInfo.Name);
context.ReportDiagnostic(diagnostic);
return; // 不生成代码
}2. 提供清晰的错误消息
原则:
- 说明问题是什么
- 说明如何修复
- 包含相关的上下文信息
3. 使用合适的严重级别
指南:
- Error: 阻止代码生成的问题
- Warning: 可能的问题但不阻止生成
- Info: 提示信息
- Hidden: IDE 内部使用
4. 防御性编程
原则:
- 检查 null 值
- 验证假设
- 优雅地处理边缘情况
项目配置建议
1. 生成器项目配置
必需的配置:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- 目标框架:netstandard2.0 以获得最大兼容性 -->
<TargetFramework>netstandard2.0</TargetFramework>
<!-- 语言版本:使用最新特性 -->
<LangVersion>latest</LangVersion>
<!-- 不生成引用程序集 -->
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<!-- 启用可空引用类型 -->
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Roslyn 包 -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>2. 使用者项目配置
引用生成器:
xml
<ItemGroup>
<!-- 作为分析器引用,不作为程序集引用 -->
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>3. NuGet 包配置
打包生成器:
xml
<PropertyGroup>
<!-- 包信息 -->
<PackageId>MyGenerator</PackageId>
<Version>1.0.0</Version>
<!-- 开发依赖 -->
<DevelopmentDependency>true</DevelopmentDependency>
<!-- 不包含 lib 文件夹 -->
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<!-- 将生成器 DLL 打包到 analyzers 文件夹 -->
<None Include="$(OutputPath)\$(AssemblyName).dll"
Pack="true"
PackagePath="analyzers/dotnet/cs"
Visible="false" />
</ItemGroup>代码组织建议
1. 文件结构
MyGenerator/
├── MyGenerator.csproj
├── MyGenerator.cs # 主生成器类
├── Models/
│ ├── ClassInfo.cs # 数据模型
│ └── PropertyInfo.cs
├── Diagnostics/
│ └── DiagnosticDescriptors.cs
├── Templates/
│ └── AttributeTemplate.cs # 特性模板
└── Helpers/
└── CodeGenerationHelpers.cs2. 命名约定
- 生成器类:
{Feature}Generator - 数据模型:
{Entity}Info - 生成的文件:
{ClassName}.{Feature}.g.cs - 诊断 ID:
{PREFIX}####(如MYGEN0001)
测试建议
1. 测试覆盖
必须测试:
- ✅ 正常情况:生成正确的代码
- ✅ 错误情况:报告正确的诊断
- ✅ 边缘情况:空类、无属性等
- ✅ 编译测试:生成的代码可以编译
2. 测试组织
MyGenerator.Tests/
├── MyGenerator.Tests.csproj
├── GeneratorTests.cs # 主要测试
├── DiagnosticTests.cs # 诊断测试
└── EdgeCaseTests.cs # 边缘情况性能监控
1. 测量生成器性能
csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// 生成器逻辑...
stopwatch.Stop();
// 在开发时记录性能数据
}2. 性能目标
- 初次编译: < 1 秒(对于中等项目)
- 增量编译: < 100 毫秒
- IDE 响应: < 50 毫秒
💡 关键要点
优先使用增量生成器
- 性能远超传统生成器
- 支持 IDE 实时反馈
优化性能
- 使用两阶段过滤
- 避免传递大对象
- 使用静态方法
正确处理错误
- 使用诊断而不是异常
- 提供清晰的错误消息
配置项目
- 使用 netstandard2.0
- 正确配置 NuGet 包
组织代码
- 清晰的文件结构
- 一致的命名约定
编写测试
- 覆盖正常和错误情况
- 测试边缘情况
🔗 相关文档
📚 下一步
学习完最佳实践后,建议继续学习:
- 故障排除 - 解决常见问题