【问题标题】:Equals Override and LINQ GroupBy等于覆盖和 LINQ GroupBy
【发布时间】:2021-03-11 17:15:17
【问题描述】:

我有两个类,一个叫 ArcPrimitive,另一个叫 CirclePrimitive。

public class ArcPrimitive : Primitive
    {
        public double Angle { get; set; }
        public double Length { get; set; }

        public override bool Equals(object obj)
        {
            if (obj is ArcPrimitive other)
            {
                return EqualsHelpers.EqualAngles(Angle, other.Angle) && EqualsHelpers.EqualLengths(Length, other.Length);
            }
            else if (obj is ArcPrimitiveType otherType)
            {
                return EqualsHelpers.EqualAngles(Angle, otherType.Angle) && EqualsHelpers.EqualLengths(Length, otherType.Length);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

public class CirclePrimitive : Primitive
    {
        public double Radius { get; set; }

        public override bool Equals(object obj)
        {
            if (obj is CirclePrimitive other)
            {
                return EqualsHelpers.EqualLengths(Radius, other.Radius);
            }
            else if (obj is CirclePrimitiveType otherType)
            {
                return EqualsHelpers.EqualLengths(Radius, otherType.Radius);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

现在,在我的项目的其他地方,我有 ArcPrimitive 对象和 CirclePrimitive 对象的集合 (List)。我想将 ArcPrimitive 对象和 CirclePrimitive 对象组合在一起,当我的重写 Equals 方法用于每个类时,它们被认为是相等的。我试图在我的 ArcPrimitive 和 CirclePrimitive 对象集合上使用 GroupBy() 扩展方法来做到这一点。但是,让 GroupBy 方法正确分组对象的唯一方法是使用以下重载并提供一个实现 IEqualityComparer 接口的类:

GroupBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>, IEqualityComparer<TKey>)

ArcPrimitiveEqualityComparer apec = new ArcPrimitiveEqualityComparer(arcPrimitives);
CirclePrimitiveEqualityComparer cpec = new CirclePrimitiveEqualityComparer(circlePrimitives);

var arcPrimitiveGroups = arcPrimitives.GroupBy(p => p, apec).ToList();
var circlePrimitiveGroups = circlePrimitives.GroupBy(p => p, cpec).ToList();

我的问题是我必须编写的 EqualityComparer 类非常不干燥(不要重复自己),这让我想知道是否有更好的方法。

public class ArcPrimitiveEqualityComparer : IEqualityComparer<ArcPrimitive>
    {
        public Dictionary<ArcPrimitive, int> ArcHashDict { get; set; }

        public ArcPrimitiveEqualityComparer(List<ArcPrimitive> arcPrimitives)
        {
            ArcHashDict = new Dictionary<ArcPrimitive, int>();
            int hCode = 0;

            foreach (ArcPrimitive arcPrimitive in arcPrimitives)
            {
                var keys = ArcHashDict.Keys;
                bool matchFound = false;

                foreach (var key in keys)
                {
                    if (arcPrimitive.Equals(key))
                    {
                        matchFound = true;
                    }
                }

                if (matchFound == false)
                {
                    ArcHashDict.Add(arcPrimitive, hCode);
                    hCode += 1;
                }
            }
        }
        
        public bool Equals(ArcPrimitive ap1, ArcPrimitive ap2)
        {
            return ap1.Equals(ap2);
        }

        public int GetHashCode(ArcPrimitive ap)
        {
            foreach (var key in ArcHashDict.Keys)
            {
                if (ap.Equals(key))
                {
                    return ArcHashDict[key];
                }
            }

            throw new Exception("ArcPrimitive does not have a hash code.");
        }
    }

public class CirclePrimitiveEqualityComparer : IEqualityComparer<CirclePrimitive>
    {
        public Dictionary<CirclePrimitive, int> CircleHashDict { get; set; }

        public CirclePrimitiveEqualityComparer(List<CirclePrimitive> circlePrimitives)
        {
            CircleHashDict = new Dictionary<CirclePrimitive, int>();
            int hCode = 0;

            foreach (CirclePrimitive circlePrimitive in circlePrimitives)
            {
                var keys = CircleHashDict.Keys;
                bool matchFound = false;

                foreach (var key in keys)
                {
                    if (circlePrimitive.Equals(key))
                    {
                        matchFound = true;
                    }
                }

                if (matchFound == false)
                {
                    CircleHashDict.Add(circlePrimitive, hCode);
                    hCode += 1;
                }
            }
        }

        public bool Equals(CirclePrimitive cp1, CirclePrimitive cp2)
        {
            return cp1.Equals(cp2);
        }

        public int GetHashCode(CirclePrimitive cp)
        {
            foreach (var key in CircleHashDict.Keys)
            {
                if (cp.Equals(key))
                {
                    return CircleHashDict[key];
                }
            }

            throw new Exception("CirclePrimitive does not have a hash code.");
        }
    }

我采用了几种不同的方法来解决这种严重的重复问题。一种是创建一个通用的 EqualityComparer 类。我在那里遇到的问题是,当泛型类中未指定对象类型时,我无法访问正确的 Equals 覆盖。我尝试的另一种方法是在我的 ArcPrimitive 和 CirclePrimitive 类中不仅覆盖 Equals,而且覆盖 GetHashCode,希望 GroupBy 只使用覆盖方法进行分组。但是,我无法弄清楚如何为这些对象正确生成哈希码,因为从 Equals 返回 true 的对象必须具有相同的哈希码,而且我无法弄清楚如何将自定义相等方法应用于哈希码函数来获取必要的哈希码。

抱歉,帖子太长了,我只是觉得有必要添加代码以提供我的问题的详细信息。

编辑:对 NetMage 评论的回应 这是一个尝试使用 IEquatable 来控制 GroupBy 对分组和键进行的相等比较的测试。

class Program
{
    static void Main(string[] args)
    {
        List<Test> testList = new List<Test>
        {
            new Test(1),
            new Test(1),
            new Test(2),
            new Test(3),
            new Test(3)
        };

        var result = testList.GroupBy(p => p);

        foreach (var group in result)
        {
            Console.WriteLine($"Key value: {group.Key.Value}; Group count {group.Count()}");
        }

        Console.ReadLine();
        
        // Output
        // Key value: 1; Group count: 1
        // Key value: 1; Group count: 1
        // Key value: 2; Group count: 1
        // Key value: 3; Group count: 1
        // Key value: 3; Group count: 1

    }
}

class Test : IEquatable<Test>
{
    public int Value { get; set; }

    public Test(int val)
    {
        Value = val;
    }

    public bool Equals(Test other)
    {
        return Value == other.Value;
    }
}

如您所见,每个对象都成为一个键并且没有一个对象被组合在一起,即使我希望将具有相同 Value 属性的对象组合在一起。

【问题讨论】:

  • 如果你倒带一点,你的最终目标是什么? GroupBy 听起来像是一个奇怪的平等比较选择。此外,GroupBy 重载中的 IEqualityComparer 应该比较选定的键而不是整个对象。
  • 如果你的原语实现IEquatable&lt;T&gt;,那么GroupBy应该默认使用它们自己的相等性。
  • @Xerillio 目标是根据我在上面的覆盖 Equals 方法中所做的比较对被认为相似/相等的项目进行分组。我想达到一个点,我有一组被认为相等的对象,以便我可以存储组中有多少个相等对象的副本以及表示该组的特征对象作为键(这就是我通过整个对象作为 groupby 的键选择器)。
  • @NetMage 请参阅上面的编辑,了解我尝试执行您的建议。实施 IEquatable 后,我无法让 GroupBy 使用我的 Equals 方法来比较有关分组对象的键。
  • 来自IEquatable&lt;T&gt; 的文档:“如果您实现IEquatable&lt;T&gt;,您还应该覆盖Equals(Object)GetHashCode() 的基类实现,以便它们的行为与Equals(T) 方法。”你的测试类没有这样做。

标签: c# linq group-by overriding equals


【解决方案1】:

IEquatable&lt;T&gt; 的正确工作版本示例:

void Main() {
    var testList = new[] { 1, 1, 2, 3, 3 }.Select(n => new Test(n)).ToList();

    var result = testList.GroupBy(p => p);

    foreach (var group in result)
        Console.WriteLine($"Key value: {group.Key.Value}; Group count {group.Count()}");

    // Output
    //Key value: 1; Group count 2
    //Key value: 2; Group count 1
    //Key value: 3; Group count 2
}

class Test : IEquatable<Test> {
    public int Value { get; set; }

    public Test(int val) => Value = val;

    public override bool Equals(object obj) => obj is Test otherTest && this.Equals(otherTest);
    public bool Equals(Test other) => other is not null && Value == other.Value;
    public override int GetHashCode() => Value;
    public static bool operator==(Test aTest, Test bTest) => aTest is not null && aTest.Equals(bTest);
    public static bool operator!=(Test aTest, Test bTest) => !(aTest == bTest);
}

【讨论】:

  • 不幸的是,与我的实际实现相比,这个示例过于简单。我的原语的哈希码不能是它们的属性值,因为我必须通过它们的属性值和容差来比较两个对象,而不仅仅是相等。我已经意识到,我可能需要做一个单独的帖子,在其中寻求帮助,为正确的哈希码函数制定一个策略,该策略在比较我的对象的属性值时考虑到容差。目前我通过使用Dictionary&lt;ArcPrimitive, int&gt; 来生成正确的哈希码来解决这个问题。
  • @jperna7254 不问你需要回答的实际问题的危害。
  • @jperna7254 考虑一下,在不了解您的数据分布的情况下,我相信您能做的最好的事情是GetHashCode() =&gt; 1。对于使用容差的相等性,您可以有 a == bb == c,但不能有 a == c,这意味着您无法计算相等值的存储桶。
猜你喜欢
  • 2018-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-14
  • 2018-10-07
  • 2014-12-31
相关资源
最近更新 更多