结论:别用 equalitycomparer.default 做自定义逻辑,它只认引用或默认值相等;真要按业务字段、忽略大小写、处理 nan 或空值,必须自己实现 iequalitycomparer——而且 gethashcode 写错,hashset 和 dictionary 就会静默失效。

直接说结论:别用 EqualityComparer<t>.Default</t> 做自定义逻辑,它只认引用或默认值相等;真要按业务字段、忽略大小写、处理 NaN 或空值,必须自己实现 IEqualityComparer<t></t> —— 而且 GetHashCode 写错,HashSet 和 Dictionary 就会静默失效。
为什么 Dictionary/HashSet 用了自定义比较器还找不到对象?
根本原因几乎总是 GetHashCode 和 Equals 不一致。比如你写了一个只看 Name 的 PersonNameComparer,但 GetHashCode 却返回 obj.GetHashCode()(即引用哈希),那两个 Name 相同的 Person 实例哈希码不同,Dictionary 根本不会把它们放进同一个桶里,ContainsKey 必然返回 false。
常见错误包括:
- 在
GetHashCode中用了未参与Equals判断的字段(比如加了Age) - 对
null字段没判空就调用.GetHashCode(),抛NullReferenceException - 用
StringComparison.OrdinalIgnoreCase调string.GetHashCode(),但忘了这个重载只在 .NET Core 2.1+ / .NET 5+ 支持,旧框架会报错
正确做法是:只要 Equals 认为相等,GetHashCode 就必须返回相同值;反之不强制要求,但冲突越少性能越好。
字符串忽略大小写的比较器怎么写才安全?
别用 ToLower() 或 ToUpper() —— 它们分配新字符串、不支持文化敏感场景(比如土耳其语的 i)、且在某些 Unicode 组合字符下行为不可靠。
推荐写法(兼容 .NET Framework 4.7.2+ 和所有现代 .NET):
public class CaseInsensitiveStringComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y) =>
string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
public int GetHashCode(string obj) =>
obj?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0;
}</string>
注意点:
-
StringComparison.OrdinalIgnoreCase是唯一被GetHashCode支持的非默认枚举值;其他如CultureInvariant或带CurrentCulture的都不行 - 如果目标框架低于 .NET Core 2.1,得退回到
obj?.ToLowerInvariant().GetHashCode() ?? 0,但务必配对使用ToLowerInvariant()而不是ToLower() - 别试图在
GetHashCode里做string.Compare,它不返回哈希码
处理 double.NaN 或 float.NaN 的相等性怎么办?
double.NaN == double.NaN 永远是 false,但很多业务场景(比如元组比较、配置快照比对)需要把它当相等处理。这时不能依赖 EqualityComparer<double>.Default</double>,得自定义。
关键在于:你无法直接实现 IEqualityComparer<double></double> 并传给 Tuple<double double></double>,因为元组的 IStructuralEquatable.Equals 接收的是非泛型 IEqualityComparer。
所以必须写一个非泛型版本:
public class NanComparer : IEqualityComparer
{
public new bool Equals(object x, object y)
{
if (x is double dx && y is double dy) return dx == dy || (double.IsNaN(dx) && double.IsNaN(dy));
if (x is float fx && y is float fy) return fx == fy || (float.IsNaN(fx) && float.IsNaN(fy));
return EqualityComparer<object>.Default.Equals(x, y);
}
public int GetHashCode(object obj) =>
obj switch
{
double d when double.IsNaN(d) => 0,
float f when float.IsNaN(f) => 0,
_ => EqualityComparer<object>.Default.GetHashCode(obj)
};
}</object></object>
然后这样用:
var t1 = Tuple.Create(1.0, double.NaN, 3.0); var t2 = Tuple.Create(1.0, double.NaN, 3.0); bool equal = ((IStructuralEquatable)t1).Equals(t2, new NanComparer()); // true
⚠️ 注意:GetHashCode 对所有 NaN 返回相同值(比如 0),否则结构相等性会失败。
什么时候该用 IEqualityComparer 而不是重写 Equals?
重写 Equals 和 GetHashCode 是类型自身的“默认相等观”,而 IEqualityComparer<t></t> 是外部视角的“临时规则”。选哪个取决于谁控制上下文:
- 如果你能改
Person类,且“按 ID 相等”就是它天然的业务含义 → 重写Equals+ 实现IEquatable<person></person> - 如果你用的是第三方类(比如
System.Drawing.Point),或同一类型需多种规则(ID 查找 vs 坐标范围匹配)→ 必须用IEqualityComparer<t></t> - 如果你只是临时去重一个
List<string></string>,用.Distinct(new CaseInsensitiveStringComparer())比改全局string行为更安全
最易忽略的一点:Dictionary<tkey tvalue></tkey> 构造时传入的 IEqualityComparer<tkey></tkey> 会绑定到整个生命周期 —— 换言之,一旦创建,就不能中途切换比较逻辑。需要多套规则?那就准备多个 Dictionary 实例。











