Skip to content

最佳实践

增量生成器管道的最佳实践和反模式

📋 文档信息

属性
难度中级
阅读时间25 分钟
前置知识管道操作、缓存机制
相关文档性能优化

🎯 学习目标

完成本文档后,你将能够:

  • ✅ 遵循管道开发最佳实践
  • ✅ 避免常见的反模式
  • ✅ 编写高性能的管道代码
  • ✅ 编写可维护的管道代码

📚 快速导航

章节说明
推荐做法最佳实践
反模式应该避免的做法
常见错误常见错误和解决方案

推荐做法

✅ 做法 1: 使用 ForAttributeWithMetadataName

原因: 性能最优,自动过滤和缓存。

csharp
// ✅ 好的做法:使用 ForAttributeWithMetadataName
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        "MyAttribute",
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) => ctx.TargetSymbol);

// ❌ 不好的做法:使用 CreateSyntaxProvider + 手动检查特性
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) =>
        {
            var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node);
            if (!symbol.GetAttributes().Any(a => a.AttributeClass?.Name == "MyAttribute"))
                return null;
            return symbol;
        })
    .Where(s => s != null);

✅ 做法 2: 使用 record 类型

原因: 自动实现 IEquatable,支持缓存。

csharp
// ✅ 好的做法:使用 record
public record ClassData(string Name, ImmutableArray<string> Properties);

// ❌ 不好的做法:使用 class 但不实现 IEquatable
public class ClassData
{
    public string Name { get; set; }
    public List<string> Properties { get; set; }
}

✅ 做法 3: 使用 ImmutableArray

原因: 值相等性,支持缓存。

csharp
// ✅ 好的做法:使用 ImmutableArray
public record ClassData(
    string Name,
    ImmutableArray<string> Properties);

// ❌ 不好的做法:使用 List
public record ClassData(
    string Name,
    List<string> Properties);  // 引用相等性

✅ 做法 4: 提前过滤

原因: 减少不必要的计算。

csharp
// ✅ 好的做法:在 predicate 中过滤
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) =>
        {
            if (node is not ClassDeclarationSyntax classDecl)
                return false;
            
            // 提前过滤:只接受公共类
            return classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
        },
        transform: (ctx, _) => ctx.Node);

// ❌ 不好的做法:在 transform 中过滤
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) =>
        {
            var classDecl = (ClassDeclarationSyntax)ctx.Node;
            
            // 晚过滤:已经创建了语义模型
            if (!classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)))
                return null;
            
            return classDecl;
        })
    .Where(c => c != null);

✅ 做法 5: 缓存昂贵操作

原因: 避免重复计算。

csharp
// ✅ 好的做法:缓存昂贵操作
var globalConfig = context.CompilationProvider
    .Select((comp, _) => PerformExpensiveOperation())
    .WithTrackingName("GlobalConfig");

var classes = context.SyntaxProvider
    .CreateSyntaxProvider(...);

var combined = classes.Combine(globalConfig);

// ❌ 不好的做法:在 transform 中执行昂贵操作
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) =>
        {
            // 每个类都执行昂贵操作
            var result = PerformExpensiveOperation();
            return (ctx.Node, result);
        });

✅ 做法 6: 添加 WithTrackingName

原因: 便于性能分析和调试。

csharp
// ✅ 好的做法:添加跟踪名称
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(...)
    .WithTrackingName("FindClasses");

var filtered = classes
    .Where(...)
    .WithTrackingName("FilterClasses");

// ❌ 不好的做法:没有跟踪名称
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(...);

var filtered = classes.Where(...);

✅ 做法 7: 避免不必要的 Collect

原因: Collect 会等待所有值,影响性能。

csharp
// ✅ 好的做法:直接处理每个类
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(...);

context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    // 为每个类生成代码
    var code = GenerateCode(classInfo);
    spc.AddSource($"{classInfo.Name}.g.cs", code);
});

// ❌ 不好的做法:不必要的 Collect
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(...);

var allClasses = classes.Collect();  // 等待所有类

context.RegisterSourceOutput(allClasses, (spc, classInfos) =>
{
    // 为每个类生成代码(但已经等待了所有类)
    foreach (var classInfo in classInfos)
    {
        var code = GenerateCode(classInfo);
        spc.AddSource($"{classInfo.Name}.g.cs", code);
    }
});

反模式

❌ 反模式 1: 在 transform 中执行昂贵操作

问题: 影响性能。

csharp
// ❌ 不好的做法
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) =>
        {
            // 昂贵的操作会在每次输入改变时执行
            Thread.Sleep(1000);
            return ctx.Node;
        });

正确做法: 将昂贵操作移到后续的 Select 中,并使用缓存。

csharp
// ✅ 好的做法
var globalData = context.CompilationProvider
    .Select((comp, _) =>
    {
        // 只执行一次
        Thread.Sleep(1000);
        return "result";
    });

var classes = context.SyntaxProvider
    .CreateSyntaxProvider(...);

var combined = classes.Combine(globalData);

❌ 反模式 2: 使用可变集合

问题: 缓存失效。

csharp
// ❌ 不好的做法:使用 List
public record ClassData(
    string Name,
    List<string> Properties);  // 可变集合

// 问题演示
var props = new List<string> { "A", "B" };
var data1 = new ClassData("User", props);
var data2 = new ClassData("User", props);

Console.WriteLine(data1 == data2);  // True(引用相同)

props.Add("C");  // 修改 List

// 问题:GetHashCode 可能不一致
var hash1 = data1.GetHashCode();
props.Add("D");
var hash2 = data1.GetHashCode();
Console.WriteLine(hash1 == hash2);  // 可能是 False

