如何正确地重写相等性?

7

我还不熟悉运算符重载。我认为自己做得很好,直到遇到了这个问题。在!=运算符上抛出了NullReferenceException异常。我猜测它是在CompareTo方法中使用该运算符,但我不确定。如果有人能指导我正确的方向,我将非常感激。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            List<Task> tasks = new List<Task>();
            tasks.Add(new Task( "first",  DateTime.Now.AddHours(2)));
            tasks.Add(new Task( "second", DateTime.Now.AddHours(4)));
            tasks.TrimExcess();
            tasks.Sort();
        }
    }
    public class Task : IComparable
    {
        public Task()
        {
        }
        public Task(string nameIn, DateTime dueIn)
        {
            nameOfTask = nameIn;
            dateDue = dueIn;
        }
        DateTime dateDue;
        string nameOfTask;

        public static bool operator <(Task t1, Task t2)
        {
            return (t1.dateDue < t2.dateDue);
        }
        public static bool operator >(Task t1, Task t2)
        {
            return (t1.dateDue > t2.dateDue);
        }
        public static bool operator ==(Task t1, Task t2)
        {
            return (t1.dateDue == t2.dateDue);
        }
        public static bool operator !=(Task t1, Task t2)
        {
                return (t1.dateDue != t2.dateDue);

        }
        public override int GetHashCode()
        {
            return Int32.Parse(this.dateDue.ToString("yyyymmddhhmmss"));
        }
        public override bool Equals(System.Object obj)
        {
            if (obj == null) return false;    

            Task t = obj as Task;
            if ((System.Object)t == null) return false;
            return (this.dateDue == t.dateDue);
        }
        int IComparable.CompareTo(object obj)
        {
            if (obj == null) return 1;

            Task t = obj as Task;
            if (t != null)
            {
                return this.dateDue.CompareTo(t.dateDue);
            }
            else
                throw new ArgumentException("Object is not a Task");
        }
    }
}

当我注释掉二元运算符时,程序按预期运行。我的问题是如何保护我的二元运算符不受空引用的影响,以便我可以将它们用于手动比较? 感谢您的时间。

1
你正在使用 != 进行比较的其中一个 Task 对象被设置为 null - Sergey Kalinichenko
1
好的,你需要假设在所有这些静态函数中t1 和/或 t2 可能为空。 - Mike Christensen
3个回答

14
到目前为止,所给出的答案都是错误的。被接受的答案是错误的,因为它无意中递归了。另一个答案是错误的,因为它说null不等于null。
你所有运算符的实现都是错误的;它们需要正确处理空输入。
你对GetHashCode的实现是严重错误的;你试图将一个十四位数放入可以接受九位数的格式中。只需在日期上调用GetHashCode;没有必要通过这种繁琐的方式将其转换为字符串,然后再将其转换为数字!
编写代码的正确方法是使用object.ReferenceEquals进行引用比较,而不是使用==和!=运算符;这样做会很容易出现意外递归。
典型的模式如下:
public static bool operator ==(Task t1, Task t2)         
{
    if (object.ReferenceEquals(t1, t2)) return true;
    // All right. We know that they are (1) not the same object, and
    // (2) not both null. Maybe one of them is null.
    if (object.ReferenceEquals(t1, null)) return false;
    if (object.ReferenceEquals(t2, null)) return false;
    // They are not the same object and both are not null.
    return t1.dateDue == t2.dateDue;
}

public static bool operator !=(Task t1, Task t2)         
{
    // Simply call the == operator and invert it.
    return !(t1 == t2);
}

public override bool Equals(object t)
{
    return (t as Task) == this;   
}

public override int GetHashCode()
{
    return this.dateDue.GetHashCode();
}

其他比较运算符请自行练习。

+1并感谢修复!我想在发布之前应该在调试器中尝试甚至最简单的代码片段... - Sergey Kalinichenko
我明白了!谢谢大师,这解决了很多问题。我的问题是我在使用自己的逻辑来测试比较而不是尝试使用object.ReferenceEquals。感谢您的耐心和回答。 - Powe8525
1
为了更好的效果,我们可以实现 IEquatable<Task> 接口。这对于值类型来说更加重要,可以避免装箱,但是对于引用类型也没有坏处。 - CodesInChaos

3

看起来你正在用!=比较其中一个Task对象,而该对象被设置为null。内置的运算符!=比较引用并不会出错,但是你的运算符尝试取消引用任务,导致出错。

public static bool operator !=(Task t1, Task t2) {
    if (ReferenceEquals(t1, null)) {
        return !ReferenceEquals(t2, null); // return true only if t2 is *not* null
    }
    if (ReferenceEquals(t2, null)) {
        return true; // we know that t1 is not null
    }
    return (t1.dateDue != t2.dateDue);
}

当两个任务均为null时,此实现会返回false。您应该在==运算符中实现对称的空检查。


1
@Powe8525 当我提到你的 bool operator == 的行为需要对称于你的 bool operator != 时,我的意思是它应该将两个 null 值视为相等,并且 null 不应该等于非空值。答案中的 != 实现遵循这些规则,因此 == 的实现也应该遵循它们。 - Sergey Kalinichenko
1
@Powe8525:你是正确的;这里给出的代码有误,因为它会调用自身。正确的代码应该以 if (object.ReferenceEquals(t1, null)) { return !object.ReferenceEquals(t2, null); } 开始。 - Eric Lippert
@EricLippert 既然您在这个线程上,我能问您一个关于允许分别重载 ==!= 的理由的离题问题吗?我认为 Ada 的规则是当重载“等于”=时,隐式地重载“不等于”/=,并禁止重载/=,这是有道理的。 - Sergey Kalinichenko
1
@dasblinkenlight:我一直觉得这是一个有点奇怪的规则。你可以独立地覆盖==和!=,以便实现具有NaN类似语义的对象--它们等于无,不等于无。同样,该语言不试图强制执行正常的算术不变量;例如,x>=y不需要与x>y || x==y相同。 - Eric Lippert
@EricLippert 谢谢!我知道背后一定有原因,现在我知道是什么了。 - Sergey Kalinichenko
显示剩余3条评论

-4
 public static bool operator !=(Task t1, Task t2) 
        { 
              if (null == t1 || null == t2) { return false;}
                return (t1.dateDue != t2.dateDue); 

        } 

3
如果t1为null且t2不是null,你会期望这个语句返回true吗? - Mike Christensen
这只是一个例子。操作者应该足够聪明以定义自己的逻辑。 - Sam Axe
你还忘了在与null比较之前将t1/t2转换为object。而且,混淆了重要的特殊情况null == null,不能简单地说这只是一个例子就可以原谅。 - CodesInChaos
4
原帖作者提出这个问题是因为他们不知道如何正确地做。假设原帖作者“足够聪明”能够检测和修复您的错误,这是否认了问题的前提。 - Eric Lippert
这不是一个 bug,而是一个例子。两个 null 是否相等是一项实现细节,不能简单地假定。定义返回结果取决于 OP。OP 正在比较属性的相等性。他们没有给我们足够的信息来假设当一个或多个输入为空时正确的返回值是什么。 - Sam Axe

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