为什么在 Hashset 或其他集合中使用继承对象时 Equals(object) 胜过 Equals(T)?

Why does Equals(object) win over Equals(T) when using an inherited object in Hashset or other Collections?

我知道在实施 IEquatable<T>.Equals(T).

时我总是必须覆盖 Equals(object)GetHashCode()

但是,我不明白,为什么在某些情况下 Equals(object) 胜过通用 Equals(T)

例如,为什么会出现以下情况?如果我为接口声明 IEquatable<T> 并为其实现具体类型 X,则在将这些类型的项目相互比较时,一般 Equals(object) 会被 Hashset<X> 调用。在所有其他情况下,至少有一侧被转换为接口,正确的 Equals(T) 被调用。

这里有一个代码示例来演示:

public interface IPerson : IEquatable<IPerson> { }

//Simple example implementation of Equals (returns always true)
class Person : IPerson
{
    public bool Equals(IPerson other)
    {
        return true;
    }

    public override bool Equals(object obj)
    {
        return true;
    }

    public override int GetHashCode()
    {
        return 0;
    }
}

private static void doEqualityCompares()
{
    var t1 = new Person();

    var hst = new HashSet<Person>();
    var hsi = new HashSet<IPerson>();

    hst.Add(t1);
    hsi.Add(t1);

    //Direct comparison
    t1.Equals(t1);                  //IEquatable<T>.Equals(T)

    hst.Contains(t1);               //Equals(object) --> why? both sides inherit of IPerson...
    hst.Contains((IPerson)t1);      //IEquatable<T>.Equals(T)

    hsi.Contains(t1);               //IEquatable<T>.Equals(T)
    hsi.Contains((IPerson)t1);      //IEquatable<T>.Equals(T)
}
当没有提供比较器时,

HashSet<T> 调用 EqualityComparer<T>.Default 获取默认的相等比较器。

EqualityComparer<T>.Default 确定 T 是否实现了 IEquatable<T>。如果是,它使用那个,如果不是,它使用 object.Equalsobject.GetHashCode.

您的 Person 对象实现 IEquatable<IPerson> 而不是 IEquatable<Person>

当你有一个 HashSet<Person> 时,它最终会检查 Person 是否是一个 IEquatable<Person>,但它不是,所以它使用 object 方法。

当你有一个 HashSet<IPerson> 时,它会检查 IPerson 是否是一个 IEquatable<IPerson>,它是,所以它使用这些方法。


至于剩下的情况,为什么会这样:

hst.Contains((IPerson)t1);

调用 IEquatable Equals 方法,即使它是在 HashSet<Person> 上调用的。在这里,您在 HashSet<Person> 上调用 Contains 并传入 IPersonHashSet<Person>.Contains要求参数为PersonIPerson 不是有效参数。然而,HashSet<Person> 也是一个 IEnumerable<Person>,并且由于 IEnumerable<T> 是协变的,这意味着它可以被视为一个 IEnumerable<IPerson>,它具有 Contains 扩展接受 IPerson 作为参数的方法(通过 LINQ)。

当提供 none 时,

IEnumerable.Contains 也使用 EqualityComparer<T>.Default 来获取其相等比较器。在这个方法调用的情况下,我们实际上是在 IEnumerable<IPerson> 上调用 Contains,这意味着 EqualityComparer<IPerson>.Default 正在检查 IPerson 是否是 IEquatable<IPerson>,就是这样,所以 Equals 方法被调用。

虽然 IComparable<in T> 相对于 T 是逆变的,因此任何实现 IComparable<Person> 的类型都会自动被视为 IComparable<IPerson> 的实现,类型 IEquatable<T> 用于密封类型,尤其是结构。 Object.GetHashCode()IEquatable<T>.Equals(T)Object.Equals(Object) 一致的要求通常意味着后两种方法的行为应该相同,这反过来又意味着其中一个应该链接到另一个。虽然将结构直接传递给正确类型的 IEquatable<T> 实现与构造该结构的盒装堆对象类型的实例并让 Equals(Object) 实现复制结构数据,引用类型不存在这种性能差异。如果 IEquatable<T>.Equals(T)Equals(Object) 是等价的并且 T 是一个可继承的引用类型,那么

之间没有有意义的区别:
bool Equals(MyType obj)
{
  MyType other = obj as MyType;
  if (other==null || other.GetType() != typeof(this))
    return false;
  ... test whether other matches this
}

bool Equals(MyType other)
{
  if (other==null || other.GetType() != typeof(this))
    return false;
  ... test whether other matches this
}

后者可以节省一次类型转换,但这不太可能产生足够的性能差异来证明使用两种方法是合理的。