缓存机制
深入理解增量生成器的缓存原理和 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🔑 关键要点
缓存最佳实践
优先使用 record 类型
csharppublic record ClassInfo(string Name, ImmutableArray<string> Properties);使用不可变集合
csharp// ✅ 使用 ImmutableArray public record ClassInfo(ImmutableArray<string> Properties); // ❌ 不要使用 List public record ClassInfo(List<string> Properties);正确实现 IEquatable
csharppublic class ClassInfo : IEquatable<ClassInfo> { // 实现 Equals(T other) // 重写 Equals(object obj) // 重写 GetHashCode() }使用 WithComparer 优化缓存
csharpvar data = provider.WithComparer(new CustomComparer());
缓存检查清单
- [ ] 使用 record 类型或实现 IEquatable
- [ ] 使用 ImmutableArray 而不是 List
- [ ] GetHashCode 不依赖可变字段
- [ ] Equals 比较所有相关字段
- [ ] 考虑使用 WithComparer 优化缓存行为
🔗 相关资源
🚀 下一步
最后更新: 2026-02-05