You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 17:27:03