Skip to content

缓存机制

深入理解增量生成器的缓存原理和 IEquatable 实现

📋 文档信息

属性
难度高级
阅读时间30 分钟
前置知识C# 相等性比较、record 类型、IEquatable
相关文档数据流转换

🎯 学习目标

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

  • ✅ 理解增量生成器的缓存原理
  • ✅ 实现正确的 IEquatable 接口
  • ✅ 使用 record 类型优化缓存
  • ✅ 使用自定义比较器控制缓存行为
  • ✅ 诊断和解决缓存问题

📚 快速导航

章节说明
缓存原理增量生成器如何使用缓存
IEquatable 实现正确实现相等性比较
record 类型使用 record 简化实现
自定义比较器WithComparer 的使用
常见问题缓存相关的常见问题

缓存原理

增量计算的核心

增量生成器的核心优势是缓存机制。只有当输入改变时,才会重新计算输出。

缓存工作流程

csharp
using Microsoft.CodeAnalysis;

/// <summary>
/// 演示缓存工作流程
/// </summary>
public class CachingWorkflow
{
    /// <summary>
    /// 定义可缓存的数据结构
    /// </summary>
    public record ClassInfo(
        string Name,
        string Namespace,
        ImmutableArray<string> Properties);
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classInfos = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) =>
                {
                    var classDecl = (ClassDeclarationSyntax)ctx.Node;
                    var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl);
                    
                    // 创建可缓存的数据结构
                    return new ClassInfo(
                        symbol.Name,
                        symbol.ContainingNamespace.ToDisplayString(),
                        symbol.GetMembers()
                            .OfType<IPropertySymbol>()
                            .Select(p => p.Name)
                            .ToImmutableArray());
                });
        
        // 只有当 ClassInfo 改变时才会重新生成代码
        context.RegisterSourceOutput(classInfos, (spc, info) =>
        {
            // 这个方法只在 info 改变时被调用
            var code = GenerateCode(info);
            spc.AddSource($"{info.Name}.g.cs", code);
        });
    }
    
    private string GenerateCode(ClassInfo info)
    {
        return $"// Class: {info.Name}";
    }
}

缓存比较过程


IEquatable 实现

为什么需要 IEquatable

csharp
using System;

/// <summary>
/// 演示为什么需要 IEquatable
/// </summary>
public class WhyIEquatable
{
    // ❌ 不好的做法:使用 class 但不实现 IEquatable
    public class ClassInfoBad
    {
        public string Name { get; set; }
        public List<string> Properties { get; set; }
    }
    
    // 问题:每次都会重新计算,因为引用不同
    public void DemonstrateProblem()
    {
        var info1 = new ClassInfoBad 
        { 
            Name = "User", 
            Properties = new List<string> { "Id", "Name" } 
        };
        
        var info2 = new ClassInfoBad 
        { 
            Name = "User", 
            Properties = new List<string> { "Id", "Name" } 
        };
        
        // 即使内容相同,引用不同
        Console.WriteLine(info1 == info2);  // False
        Console.WriteLine(info1.Equals(info2));  // False
        
        // 结果:缓存失效,每次都重新生成代码
    }
    
    // ✅ 好的做法:实现 IEquatable
    public class ClassInfoGood : IEquatable<ClassInfoGood>
    {
        public string Name { get; set; }
        public ImmutableArray<string> Properties { get; set; }
        
        public bool Equals(ClassInfoGood other)
        {
            if (other is null) return false;
            if (ReferenceEquals(this, other)) return true;
            
            return Name == other.Name &&
                   Properties.SequenceEqual(other.Properties);
        }
        
        public override bool Equals(object obj)
        {
            return Equals(obj as ClassInfoGood);
        }
        
        public override int GetHashCode()
        {
            var hash = new HashCode();
            hash.Add(Name);
            foreach (var prop in Properties)
            {
                hash.Add(prop);
            }
            return hash.ToHashCode();
        }
    }
    
    // 现在缓存可以正常工作
    public void DemonstrateSolution()
    {
        var info1 = new ClassInfoGood 
        { 
            Name = "User", 
            Properties = ImmutableArray.Create("Id", "Name") 
        };
        
        var info2 = new ClassInfoGood 
        { 
            Name = "User", 
            Properties = ImmutableArray.Create("Id", "Name") 
        };
        
        // 内容相同,相等性比较返回 true
        Console.WriteLine(info1.Equals(info2));  // True
        
        // 结果:缓存生效,不会重新生成代码
    }
}