正确做法: 使用 ImmutableArray。

csharp
// ✅ 好的做法:使用 ImmutableArray
public record ClassData(
    string Name,
    ImmutableArray<string> Properties);  // 不可变集合

❌ 反模式 3: 忘记实现 IEquatable

问题: 缓存失效,每次都重新计算。

csharp
// ❌ 不好的做法:class 但不实现 IEquatable
public class ClassData
{
    public string Name { get; set; }
    public ImmutableArray<string> Properties { get; set; }
}

// 问题:引用不同,即使内容相同
var data1 = new ClassData { Name = "User", Properties = ImmutableArray.Create("A") };
var data2 = new ClassData { Name = "User", Properties = ImmutableArray.Create("A") };

Console.WriteLine(data1.Equals(data2));  // False
// 结果:缓存失效,重新生成代码

正确做法: 使用 record 或手动实现 IEquatable。

csharp
// ✅ 好的做法:使用 record
public record ClassData(
    string Name,
    ImmutableArray<string> Properties);

❌ 反模式 4: 过度使用 Collect

问题: 影响性能,失去增量优势。

csharp
// ❌ 不好的做法:不必要的 Collect
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(...);

var allClasses = classes.Collect();  // 等待所有类

context.RegisterSourceOutput(allClasses, (spc, classInfos) =>
{
    foreach (var classInfo in classInfos)
    {
        // 为每个类生成独立的文件
        var code = GenerateCode(classInfo);
        spc.AddSource($"{classInfo.Name}.g.cs", code);
    }
});

正确做法: 直接处理每个值。

csharp
// ✅ 好的做法:直接处理
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(...);

context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    // 直接为每个类生成代码
    var code = GenerateCode(classInfo);
    spc.AddSource($"{classInfo.Name}.g.cs", code);
});

常见错误

🐛 错误 1: 忘记实现 IEquatable

描述: 数据结构没有实现 IEquatable,导致缓存失效。

解决方案: 使用 record 类型或手动实现 IEquatable。

csharp
// ✅ 解决方案 1: 使用 record
public record ClassData(string Name, ImmutableArray<string> Properties);

// ✅ 解决方案 2: 手动实现 IEquatable
public class ClassData : IEquatable<ClassData>
{
    public string Name { get; set; }
    public ImmutableArray<string> Properties { get; set; }
    
    public bool Equals(ClassData other)
    {
        if (other is null) return false;
        return Name == other.Name &&
               Properties.SequenceEqual(other.Properties);
    }
    
    public override bool Equals(object obj) => Equals(obj as ClassData);
    
    public override int GetHashCode()
    {
        var hash = new HashCode();
        hash.Add(Name);
        foreach (var prop in Properties)
            hash.Add(prop);
        return hash.ToHashCode();
    }
}

🐛 错误 2: 使用可变集合

描述: 使用 List 等可变集合,导致缓存比较不正确。

解决方案: 使用 ImmutableArray 等不可变集合。

csharp
// ✅ 解决方案
public record ClassData(
    string Name,
    ImmutableArray<string> Properties);  // 使用 ImmutableArray

🐛 错误 3: 在 predicate 中访问语义模型

描述: 在 predicate 中访问语义模型,影响性能。

解决方案: 只在 transform 中访问语义模型。

csharp
// ❌ 错误做法
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, ct) =>
        {
            // 错误:在 predicate 中访问语义模型
            var model = ct.SemanticModel;
            return node is ClassDeclarationSyntax;
        },
        transform: (ctx, _) => ctx.Node);

// ✅ 正确做法
var classes = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) =>
        {
            // 正确:在 transform 中访问语义模型
            var model = ctx.SemanticModel;
            return ctx.Node;
        });

🐛 错误 4: GetHashCode 不一致

描述: GetHashCode 依赖可变字段。

解决方案: 使用不可变类型或只读属性。

csharp
// ❌ 错误做法
public class ClassData : IEquatable<ClassData>
{
    public string Name { get; set; }  // 可变
    
    public override int GetHashCode()
    {
        return Name?.GetHashCode() ?? 0;  // 依赖可变字段
    }
}

// ✅ 解决方案:使用 record(不可变)
public record ClassData(string Name);

最佳实践检查清单

开发前检查

  • [ ] 使用 ForAttributeWithMetadataName 而不是 CreateSyntaxProvider
  • [ ] 使用 record 类型
  • [ ] 使用 ImmutableArray 而不是 List
  • [ ] 在 predicate 中提前过滤
  • [ ] 缓存昂贵操作到 CompilationProvider
  • [ ] 添加 WithTrackingName 进行性能分析

代码审查检查

  • [ ] 没有在 transform 中执行昂贵操作
  • [ ] 没有使用可变集合
  • [ ] 正确实现了 IEquatable
  • [ ] 没有不必要的 Collect 操作
  • [ ] 没有在 predicate 中访问语义模型
  • [ ] GetHashCode 不依赖可变字段

🔑 关键要点

性能最佳实践

  1. 使用 ForAttributeWithMetadataName (10-100x 提升)
  2. 提前过滤 (减少 90% 的计算)
  3. 使用 record + ImmutableArray (提升缓存效率)
  4. 缓存昂贵操作 (避免重复计算)
  5. 添加 WithTrackingName (识别瓶颈)

可维护性最佳实践

  1. 使用 record 类型 (简化代码)
  2. 添加 WithTrackingName (便于调试)
  3. 使用结构化日志 (便于诊断)
  4. 编写单元测试 (验证正确性)
  5. 添加文档注释 (便于理解)

🔗 相关资源


🚀 下一步


最后更新: 2026-02-05

基于 MIT 许可发布