【问题标题】:writing a custom comparer for linq groupby为 linq groupby 编写自定义比较器
【发布时间】:2016-06-09 18:38:57
【问题描述】:

同样,这个示例是我实际问题的一个非常简化的版本,涉及用于 linq 分组的自定义比较器。我做错了什么?

下面的代码产生下面的结果 (1.2, 0), (4.1, 0), (4.1, 0), (1.1, 0),

但是我期待以下,因为 1.1 和 1.2 相差

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<Point> points = new List<Point> { 
            new Point(1.1, 0.0)
            , new Point(4.1, 0.0) 
            , new Point(1.2, 0.0)
            , new Point(4.1, 0.0)
        };

        foreach (var group in points.GroupBy(p => p, new PointComparer()))
        {
            foreach (var num in group)
                Console.Write(num.ToString() + ", ");

            Console.WriteLine();
        }

        Console.ReadLine();
    }
}

class PointComparer : IEqualityComparer<Point>
{
    public bool Equals(Point a, Point b)
    {
        return Math.Abs(a.X - b.X) < 1.0;
    }

    public int GetHashCode(Point point)
    {
        return point.X.GetHashCode()
            ^ point.Y.GetHashCode();
    }
}

class Point
{
    public double X;
    public double Y;

    public Point(double p1, double p2)
    {
        X = p1;
        Y = p2;
    }

    public override string ToString()
    {
        return "(" + X + ", " + Y + ")";
    }
}

【问题讨论】:

  • 我认为您不能使用 group by 作为聚类点的解决方案。原因之一是 GetHashcode 必须为相等的项目返回相同的哈希值。
  • 我知道这里没有这样做,但要注意的一件事是:不要使用 PointComparer.Default 而不是 new PointComparer() - 尽管文档说了什么,但它不会创建一个新的PointComparer,而是创建一个ObjectEqualityComparer`1

标签: linq iequalitycomparer


【解决方案1】:

使用相等比较器的分组算法(我认为是所有 LINQ 方法)总是首先比较哈希码,并且只有在两个哈希码相等时才执行 Equals。您可以看到,如果您在相等比较器中添加跟踪语句:

class PointComparer : IEqualityComparer<Point>
{
    public bool Equals(Point a, Point b)
    {
        Console.WriteLine("Equals: point {0} - point {1}", a, b);
        return Math.Abs(a.X - b.X) < 1.0;
    }

    public int GetHashCode(Point point)
    {
        Console.WriteLine("HashCode: {0}", point);
        return point.X.GetHashCode()
            ^ point.Y.GetHashCode();
    }
}

结果:

HashCode: (1.1, 0)
HashCode: (4.1, 0)
HashCode: (1.2, 0)
HashCode: (4.1, 0)
Equals: point (4.1, 0) - point (4.1, 0)
(1.1, 0), 
(4.1, 0), (4.1, 0), 
(1.2, 0), 

仅对哈希码相等的两个点Equals 执行。

现在您可以通过始终返回 0 作为哈希码来欺骗比较。如果你这样做,输出将是:

HashCode: (1.1, 0)
HashCode: (4.1, 0)
Equals: point (1.1, 0) - point (4.1, 0)
HashCode: (1.2, 0)
Equals: point (4.1, 0) - point (1.2, 0)
Equals: point (1.1, 0) - point (1.2, 0)
HashCode: (4.1, 0)
Equals: point (4.1, 0) - point (4.1, 0)
(1.1, 0), (1.2, 0), 
(4.1, 0), (4.1, 0), 

现在对于每对 Equals 都已执行,并且您已经完成了分组。

但是……

什么是“相等”?如果您再添加一个点(2.1, 0.0),您想要一组中的哪些点?使用符号 表示模糊相等,我们有 -

1.1 ≈ 1.2
1.2 ≈ 2.1

但是

1.1 !≈ 2.1

这意味着1.12.1 永远不会在一个组中(他们的Equals 永远不会通过)并且这取决于点的顺序1.1 还是@ 987654337@ 与1.2 分组。

所以你在这里是在一个滑坡上。通过邻近度聚类点绝非易事。你正在进入cluster analysis的领域。

【讨论】:

  • 这需要一些思考。我尝试了您始终返回哈希码 0 的建议,并且使用我的实际数据(不在示例中)它运行良好。我必须分析它可能失败的地方。
  • 获得某种规律性(可预测的结果)的一种方法是在分组之前始终以相同的方式对点进行分组(例如先是 X,然后是 Y)。
【解决方案2】:

不要忘记GetHashCode 的效果。期望GetHashCode 将始终为任何两个对象返回相同的值,因为每个Equals 将返回true。如果你未能达到预期,你会得到意想不到的结果。

具体来说,GroupBy 可能使用像哈希表这样的东西来允许它将项目组合在一起,而无需将每个项目与其他项目进行比较。如果GetHashCode 返回的值最终不会将两个对象放入哈希表的同一个桶中,它将假定它们不相等,并且永远不会尝试对它们调用Equals

当您尝试找出GetHashCode 的正确实现时,您会发现,您尝试对对象进行分组的方式存在根本问题。如果您的点的 x 值为 1.01.62.2,您会期待什么? 1.02.2 彼此相距太远,无法属于同一组,但 1.6 与其他两个点足够接近,应该与它们在同一组中。所以你的Equals 方法打破了Transitive 相等的属性:

只要 A = B 和 B = C,那么 A = C

如果您尝试进行集群分组,您将需要使用更不同的数据结构和算法。如果您只是想稍微标准化点的位置,您可以只说points.GroupBy(p =&gt; (int)p.X) 并完全避免使用相等比较器。

【讨论】:

  • Thata 正是我所看到的,我的 GetHasCode 对这个问题有重大影响。我对其所做的任何更改都会导致我的输出发生更改。我的 GetHashCode 方法应该是什么样的?
  • @DustyB:查看我的更新答案。您当前对使两个项目“相等”的定义是站不住脚的。您必须想出一个更具体的想法,了解您尝试对项目进行分组的依据,或者使用基于聚类而不是相等的不同数据结构和算法。
  • 我的实际问题使用 3D 点,我想将它们聚集在一起,使 10 个单位内和同一平面上的点组合在一起。使用自定义比较器是否可行?
  • @DustyB:不可靠。如果 A 点和 B 点相距在 10 个单位以内,B 和 C 相距在 10 个单位以内,但 A 不在 C 的 10 个单位范围内,那么它们是否应该在一个组中?无论您如何回答这个问题,您选择的任何纯粹基于 平等 的分组算法在某些情况下都将无法产生您想要的输出。
猜你喜欢
  • 1970-01-01
  • 2019-06-28
  • 1970-01-01
  • 2021-01-16
  • 2020-11-27
  • 2019-03-14
  • 2010-11-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多