正确实现 IEquatable

csharp
using System;
using System.Collections.Immutable;

/// <summary>
/// 正确实现 IEquatable 的示例
/// </summary>
public class ClassInfo : IEquatable<ClassInfo>
{
    public string Name { get; }
    public string Namespace { get; }
    public ImmutableArray<string> Properties { get; }
    
    public ClassInfo(string name, string ns, ImmutableArray<string> properties)
    {
        Name = name;
        Namespace = ns;
        Properties = properties;
    }
    
    // 1. 实现 IEquatable<T>.Equals
    public bool Equals(ClassInfo other)
    {
        // 处理 null
        if (other is null) return false;
        
        // 处理引用相等
        if (ReferenceEquals(this, other)) return true;
        
        // 比较所有字段
        return Name == other.Name &&
               Namespace == other.Namespace &&
               Properties.SequenceEqual(other.Properties);
    }
    
    // 2. 重写 Object.Equals
    public override bool Equals(object obj)
    {
        return Equals(obj as ClassInfo);
    }
    
    // 3. 重写 GetHashCode
    public override int GetHashCode()
    {
        var hash = new HashCode();
        hash.Add(Name);
        hash.Add(Namespace);
        
        // 对于集合,需要添加每个元素
        foreach (var prop in Properties)
        {
            hash.Add(prop);
        }
        
        return hash.ToHashCode();
    }
    
    // 4. 可选:重载 == 和 != 运算符
    public static bool operator ==(ClassInfo left, ClassInfo right)
    {
        if (left is null) return right is null;
        return left.Equals(right);
    }
    
    public static bool operator !=(ClassInfo left, ClassInfo right)
    {
        return !(left == right);
    }
}

IEquatable 实现检查清单

csharp
/// <summary>
/// IEquatable 实现检查清单
/// </summary>
public class IEquatableChecklist
{
    // ✅ 1. 实现 IEquatable<T>
    public class MyData : IEquatable<MyData>
    {
        public string Name { get; set; }
        
        // ✅ 2. 实现 Equals(T other)
        public bool Equals(MyData other)
        {
            if (other is null) return false;
            if (ReferenceEquals(this, other)) return true;
            return Name == other.Name;
        }
        
        // ✅ 3. 重写 Equals(object obj)
        public override bool Equals(object obj)
        {
            return Equals(obj as MyData);
        }
        
        // ✅ 4. 重写 GetHashCode()
        public override int GetHashCode()
        {
            return Name?.GetHashCode() ?? 0;
        }
        
        // ✅ 5. 可选:重载运算符
        public static bool operator ==(MyData left, MyData right)
        {
            if (left is null) return right is null;
            return left.Equals(right);
        }
        
        public static bool operator !=(MyData left, MyData right)
        {
            return !(left == right);
        }
    }
}

record 类型

record 的优势

record 类型自动实现 IEquatable,大大简化了代码:

csharp
using System.Collections.Immutable;

/// <summary>
/// 使用 record 类型简化 IEquatable 实现
/// </summary>
public class RecordAdvantages
{
    // ❌ 使用 class:需要手动实现 IEquatable(约 50 行代码)
    public class ClassInfoClass : IEquatable<ClassInfoClass>
    {
        public string Name { get; }
        public string Namespace { get; }
        public ImmutableArray<string> Properties { get; }
        
        public ClassInfoClass(string name, string ns, ImmutableArray<string> properties)
        {
            Name = name;
            Namespace = ns;
            Properties = properties;
        }
        
        public bool Equals(ClassInfoClass other)
        {
            if (other is null) return false;
            if (ReferenceEquals(this, other)) return true;
            return Name == other.Name &&
                   Namespace == other.Namespace &&
                   Properties.SequenceEqual(other.Properties);
        }
        
        public override bool Equals(object obj) => Equals(obj as ClassInfoClass);
        
