为什么在 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.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>
上调用的。在这里,您在 HashSet<Person>
上调用 Contains
并传入 IPerson
。 HashSet<Person>.Contains
要求参数为Person
; IPerson
不是有效参数。然而,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
}
后者可以节省一次类型转换,但这不太可能产生足够的性能差异来证明使用两种方法是合理的。
我知道在实施 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.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>
上调用的。在这里,您在 HashSet<Person>
上调用 Contains
并传入 IPerson
。 HashSet<Person>.Contains
要求参数为Person
; IPerson
不是有效参数。然而,HashSet<Person>
也是一个 IEnumerable<Person>
,并且由于 IEnumerable<T>
是协变的,这意味着它可以被视为一个 IEnumerable<IPerson>
,它具有 Contains
扩展接受 IPerson
作为参数的方法(通过 LINQ)。
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
}
后者可以节省一次类型转换,但这不太可能产生足够的性能差异来证明使用两种方法是合理的。