C#重写Equals后LINQ GroupBy分组的比较器与GetHashCode实现问题
问题根源分析
- 违反.NET相等性约定:框架明确要求「若两个对象通过
Equals()判定为相等,则必须返回相同哈希码」。你当前的ArcPrimitive/CirclePrimitive重写了Equals(),但GetHashCode()返回base.GetHashCode()(即object的引用哈希),导致值相等但引用不同的对象哈希码完全不同。 - 测试IEquatable失效的核心原因:你写的
Test类仅实现了IEquatable<Test>,但未重写object.Equals()和GetHashCode()。GroupBy()默认使用的EqualityComparer<T>.Default会先比较哈希码——因GetHashCode()用引用哈希,即使Value相同,哈希码不同就会直接分到不同组,根本不会触发Equals()的比较。 - 自定义EqualityComparer的冗余本质:你硬编码的哈希字典是在绕过错误的
GetHashCode()逻辑,但完全重复实现了相同判定规则,违反DRY原则。
最优解决方案
1. 修正相等性约定(核心修复)
1.1 重写GetHashCode()
因你的Equals()使用EqualsHelpers处理浮点精度近似相等(如角度/长度在误差范围内判定为相等),不能直接用原始double值生成哈希码,需:
- 先把参与相等判断的属性按误差精度规范化(与
EqualsHelpers的误差范围完全一致) - 再用规范化后的值生成哈希码
示例:ArcPrimitive修正代码
public class ArcPrimitive : Primitive { public double Angle { get; set; } public double Length { get; set; } // 保留原有Equals逻辑(补充null防护) public override bool Equals(object obj) { if (obj is null) return false; if (obj is ArcPrimitive other) return EqualsHelpers.EqualAngles(Angle, other.Angle) && EqualsHelpers.EqualLengths(Length, other.Length); if (obj is ArcPrimitiveType otherType) return EqualsHelpers.EqualAngles(Angle, otherType.Angle) && EqualsHelpers.EqualLengths(Length, otherType.Length); return false; } // 关键:重写GetHashCode,与Equals逻辑匹配 public override int GetHashCode() { // 必须与EqualsHelpers的误差值完全一致(假设EqualAngles/EqualLengths的误差为1e-6) const double tolerance = 1e-6; // 规范化浮点值:将误差范围内的浮点值映射为同一值 double normalizedAngle = Math.Round(Angle / tolerance) * tolerance; double normalizedLength = Math.Round(Length / tolerance) * tolerance; // 用规范化后的值生成哈希(.NET Core 2.1+/.NET Framework 4.6.1+可用HashCode.Combine) return HashCode.Combine(normalizedAngle, normalizedLength); } }
示例:CirclePrimitive修正代码
public class CirclePrimitive : Primitive { public double Radius { get; set; } // 保留原有Equals逻辑(补充null防护) public override bool Equals(object obj) { if (obj is null) return false; if (obj is CirclePrimitive other) return EqualsHelpers.EqualLengths(Radius, other.Radius); if (obj is CirclePrimitiveType otherType) return EqualsHelpers.EqualLengths(Radius, otherType.Radius); return false; } // 关键:重写GetHashCode public override int GetHashCode() { const double tolerance = 1e-6; // 与EqualsHelpers.EqualLengths的误差一致 double normalizedRadius = Math.Round(Radius / tolerance) * tolerance; return HashCode.Combine(normalizedRadius); } }
注意:若使用旧版.NET框架(无
HashCode.Combine),可替换为Tuple.Create(normalizedAngle, normalizedLength).GetHashCode()实现。
1.2 修复之前的IEquatable测试
你之前的Test类需补充object.Equals()和GetHashCode()的重写:
class Test : IEquatable<Test> { public int Value { get; set; } public Test(int val) => Value = val; // 实现IEquatable<Test>逻辑(补充null防护) public bool Equals(Test other) => other != null && Value == other.Value; // 必须重写object.Equals(框架约定) public override bool Equals(object obj) => Equals(obj as Test); // 必须重写GetHashCode(框架约定) public override int GetHashCode() => Value.GetHashCode(); }
此时调用testList.GroupBy(p => p)会正确分组(相同Value的对象归为一组)。
2. 泛型EqualityComparer(可选:用于不可修改的类)
若需对无法修改源码的第三方类实现分组,可实现通用泛型比较器,避免重复代码:
public class GenericEqualityComparer<T> : IEqualityComparer<T> { private readonly Func<T, T, bool> _equals; private readonly Func<T, int> _getHashCode; // 允许传入自定义相等/哈希逻辑,或默认用类自身的Equals/GetHashCode public GenericEqualityComparer(Func<T, T, bool> equals = null, Func<T, int> getHashCode = null) { _equals = equals ?? ((a, b) => a.Equals(b)); _getHashCode = getHashCode ?? (obj => obj.GetHashCode()); } public bool Equals(T x, T y) => _equals(x, y); public int GetHashCode(T obj) => _getHashCode(obj); }
使用示例(无需再为每个类写自定义比较器):
// 直接用ArcPrimitive自身的Equals/GetHashCode逻辑 var arcGroups = arcPrimitives.GroupBy(p => p, new GenericEqualityComparer<ArcPrimitive>()).ToList(); var circleGroups = circlePrimitives.GroupBy(p => p, new GenericEqualityComparer<CirclePrimitive>()).ToList();
内容的提问来源于stack exchange,提问作者jperna7254
相关产品推荐
相关产品推荐