        public override int GetHashCode()
        {
            var hash = new HashCode();
            hash.Add(Name);
            hash.Add(Namespace);
            foreach (var prop in Properties)
                hash.Add(prop);
            return hash.ToHashCode();
        }
    }
    
    // ✅ 使用 record:自动实现 IEquatable(1 行代码)
    public record ClassInfoRecord(
        string Name,
        string Namespace,
        ImmutableArray<string> Properties);
    
    // record 自动提供:
    // - IEquatable<T> 实现
    // - Equals(T other) 方法
    // - Equals(object obj) 重写
    // - GetHashCode() 重写
    // - == 和 != 运算符重载
    // - ToString() 重写
    // - 解构方法
    // - with 表达式支持
}

record 的相等性语义

csharp
using System.Collections.Immutable;

/// <summary>
/// 演示 record 的相等性语义
/// </summary>
public class RecordEqualitySemantics
{
    public record ClassInfo(
        string Name,
        string Namespace,
        ImmutableArray<string> Properties);
    
    public void DemonstrateEquality()
    {
        var info1 = new ClassInfo(
            "User",
            "MyApp.Models",
            ImmutableArray.Create("Id", "Name"));
        
        var info2 = new ClassInfo(
            "User",
            "MyApp.Models",
            ImmutableArray.Create("Id", "Name"));
        
        // ✅ 值相等性(不是引用相等性)
        Console.WriteLine(info1 == info2);  // True
        Console.WriteLine(info1.Equals(info2));  // True
        
        // ✅ GetHashCode 一致
        Console.WriteLine(info1.GetHashCode() == info2.GetHashCode());  // True
        
        // ✅ with 表达式创建副本
        var info3 = info1 with { Name = "Product" };
        Console.WriteLine(info3.Name);  // "Product"
        Console.WriteLine(info3.Namespace);  // "MyApp.Models"
        
        // ✅ 解构
        var (name, ns, props) = info1;
        Console.WriteLine(name);  // "User"
    }
}

record 最佳实践

csharp
using System.Collections.Immutable;

/// <summary>
/// record 类型最佳实践
/// </summary>
public class RecordBestPractices
{
    // ✅ 好的做法:使用 record + ImmutableArray
    public record ClassInfoGood(
        string Name,
        string Namespace,
        ImmutableArray<string> Properties);
    
    // ❌ 不好的做法:使用 record + List
    public record ClassInfoBad(
        string Name,
        string Namespace,
        List<string> Properties);  // 可变集合!
    
    public void DemonstrateProblem()
    {
        var props = new List<string> { "Id", "Name" };
        var info1 = new ClassInfoBad("User", "MyApp", props);
        var info2 = new ClassInfoBad("User", "MyApp", props);
        
        // 问题:引用相同的 List
        Console.WriteLine(info1 == info2);  // True(因为引用相同)
        
        // 修改 List
        props.Add("Email");
        
        // 问题:两个 record 都被修改了
        Console.WriteLine(info1.Properties.Count);  // 3
        Console.WriteLine(info2.Properties.Count);  // 3
        
        // 问题:GetHashCode 可能不一致
        var hash1 = info1.GetHashCode();
        props.Add("Phone");
        var hash2 = info1.GetHashCode();
        Console.WriteLine(hash1 == hash2);  // 可能是 False
    }
    
    public void DemonstrateSolution()
    {
        var info1 = new ClassInfoGood(
            "User",
            "MyApp",
            ImmutableArray.Create("Id", "Name"));
        
        var info2 = new ClassInfoGood(
            "User",
            "MyApp",
            ImmutableArray.Create("Id", "Name"));
        
        // ✅ 值相等性正常工作
        Console.WriteLine(info1 == info2);  // True
        
        // ✅ 不能修改 ImmutableArray
        // info1.Properties.Add("Email");  // 编译错误
        
        // ✅ GetHashCode 一致
        var hash1 = info1.GetHashCode();
        var hash2 = info1.GetHashCode();
        Console.WriteLine(hash1 == hash2);  // True
    }
}

自定义比较器

WithComparer 的使用

使用 WithComparer 可以自定义缓存行为:

csharp
using Microsoft.CodeAnalysis;
using System.Collections.Generic;

