如果我实现了IEquatable <T>,那么重写Equals方法很重要吗?

8

我知道在实现自定义相等性检查时覆盖 GetHashCode 的重要性 - 我已经实现了 IEquality<T> 接口,并且我知道泛型和非泛型的 Equals 之间的区别 如此讨论. 现在,是否有必要重写 Equals(object t)?一切不都可以包含在泛型 Equals(T t) 中吗?

public override int GetHashCode() //required for hashsets and dictionaries
{
    return Id;
}

public bool Equals(T other) //IEquatable<T> here
{
    return Id == other.Id;
}

public override bool Equals(object obj) //required??
{
    return Equals(obj as T);
}

注意你的实现。在通用版本中,你应该考虑othernull的情况,因为如果other不是T类型,那么obj as T会返回null - Jamiec
@Jamiec 是的,这只是一个例子。我知道的。谢谢。 - nawfal
1
太好了,很高兴你意识到了这一点。我更多地考虑未来的访问者,他们可能会看到这个问题,并认为这是实现“IEquatable<T>”的正确方法,但它缺少了一个重要部分。 - Jamiec
3个回答

11
未密封类型不应实现IEquatable<T>,因为确保(甚至使之可能)派生类型正确实现它的唯一方法是实现IEquatable<T>.Equals(T)以调用Object.Equals(Object)。 因为IEquatable<T>的整个目的是避免在比较之前浪费CPU时间将东西转换为Object,所以调用Object.Equals(Object)的实现无法比未实现接口时实现的默认行为更快。

密封类类型可能通过实现IEquatable<T>来获得轻微的性能优势;首选风格是让Object.Equals(Object)尝试将参数强制转换为T;如果转换成功,则使用IEquatable<T>.Equals实现;否则返回false。 无论何种情况,当传递相同对象实例时,IEquatable.Equals(T)Object.Equals(Object)都不应产生不同的结果。

结构类型可以使用与密封类类型相同的模式。 尝试强制转换的方法略有不同,因为失败的转换不能返回null,但模式仍应相同。 如果结构实现了变异接口(例如List<T>.Enumerator,实现IEnumerator<T>的结构),则等式比较的正确行为有点模糊。

顺便提一下,IComparable<T>IEquatable<T>应视为彼此独立。 尽管当X.CompareTo(Y)为零时,X.Equals(Y)通常为真,但某些类型可能具有自然排序,其中两个事物可能不同而没有一个排在另一个之上。 例如,可以有一个NamedThing<T>类型,该类型将stringT组合在一起。 这种类型将支持名称的自然排序,但可能不支持T的排序。 名称匹配但T不同的两个实例应为CompareTo返回0,但对于Equals返回false。 因此,如果未更改Equals,覆盖IComparable不需要覆盖GetHashCode


总体来说,您的见解非常好,尽管不完全涉及问题,+1。我对您关于IEquatable仅适用于密封类型的想法有一点疑问。让我说我有一个未密封的Person:IEquatable <Person>,它具有其自己的Equals实现,只是return Id == other.Id,其中IdPerson类上的字段。现在,如果我将Person派生到Student,而Student没有覆盖任何内容,那么您对性能损失的看法是什么?还有关于再次使用非泛型Equals?您能否用上面的示例更详细地解释一下? - nawfal
我在想如果需要比较 Student 是否相等,那么 Person 类上相同的 Equals 方法会再次被使用吗?或者你是指将使用 Student 的默认非泛型引用相等性检查 Equals 方法? - nawfal
@nawfal:如果Student没有实现IEquatable<Student>,那么一个Student的哈希集合将使用虚拟的Object.Equals(Object)方法。如果它实现了IEquatable<Student>,那么它必须要么有这样的实现链到基类IEquatable<Person>.Equals方法,要么硬编码自己的该方法副本。前者会抵消实现IEquatable<Student>的任何性能优势,后者则会冒着如果IEquatable<Person>的行为发生变化而破坏事情的风险。在...使用IEquatable<T>的性能优势上... - supercat
结构体类型通常相当大。使用类类型可以获得的最大性能优势要小得多。即使在频繁使用的代码中获得了小的性能提升,如果不需要牺牲<i>任何</i>东西,那么这可能是值得的,但限制派生类的行为似乎并不是一个胜利。此外,我认为更清晰的设计可能是拥有一个密封类型,其中包含一个ID和可继承的“PersonInfo”对象,后者的比较方法将进行完整比较,前者对象将指定不变量... - supercat
具有相同ID的两个实例必须始终持有彼此比较相等的PersonInfo实例的引用。这将澄清ID和对象内容之间的关系,即使这样的派生添加更多字段,也将继续保持。 - supercat

7

当涉及到重载时,你应该覆盖Equals(object t)方法,否则当使用该重载时,你可能会得到错误的结果。你不能假设Equals(T other)是将被调用的重载。

如果你不覆盖它,引用相等性将被使用,这意味着像下面这样的东西会返回false:

myObject1.Id = 1;
myObject2.Id = 1;

myObject1.Equals((object)myObject2); // false!

另一个可能出现的问题是关于继承类 - 如果你将你的类型与继承类型进行比较,这很容易失败。


我在问非泛型Equals什么时候可以被调用?那泛型Equals有什么意义?抱歉,我感觉这个回答不完整。 - nawfal
@nawfal - 我在我的回答中添加了细节。 - Oded
好的,我现在明白了。但是这个示例很罕见,不过我理解了重点。 - nawfal
@nawfal - 这个例子是为了说明可能存在的问题。 - Oded
@nawfal “另一个可能的问题涉及继承类 - 如果您将您的类型与继承类型进行比较,这很容易失败。”为什么会这样?我原本认为如果您比较继承自T的类型,则应调用Equal(T)。 - Marwie

3

msdn

如果您实现了IEquatable,则还应该重写基类Object.Equals(Object)和GetHashCode的实现,以使它们的行为与IEquatable.Equals方法一致。如果您确实重写了Object.Equals(Object),则在调用类的静态Equals(System.Object, System.Object)方法时也会调用您重写的实现。此外,您还应该重载op_Equality和op_Inequality运算符。这确保了所有相等测试返回一致的结果。

我也可以进行更好的谷歌搜索。这里有一个JaredPar在MSDN博客上的好文章讨论了这个主题。

简而言之,重写Equals(object obj)

需要为对象类的静态Equals(object obj1, object obj2)方法,或者为ArrayList实例的Contains(object item)等方法。

由于链接更详细地讨论了这个问题,因此接受此内容。感谢Oded。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接