Почему Equals (объект) выигрывает над Equals (T) при использовании унаследованного объекта в Hashset или других коллекциях?
Мне известно, что при реализации 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)
}
Ответы
Ответ 1
HashSet<T>
вызывает EqualityComparer<T>.Default
, чтобы получить сопоставитель равенства по умолчанию, когда не предоставляется сопоставитель.
EqualityComparer<T>.Default
определяет, реализует ли T
IEquatable<T>
. Если это так, оно использует это, если нет, оно использует object.Equals
и object.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>
. Здесь вы вызываете Contains
на HashSet<Person>
и передаете . HashSet<Person>.Contains
требует, чтобы параметр был Person
; a IPerson
не является допустимым аргументом. Тем не менее, HashSet<Person>
также является IEnumerable<Person>
, а поскольку IEnumerable<T>
является ковариантным, это означает, что его можно рассматривать как IEnumerable<IPerson>
, который имеет метод расширения Contains
(через LINQ), который принимает IPerson
как параметр.
IEnumerable.Contains
также использует EqualityComparer<T>.Default
, чтобы получить свой сопоставитель по равенству, когда ни один не предоставлен. В случае вызова этого метода мы фактически вызываем Contains
на IEnumerable<IPerson>
, что означает, что EqualityComparer<IPerson>.Default
проверяет, является ли IPerson
IEquatable<IPerson>
, что и есть, так что метод Equals
называется.
Ответ 2
Хотя IComparable<in T>
является контравариантным относительно T
, так что любой тип, реализующий IComparable<Person>
, автоматически считается реализацией IComparable<IPerson>
, тип IEquatable<T>
предназначен для использования с закрытыми типами, особенно структур. Требование, чтобы Object.GetHashCode()
было согласуется как с IEquatable<T>.Equals(T)
, так и с Object.Equals(Object)
, как правило, подразумевает, что последние два метода должны вести себя одинаково, что, в свою очередь, подразумевает, что один из них должен быть привязан к другому. Хотя существует большая разница в производительности между передачей структуры непосредственно в реализацию IEquatable<T>
правильного типа, по сравнению с построением экземпляра структуры типа boxed-heap-object и с копией реализации 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
}
Последний может сохранить один тип, но это вряд ли позволит сделать достаточную разницу в производительности, чтобы оправдать наличие двух методов.