定义运算符 == 但不定义 Equals() 或 GetHashCode() 有啥问题?

Posted

技术标签:

【中文标题】定义运算符 == 但不定义 Equals() 或 GetHashCode() 有啥问题?【英文标题】:What's wrong with defining operator == but not defining Equals() or GetHashCode()?定义运算符 == 但不定义 Equals() 或 GetHashCode() 有什么问题? 【发布时间】:2012-06-03 03:48:15 【问题描述】:

下面的代码

public struct Person

    public int ID;
    public static bool operator ==(Person a, Person b)  return  a.Equals(b); 
    public static bool operator !=(Person a, Person b)  return !a.Equals(b); 

为什么编译器会给我这些警告? 不定义下面的方法有什么问题?

warning CS0660: 'Person' defines operator == or operator != but
    does not override Object.Equals(object o)

warning CS0661: 'Person' defines operator == or operator != but
    does not override Object.GetHashCode()

【问题讨论】:

我认为你是对的。 ==!= 运算符将不存在(它是 struct!)如果没有您定义它们。另一方面,显然,您将它们定义为与ValueType.Equals(Object) 的行为完全相同。因此,在不更改该方法的情况下覆盖该方法看起来会很奇怪。然而,编译器并没有意识到(也不检查)你的== 实现的主体完全等同于Equals,我想。 @JeppeStigNielsen:是的,我一开始也是这么想的,但后来我想:即使我确实覆盖了Equals(即编译器不能'不验证==的正文),所以这不可能是原因...... 警告不是由一个非常聪明的人发出的。你知道,我看到人们覆盖(和改变)Equals 而不覆盖 GetHashCode。编译器会警告他们。好的!然后他们在 VS 中输入:override Ge,他们会看到他们选择的完成。编辑为他们写了:public overide int GetHashCode() return base.GetHashCode(); 编译器不再警告 :-( 他们继续发送代码... 【参考方案1】:
public struct Coord

    public int x;
    public int y;

    public Coord(int x, int y)
    
        this.x = x;
        this.y = y;
    

    public static bool operator ==(Coord c1, Coord c2)
    
        return c1.x == c2.x && c1.y == c2.y;
    

    public static bool operator !=(Coord c1, Coord c2)
    
        return !(c1 == c2);
    

    public bool Equals(Coord other)
    
        return x == other.x && y == other.y;
    

    public override bool Equals(object obj)
    
        if (ReferenceEquals(null, obj)) return false;
        return obj is Coord && Equals((Coord) obj);
    

    public override int GetHashCode()
    
        return 0;
    

这是一个例子。希望对您有所帮助。

【讨论】:

【参考方案2】:

框架内普遍期望certain operations 应始终产生相同的结果。原因是某些操作(特别是排序和搜索,它们构成任何应用程序的很大一部分)依赖于这些不同的操作来产生有意义且一致的结果。在这种情况下,您打破了其中的几个假设:

如果在ab之间有一个有效的操作==,它应该产生与a.Equals(b)相同的结果 类似,如果在ab 之间有一个有效的操作!=,它应该产生与!a.Equals(b) 相同的结果 如果存在两个对象ab,其中a == b,则ab 在存储在哈希表中时应该产生相同的键。

前两个,IMO,很明显;如果您要定义两个对象相等的含义,您应该包括可以检查两个对象是否相等的所有方法。请注意,编译器不会(通常,不能)强制您实际遵循这些规则。它不会对操作符的主体执行复杂的代码分析,以查看它们是否已经模仿 Equals,因为在最坏的情况下,这可能等同于 solving the halting problem.

然而,它可以做的是检查您最有可能违反这些规则的情况,特别是您提供了自定义比较运算符并且没有提供自定义 Equals 方法。这里的假设是,如果您不希望他们做一些特殊的事情,您就不会费心提供操作员,在这种情况下,您应该为 all 需要的方法提供自定义行为同步。

如果您确实将Equals 实现为与== 不同的东西,编译器不会抱怨;你会达到 C# 愿意努力阻止你做一些愚蠢的事情的极限。它愿意阻止您在代码中意外引入细微的错误,但它会让您有意这样做,如果这是您想要的。

第三个假设与框架中的许多内部操作使用哈希表的某些变体这一事实有关。如果我有两个对象,按照我的定义,“相等”,那么我应该能够做到这一点:

if (a == b)

    var tbl = new HashTable();
    tbl.Add(a, "Test");

    var s = tbl[b];
    Debug.Assert(s.Equals("Test"));

这是哈希表的一个基本属性,如果它突然不正确会导致非常奇怪的问题。

【讨论】:

【参考方案3】:

我猜你收到这些警告是因为编译器不知道你在 == 方法中使用了 Equals

假设你有这个实现

public struct  Person

    public int ID;
    public static bool operator ==(Person a, Person b)  return Math.Abs(a.ID - b.ID) <= 5; 
    public static bool operator !=(Person a, Person b)  return Math.Abs(a.ID - b.ID) > 5; 

然后

 Person p1 = new Person()  ID = 1 ;
 Person p2 = new Person()  ID = 4 ;

 bool b1 = p1 == p2;
 bool b2 = p1.Equals(p2);

b1 将是 true,但 b2 false

--编辑--

现在假设你想这样做

Dictionary<Person, Person> dict = new Dictionary<Person, Person>();
dict.Add(p1, p1);
var x1 = dict[p2]; //Since p2 is supposed to be equal to p1 (according to `==`), this should return p1

但这会引发类似 KeyNotFound 的异常

但是如果你添加

public override bool Equals(object obj)

    return Math.Abs(ID - ((Person)obj).ID) <= 5; 

public override int GetHashCode()

    return 0;

你会得到你想要的。

编译器只是警告你可能会遇到类似的情况

【讨论】:

我一开始也是这么想的,但是,如果我覆盖这些方法,情况会如何变化? 您可以将Equals 方法实现为return Math.Abs(a.ID - b.ID) &lt;= 5;,然后您的所有代码都会保持一致。 好吧,我的意思是,如果你说问题是 "编译器不知道你在== 方法中使用了Equals",那么就是那个问题如果我覆盖 Equals 和/或 GetHashCode... 仍然存在,那么发生了什么变化?【参考方案4】:

编辑:此答案已得到更正,除其他事项外,请注意用户定义的值类型不会生成 ==,并提及 ValueType.Equals 的性能问题。


一般来说,覆盖一个,但不是全部,是令人困惑的。用户不希望被覆盖,或者两者都被覆盖,具有相同的语义。

Microsoft 的 recommendations 用于此状态(除其他外):

只要实现 Equals 方法,就实现 GetHashCode 方法。这使 Equals 和 GetHashCode 保持同步。

在实现相等运算符 (==) 时覆盖 Equals 方法,并使它们执行相同的操作。

在您的情况下,您有正当理由推迟到Equals(编译器不会自动实现==)并仅覆盖这两个(==/!=)。但是,仍然存在性能问题,因为 ValueType.Equals 使用反射:

"重写特定类型的 Equals 方法以改进 该方法的性能,更紧密地代表了概念 类型的相等性。”

因此,仍然建议最后覆盖所有 (==/!=/Equals)。当然,对于这个微不足道的结构,性能可能并不重要。

【讨论】:

对不起,我不明白......当我不覆盖它们时,语义有何不同? @Mehrdad,你是对的。在这种情况下,您不应该覆盖任何与相等相关的内容,因为您需要默认值。 “默认的 == 和 != 已经是你想要的了。” -- 问题是,这并不总是正确的。例如。如果字段是string,而不是int,则运算符==预定义。然而,在这种情况下,我的方法也没有任何问题。或者有吗? 但在这种情况下,原始海报已经确保 Equals(Object)== “做同样的事情”(你的报价)。 @Mehrdad,我错了。用户定义的值类型没有自动的==(无论字段如何)。【参考方案5】:

您需要做的就是将另一个成员添加到您的结构中,比如 Forename。

那么如果你有两个 ID 为 63 但名字不同的人,他们是否相等?

一切都取决于您想要实现的“相同”的定义。

使用更好的示例结构,编写一个 noddy 应用程序来执行各种方法,看看当你改变相等和/或等价的定义时会发生什么,如果它们不完全一致,你最终会得到类似 !( a == b) != (a != b),这可能是真的,但如果你不覆盖所有使用你的代码的方法,那么谁会想知道你的意图是什么。

基本上,编译器是在告诉你要成为好公民并明确你的意图。

【讨论】:

+1 最后一句话(假设它是正确的)回答了我的问题:你说这只是一个清晰度问题,而不是一个正确性问题。 不,它也是正确的。您的代码是正确的,因为您没有更改相同的含义。如果您没有覆盖其他两种方法,则它们会导致它们或它不正确,并且您的代码的用户将不得不猜测。如果我在同行评审你的代码,我会告诉你取消你的覆盖,或者质疑你的覆盖是否正确【参考方案6】:

如果您覆盖 EqualsGetHashCode,您甚至不需要覆盖运算符,这是一种更简洁的方法。 编辑:它应该可以工作,因为这是一个结构。

【讨论】:

您介意扩展您的第二点吗?它怎么不能正常工作? .Equals() 适用于值类型,但不适用于引用类型(类),它会尝试检查两个对象是否引用同一个实例,而不是内部的值(例如 id) 看看这个链接:***.com/questions/1502451/… ...您指的是该链接中的哪个解释? 我不认为有什么问题,因为它是一个结构,我收回它应该工作,即使没有覆盖等于。【参考方案7】:

阅读 MSDN 页面。

CS0660

CS0661

编译器基本上是在说:“既然你说知道如何比较你的对象,你就应该让它一直这样比较。”

【讨论】:

这只是在乞求我的问题:为什么重载==“暗示”我想覆盖这些方法? @Mehrdad,这并不是你真正想要的。这是你代码的调用者想要一致性,他们也不想要不必要的覆盖。 1.因为调用代码应该能够互换使用 == 和 Equals。 2. 如果你想要自定义平等,为什么不呢? @KendallFrey:为什么在我的示例中调用代码不能交替使用==Equals @Mehrdad:他们可以。这意味着覆盖 == 是浪费时间。【参考方案8】:

可能是因为默认的Equals() 方法对于实际系统来说不够好(例如,在你的类中它应该比较ID 字段)。

【讨论】:

但是不能指望编译器知道这已经足够好了。 我不明白你所说的“足够好”是什么意思...如果我不覆盖 EqualsGetHashCode,重载 ==!= 不会“足够好”? (能给我举个例子吗?)谢谢!

以上是关于定义运算符 == 但不定义 Equals() 或 GetHashCode() 有啥问题?的主要内容,如果未能解决你的问题,请参考以下文章

比较运算符compareTo()equals()==之间的区别与应用总结

java中equals和 == 的区别

equals()和==到底有啥区别啊?

java中的equals与==

数据类型与运算符小结(JAVA)

JAVA常见问题