Skip to content

最佳实践

源生成器开发的最佳实践和性能优化建议

📚 本文档内容

本文档提供源生成器开发的最佳实践,包括:

  • 性能优化建议
  • 错误处理策略
  • 项目配置建议
  • 代码组织建议
  • 测试建议
  • 性能监控

📋 文档信息

项目信息
文档标题最佳实践
所属系列学习指南
难度级别⭐⭐⭐⭐ 高级
预计阅读时间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. 避免在管道中传递大对象

为什么:

  • SyntaxNodeISymbol 很大
  • 传递它们会破坏缓存
  • 增加内存压力

对比:

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.cs

2. 命名约定

  • 生成器类: {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 毫秒

💡 关键要点

  1. 优先使用增量生成器

    • 性能远超传统生成器
    • 支持 IDE 实时反馈
  2. 优化性能

    • 使用两阶段过滤
    • 避免传递大对象
    • 使用静态方法
  3. 正确处理错误

    • 使用诊断而不是异常
    • 提供清晰的错误消息
  4. 配置项目

    • 使用 netstandard2.0
    • 正确配置 NuGet 包
  5. 组织代码

    • 清晰的文件结构
    • 一致的命名约定
  6. 编写测试

    • 覆盖正常和错误情况
    • 测试边缘情况

🔗 相关文档


📚 下一步

学习完最佳实践后,建议继续学习:

  1. 故障排除 - 解决常见问题

基于 MIT 许可发布