【问题标题】:Differences between IEquatable<T>, IEqualityComparer<T>, and overriding .Equals() when using LINQ on a custom object collection?在自定义对象集合上使用 LINQ 时 IEquatable<T>、IEqualityComparer<T> 和覆盖 .Equals() 之间的区别?
【发布时间】:2017-09-08 13:46:11
【问题描述】:

在比较自定义对象的两个集合时,我在使用 Linq 的 .Except() 方法时遇到了一些困难。

我从Object 派生了我的类,并实现了Equals()GetHashCode() 和运算符==!= 的覆盖。我还创建了一个CompareTo() 方法。

在我的两个集合中,作为调试实验,我从每个列表中取出第一项(这是重复的)并进行比较如下:

itemListA[0].Equals(itemListB[0]);     // true
itemListA[0] == itemListB[0];          // true
itemListA[0].CompareTo(itemListB[0]);  // 0

在所有三种情况下,结果都是我想要的。但是,当我使用 Linq 的 except() 方法时,重复的项目被 not 删除:

List<myObject> newList = itemListA.Except(itemListB).ToList();

了解 Linq 如何进行比较时,我发现了各种(冲突的?)方法,这些方法说我需要从 IEquatable&lt;T&gt;IEqualityComparer&lt;T&gt; 等继承。

我很困惑,因为当我从 IEquatable&lt;T&gt; 继承时,我需要提供一个新的 Equals() 方法,其签名与我已经覆盖的签名不同。我是否需要两个具有不同签名的此类方法,或者我是否应该不再从Object 派生我的类?

我的对象定义(简化)如下所示:

public class MyObject : Object
{
    public string Name {get; set;}
    public DateTime LastUpdate {get; set;}

    public int CompareTo(MyObject other)
    {
        // ...
    }

    public override bool Equals(object obj)
    {
        // allows some tolerance on LastUpdate
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 23 + Name.GetHashCode();
            hash = hash * 23 + LastUpdate.GetHashCode();
            return hash;
        }
    }

    // Overrides for operators
}

我注意到,当我从IEquatable&lt;T&gt; 继承时,我可以使用IEquatable&lt;MyObject&gt;IEquatable&lt;object&gt; 来实现;当我使用其中一个或另一个时,Equals() 签名的要求会发生变化。推荐的方式是什么?

我想要完成的工作:

我希望能够使用 Linq (Distinct/Except) 以及标准相等运算符(==!=)而无需重复代码。如果两个对象的名称相同并且LastUpdate 属性在几秒内(用户指定的)容差范围内,则比较应该允许两个对象被视为相等。

编辑:

显示GetHashCode() 代码。

【问题讨论】:

  • 一个类不是自动派生自Object吗? CompareTo 方法还需要 IComparable(T)
  • 我怀疑问题是您的GetHashCode() 逻辑存在缺陷。我认为您不需要执行任何特定的操作即可使其正常工作。可以发一下吗?
  • “允许一些容忍度”听起来你也可能会破坏传递性。
  • @Jon 这用于比较两台不同计算机上的项目(该类被序列化和传输)。彼此相差几秒钟的项目应被视为相同,以允许存在细微的时钟差异。也许另一种方法会更好?
  • @JYelton:正如我所说,这打破了传递性,其中 x.equals(y) 和 y.equals(z) 意味着 x.equals(z)。 x 和 y 可能会延迟 3 秒,y 和 z 可能会延迟 3 秒......所以 x 和 z 可能会延迟 6 秒。如果你的容忍度是 5 秒,那么 bang 就具有传递性。

标签: c# linq


【解决方案1】:

您是否覆盖object.Equalsobject.GetHashCode、实现IEquatable 或提供IEqualityComparer 都没有关系。它们都可以工作,只是方式略有不同。

1) 从object 覆盖EqualsGetHashCode

从某种意义上说,这是基本情况。它通常会起作用,假设您可以编辑类型以确保两种方法的实现符合要求。在很多情况下这样做并没有错。

2) 实现IEquatable

这里的关键是您可以(并且应该)实现IEquatable&lt;YourTypeHere&gt;。这与 #1 之间的主要区别在于您对 Equals 方法具有强类型,而不仅仅是让它使用 object。这对程序员来说更方便(增加了类型安全),也意味着任何值类型都不会被装箱,因此这可以提高自定义结构的性能。如果你这样做,你几乎应该总是这样做除了#1,而不是代替。在这里使用Equals 方法在功能上与object.Equals 不同将是......不好。不要那样做。

3) 实现IEqualityComparer

这与前两个完全不同。这里的想法是对象没有得到它自己的哈希码,或者看它是否等于其他东西。这种方法的重点是一个对象不知道如何正确地获取它的哈希值或查看它是否等于其他东西。也许是因为您不控制类型的代码(即第 3 方库)并且他们没有费心重写该行为,或者他们确实重写了它,但您只想要您自己对“平等”的独特定义这个特定的上下文。