/// <summary>
/// 使用 WithComparer 自定义缓存行为
/// </summary>
public class CustomComparerDemo
{
    /// <summary>
    /// 类信息
    /// </summary>
    public class ClassInfo
    {
        public string Name { get; set; }
        public string Namespace { get; set; }
        public List<string> Properties { get; set; }
        public string Documentation { get; set; }
    }
    
    /// <summary>
    /// 自定义比较器:忽略文档注释的变化
    /// </summary>
    public class ClassInfoComparer : IEqualityComparer<ClassInfo>
    {
        public bool Equals(ClassInfo x, ClassInfo y)
        {
            if (x == null && y == null) return true;
            if (x == null || y == null) return false;
            
            // 只比较名称、命名空间和属性
            // 忽略文档注释的变化
            return x.Name == y.Name &&
                   x.Namespace == y.Namespace &&
                   x.Properties.SequenceEqual(y.Properties);
        }
        
        public int GetHashCode(ClassInfo obj)
        {
            if (obj == null) return 0;
            
            var hash = new HashCode();
            hash.Add(obj.Name);
            hash.Add(obj.Namespace);
            foreach (var prop in obj.Properties)
            {
                hash.Add(prop);
            }
            // 不包含 Documentation
            return hash.ToHashCode();
        }
    }
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classInfos = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) =>
                {
                    var classDecl = (ClassDeclarationSyntax)ctx.Node;
                    var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl);
                    
                    return new ClassInfo
                    {
                        Name = symbol.Name,
                        Namespace = symbol.ContainingNamespace.ToDisplayString(),
                        Properties = symbol.GetMembers()
                            .OfType<IPropertySymbol>()
                            .Select(p => p.Name)
                            .ToList(),
                        Documentation = symbol.GetDocumentationCommentXml()
                    };
                })
            .WithComparer(new ClassInfoComparer());  // 使用自定义比较器
        
        // 现在,只有当名称、命名空间或属性改变时才会重新生成
        // 文档注释的变化不会触发重新生成
        context.RegisterSourceOutput(classInfos, (spc, info) =>
        {
            var code = GenerateCode(info);
            spc.AddSource($"{info.Name}.g.cs", code);
        });
    }
    
    private string GenerateCode(ClassInfo info)
    {
        return $"// Class: {info.Name}";
    }
}

常见的自定义比较器场景

csharp
using System.Collections.Generic;

/// <summary>
/// 常见的自定义比较器场景
/// </summary>
public class CommonComparerScenarios
{
    // 场景 1: 忽略大小写
    public class CaseInsensitiveComparer : IEqualityComparer<string>
    {
        public bool Equals(string x, string y)
        {
            return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
        }
        
        public int GetHashCode(string obj)
        {
            return obj?.ToLowerInvariant().GetHashCode() ?? 0;
        }
    }
    
    // 场景 2: 只比较部分字段
    public class PartialFieldComparer : IEqualityComparer<ClassInfo>
    {
        public bool Equals(ClassInfo x, ClassInfo y)
        {
            if (x == null && y == null) return true;
            if (x == null || y == null) return false;
            
            // 只比较名称,忽略其他字段
            return x.Name == y.Name;
        }
        
        public int GetHashCode(ClassInfo obj)
        {
            return obj?.Name?.GetHashCode() ?? 0;
        }
    }
    
    // 场景 3: 自定义相等性逻辑
    public class CustomLogicComparer : IEqualityComparer<ClassInfo>
    {
        public bool Equals(ClassInfo x, ClassInfo y)
        {
            if (x == null && y == null) return true;
            if (x == null || y == null) return false;
            
            // 自定义逻辑:如果属性数量相同,认为相等
            return x.Properties.Count == y.Properties.Count;
        }
        
        public int GetHashCode(ClassInfo obj)
        {
            return obj?.Properties.Count.GetHashCode() ?? 0;
        }
    }
    
    public class ClassInfo
    {
        public string Name { get; set; }
        public List<string> Properties { get; set; }
    }
}

常见问题

问题 1: 缓存不生效

症状: 每次编译都重新生成代码,即使没有改变。

原因: 数据结构没有正确实现 IEquatable。

解决方案:

csharp
// ❌ 问题代码
public class ClassInfo  // 没有实现 IEquatable
{
    public string Name { get; set; }
    public List<string> Properties { get; set; }  // 可变集合
}

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

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

