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

本文关键字:定义 什么 问题 GetHashCode 运算符 Equals | 更新日期: 2023-09-27 18:35:38

对于下面的代码

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()

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

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


一般来说,覆盖一个(但不是全部)是令人困惑的。 用户不希望使用相同的语义覆盖两者。

Microsoft对此状态的建议(除其他事项外):

  • 每当实现 Equals 方法时,都要实现 GetHashCode 方法。这使 Equals 和 GetHashCode 保持同步。

  • 每当实现相等运算符 (==) 时,重写 Equals 方法,并使它们执行相同的操作。

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

"重写特定类型的 Equals 方法以改进 该方法的性能和更贴近的概念 类型相等。

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

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

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

IMO,是显而易见的;如果你正在定义两个物体相等的含义,你应该包括所有可以检查两个物体相等的方法。请注意,编译器不会(通常不能)强制您实际遵循这些规则。它不会对操作员的主体进行复杂的代码分析,以查看他们是否已经模仿了Equals因为在最坏的情况下,这可能等同于解决停止问题。

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

如果你确实实现了Equals==不同的东西,编译器就不会抱怨;你会达到C#愿意尝试阻止你做蠢事的极限。它愿意阻止你意外地在你的代码中引入微妙的错误,但如果这是你想要的,它会让你故意这样做。

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

if (a == b)
{
    var tbl = new HashTable();
    tbl.Add(a, "Test");
    var s = tbl[b];
    Debug.Assert(s.Equals("Test"));
}

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

我猜你会收到这些警告,因为编译器不知道你在==方法中使用了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 为,但 B2 为假

--编辑--

现在假设你想这样做

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;
}

你会得到你想要的。

编译器只是警告您可能会面临类似的情况

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

因此,如果您有两个ID为63但名字不同的人,他们是否相等?

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

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

基本上,编译器告诉你要做一个好公民,并明确你的意图。

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

阅读 MSDN 页面。

CS0660

CS0661

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

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

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;
    }
}

下面是一个示例。希望对您有所帮助。