在这种情况下,您创建一个完全独立的“比较器”对象,该对象接收两个不同的对象,并告知您它们是否相等,或者一个对象的哈希码是什么。使用此解决方案时无论EqualsGetHashCode 方法在类型本身中做什么,您都不会使用它。


请注意,所有这些都与 == 运算符完全无关,它是自己的野兽。

【讨论】:

  • 这有助于很多了解这些不同界面背后的目的。谢谢。
  • 除了#1之外只做#2是非常重要的。在不覆盖GetHashCode() 的情况下实现IEquatable&lt;&gt; 不会给出编译器警告(可悲),但它会导致在Dictionary&lt;YourTypeHere, X&gt;HashSet&lt;YourTypeHere&gt; 和许多其他情况下出现故障的类型(例如原始的Except 方法上面的问题,或Distinct 方法),其中隐式(或当然显式)使用了EqualityComparer&lt;YourTypeHere&gt;.Default 比较器。
  • @JeppeStigNielsen @Servy 为什么我在实现IEquatable 时要覆盖Equals? (我当然会覆盖GetHashCode()
  • @AlexanderDerck 如果有人使用一个类型,比如System.Collections.Hashtable,它直接使用Equals(object)GetHashCode(),而不知道或关心IEquatable&lt;&gt;类型怎么办?
  • @JeppeStigNielsen 是的,也找到了this good blog
【解决方案2】:

我在对象中用于相等的基本模式如下。请注意,只有 2 个方法具有特定于对象的实际逻辑。其余的只是提供给这两种方法的样板代码

class MyObject : IEquatable<MyObject> { 
  public bool Equals(MyObject other) { 
    if (Object.ReferenceEquals(other, null)) {
      return false;
    }

    // Actual equality logic here
  }

  public override int GetHashCode() { 
    // Actual Hashcode logic here
  }

  public override bool Equals(Object obj) {
    return Equals(obj as MyObject);
  }

  public static bool operator==(MyObject left, MyObject right) { 
    if (Object.ReferenceEquals(left, null)) {
      return Object.ReferenceEquals(right, null);
    }
    return left.Equals(right);
  }

  public static bool operator!=(MyObject left, MyObject right) {
    return !(left == right);
  }
}

如果您遵循此模式,则实际上无需提供自定义IEqualityComparer&lt;MyObject&gt;EqualityComparer&lt;MyObject&gt;.Default 就足够了,因为它将依赖 IEquatable&lt;MyObject&gt; 来执行相等检查

【讨论】:

  • 我的 GetHashCode 方法考虑了每个属性,而 Equals 方法提供了 LastUpdate 属性的容差。听起来 HashCode 生成也需要考虑到这一点,对吗?
  • @JYelton 是的。哈希码的实现应该只基于参与相等的值。如果哈希码是您的问题,一种调试方法是返回一个常量(比如 0)。如果您的代码以恒定返回值工作,那么您的哈希码实现中存在错误
  • 有几个问题。最糟糕的是==递归调用自己!
  • @JeppeStigNielsen doh,当我对 class 类型进行相等时,我总是会犯 == 错误。
  • “这里的实际相等逻辑”应该以if (Object.ReferenceEquals(other, null) || GetType() != other.GetType()) { return false; } 或类似开头。那是因为我们有一个不是sealed 的类。 this 可能比other 派生更多,或者other 可能比this 派生更多。在这种情况下,我们必须返回false。否则,如果有人从我们的非密封类派生,它将中断。
【解决方案3】:

您不能“在 LastUpdate 上允许一些容差”,然后使用使用 LastUpdate 的严格值的 GetHashCode() 实现!

假设this 实例在23:13:13.933 处具有LastUpdate,并且obj 实例具有23:13:13.932。那么这两个可能与您的宽容想法相当。但如果是这样,它们的哈希码必须是相同的数字。但这不会发生,除非你非常幸运,因为 DateTime.GetHashCode() 不应该为这两次提供相同的哈希值。

此外,您的 Equals 方法在数学上大多是传递关系。并且“大约等于”不能传递。它的传递闭包是标识一切的微不足道的关系。

【讨论】:

  • GetHashCode() 在平等方面的作用对我来说不是很清楚,但由于从这里的答案中获得了一些见解,所以很快就会为人所知,谢谢。
  • @JYelton 重要的规则是:如果 Equals 说两个实例“相等”,那么GetHashCode 必须为两者返回相同的数字。 (那么如果Equals 说它们不同,GetHashCode 可以(并且应该在尽可能多的情况下)给出不同的数字。)
猜你喜欢
  • 1970-01-01
  • 2016-04-24
  • 2013-08-09
  • 1970-01-01
  • 1970-01-01
  • 2011-10-20
  • 2011-09-11
  • 2011-12-27
  • 2011-02-13
相关资源
最近更新 更多