问题 2: GetHashCode 不一致

症状: 缓存行为不可预测。

原因: GetHashCode 依赖可变字段。

解决方案:

csharp
// ❌ 问题代码
public class ClassInfo : IEquatable<ClassInfo>
{
    public string Name { get; set; }  // 可变
    
    public override int GetHashCode()
    {
        return Name?.GetHashCode() ?? 0;  // 依赖可变字段
    }
}

// 问题演示
var info = new ClassInfo { Name = "User" };
var hash1 = info.GetHashCode();

info.Name = "Product";  // 修改字段
var hash2 = info.GetHashCode();  // hash 改变了!

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

// 或者使用只读属性
public class ClassInfo : IEquatable<ClassInfo>
{
    public string Name { get; }  // 只读
    
    public ClassInfo(string name)
    {
        Name = name;
    }
    
    public override int GetHashCode()
    {
        return Name?.GetHashCode() ?? 0;
    }
}

问题 3: 集合比较错误

症状: 即使集合内容相同,也认为不相等。

原因: 使用引用相等性而不是值相等性。

解决方案:

csharp
// ❌ 问题代码
public record ClassInfo(
    string Name,
    List<string> Properties);  // List 使用引用相等性

public void DemonstrateProblem()
{
    var info1 = new ClassInfo("User", new List<string> { "Id", "Name" });
    var info2 = new ClassInfo("User", new List<string> { "Id", "Name" });
    
    Console.WriteLine(info1 == info2);  // False(List 引用不同)
}

// ✅ 解决方案:使用 ImmutableArray
public record ClassInfo(
    string Name,
    ImmutableArray<string> Properties);  // ImmutableArray 使用值相等性

public void DemonstrateSolution()
{
    var info1 = new ClassInfo("User", ImmutableArray.Create("Id", "Name"));
    var info2 = new ClassInfo("User", ImmutableArray.Create("Id", "Name"));
    
    Console.WriteLine(info1 == info2);  // True
}

问题 4: 性能问题

症状: 生成器运行缓慢。

原因: Equals 或 GetHashCode 实现效率低。

解决方案:

csharp
// ❌ 问题代码:低效的 GetHashCode
public class ClassInfo : IEquatable<ClassInfo>
{
    public ImmutableArray<string> Properties { get; set; }
    
    public override int GetHashCode()
    {
        // 低效:每次都创建新字符串
        return string.Join(",", Properties).GetHashCode();
    }
}

// ✅ 解决方案:高效的 GetHashCode
public class ClassInfo : IEquatable<ClassInfo>
{
    public ImmutableArray<string> Properties { get; set; }
    
    public override int GetHashCode()
    {
        // 高效:使用 HashCode 结构
        var hash = new HashCode();
        foreach (var prop in Properties)
        {
            hash.Add(prop);
        }
        return hash.ToHashCode();
    }
}

// ✅ 最佳方案:使用 record
public record ClassInfo(ImmutableArray<string> Properties);
// record 自动生成高效的 GetHashCode

🔑 关键要点

缓存最佳实践

  1. 优先使用 record 类型

    csharp
    public record ClassInfo(string Name, ImmutableArray<string> Properties);
  2. 使用不可变集合

    csharp
    // ✅ 使用 ImmutableArray
    public record ClassInfo(ImmutableArray<string> Properties);
    
    // ❌ 不要使用 List
    public record ClassInfo(List<string> Properties);
  3. 正确实现 IEquatable

    csharp
    public class ClassInfo : IEquatable<ClassInfo>
    {
        // 实现 Equals(T other)
        // 重写 Equals(object obj)
        // 重写 GetHashCode()
    }
  4. 使用 WithComparer 优化缓存

    csharp
    var data = provider.WithComparer(new CustomComparer());

缓存检查清单

  • [ ] 使用 record 类型或实现 IEquatable
  • [ ] 使用 ImmutableArray 而不是 List
  • [ ] GetHashCode 不依赖可变字段
  • [ ] Equals 比较所有相关字段
  • [ ] 考虑使用 WithComparer 优化缓存行为

🔗 相关资源


🚀 下一步


最后更新: 2026-02-05

基于 MIT 许可发布