48

我最近遇到了这个问题,到目前为止,我一直很高兴地覆盖相等运算符(==)和/或Equals方法,以查看两个引用类型是否实际上包含相同的数据(即看起来相同的两个不同实例)。

自从我更多地参与自动化测试(将参考/预期数据与返回的数据进行比较)以来,我一直在使用它。

在查看MSDN 中的一些编码标准指南时,我遇到了一篇反对它的文章。现在我明白这篇文章为什么这么说(因为它们不是同一个实例)但它没有回答这个问题:

  1. 比较两种引用类型的最佳方法是什么?
  2. 我们应该实现IComparable吗?(我还看到有人提到这应该只为值类型保留)。
  3. 有什么我不知道的接口吗?
  4. 我们应该自己动手吗?!

非常感谢^_^

更新

看起来我误读了一些文档(这是漫长的一天)并且覆盖Equals可能是要走的路..

如果您正在实现引用类型,如果您的类型看起来像 Point、String、BigNumber 等基本类型,则应考虑覆盖引用类型上的 Equals 方法。大多数引用类型不应重载相等运算符,即使它们覆盖 Equals也是如此。但是,如果您正在实现旨在具有值语义的引用类型,例如复数类型,则应覆盖相等运算符。

4

9 回答 9

26

在 .NET 中正确、高效且不重复代码地实现相等是很困难的。具体来说,对于具有值语义的引用类型(即,将等价视为相等的不可变类型System.IEquatable<T>,您应该实现接口,并且您应该实现所有不同的操作(Equals,GetHashCode==, !=)。

例如,这是一个实现值相等的类:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

    public override bool Equals(object obj) => Equals(obj as Point);

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

上面代码中唯一可移动的部分是粗体部分:第二行Equals(Point other)GetHashCode()方法。其他代码应保持不变。

对于不代表不可变值的引用类,不要实现运算符==!=。相反,使用它们的默认含义,即比较对象身份。

代码故意等同于派生类类型的对象。通常,这可能是不可取的,因为基类和派生类之间的相等性没有明确定义。不幸的是,.NET 和编码指南在这里不是很清楚。Resharper 创建的代码发布在另一个答案中,在这种情况下容易受到不良行为的影响,因为Equals(object x)并且Equals(SecurableResourcePermission x) 会以不同的方式处理这种情况。

为了改变这种行为,必须在Equals上面的强类型方法中插入额外的类型检查:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}
于 2008-09-19T18:27:11.260 回答
23

看起来你正在用 C# 编码,它有一个名为 Equals 的方法,你的类应该实现,如果你想使用其他度量来比较两个对象,而不是“这两个指针是”(因为对象句柄就是指针)相同的内存地址?”。

我从这里获取了一些示例代码:

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Java 有非常相似的机制。equals()方法是Object类的一部分,如果你想要这种类型的功能,你的类会重载它。

重载 '==' 对对象来说可能不是一个好主意的原因是,通常,您仍然希望能够进行“这些指针是否相同”的比较。例如,这些通常依赖于将元素插入到不允许重复的列表中,并且如果此运算符以非标准方式重载,您的某些框架内容可能无法工作。

于 2008-09-19T18:13:21.217 回答
16

下面我总结了实现 IEquatable 时需要做的事情,并从各种 MSDN 文档页面中提供了理由。


概括

  • 当需要测试值相等时(例如在集合中使用对象时),您应该为您的类实现 IEquatable 接口、覆盖 Object.Equals 和 GetHashCode。
  • 当需要测试引用相等性时,您应该使用 operator==,operator!= 和Object.ReferenceEquals
  • 您应该只为ValueTypes和不可变引用类型覆盖 operator== 和 operator!= 。

理由

IEquatable

System.IEquatable 接口用于比较对象的两个实例是否相等。根据类中实现的逻辑比较对象。比较结果是一个布尔值,指示对象是否不同。这与 System.IComparable 接口相反,后者返回一个整数,指示对象值的不同之处。

IEquatable 接口声明了两个必须重写的方法。Equals 方法包含执行实际比较的实现,如果对象值相等则返回 true,否则返回 false。GetHashCode 方法应返回一个唯一的哈希值,该值可用于唯一标识包含不同值的相同对象。使用的散列算法类型是特定于实现的。

IEquatable.Equals 方法

  • 您应该为您的对象实现 IEquatable 以处理它们将存储在数组或通用集合中的可能性。
  • 如果您实现 IEquatable,您还应该重写 Object.Equals(Object) 和 GetHashCode 的基类实现,以便它们的行为与 IEquatable.Equals 方法的行为一致

覆盖 Equals() 和运算符 == 的指南(C# 编程指南)

  • x.Equals(x) 返回真。
  • x.Equals(y) 返回与 y.Equals(x) 相同的值
  • 如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。
  • x 的连续调用。只要 x 和 y 引用的对象没有被修改,等于 (y) 就会返回相同的值。
  • X。Equals (null) 返回 false(仅适用于不可为空的值类型。有关更多信息,请参阅可空类型(C# 编程指南)。)
  • Equals 的新实现不应该抛出异常。
  • 建议任何覆盖 Equals 的类也覆盖 Object.GetHashCode。
  • 建议任何类除了实现 Equals(object) 外,还为自己的类型实现 Equals(type),以提高性能。

默认情况下,运算符 == 通过确定两个引用是否指示同一个对象来测试引用是否相等。因此,引用类型不必实现 operator == 即可获得此功能。当类型是不可变的,即实例中包含的数据不能更改时,重载运算符 == 来比较值相等而不是引用相等可能很有用,因为作为不可变对象,它们可以被视为与 long 相同因为它们具有相同的价值。在非不可变类型中覆盖 operator == 不是一个好主意。

  • 重载的运算符 == 实现不应抛出异常。
  • 任何重载运算符 == 的类型也应该重载运算符 !=。

== 运算符(C# 参考)

  • 对于预定义的值类型,相等运算符 (==) 如果其操作数的值相等则返回 true,否则返回 false。
  • 对于字符串以外的引用类型,== 如果它的两个操作数引用同一个对象,则返回 true。
  • 对于字符串类型,== 比较字符串的值。
  • 在 operator== 覆盖中使用 == 比较测试 null 时,请确保使用基对象类运算符。如果不这样做,将发生无限递归,从而导致堆栈溢出。

Object.Equals 方法(对象)

如果您的编程语言支持运算符重载,并且您选择为给定类型重载相等运算符,则该类型必须覆盖 Equals 方法。Equals 方法的此类实现必须返回与相等运算符相同的结果

以下指南用于实现值类型

  • 考虑覆盖 Equals 以获得比 ValueType 上 Equals 的默认实现所提供的性能更高的性能。
  • 如果重写 Equals 并且语言支持运算符重载,则必须为值类型重载相等运算符。

以下指南用于实现引用类型

  • 如果类型的语义基​​于该类型表示某些值这一事实,请考虑在引用类型上覆盖 Equals。
  • 大多数引用类型不能重载相等运算符,即使它们重写了 Equals。但是,如果您正在实现旨在具有值语义的引用类型,例如复数类型,则必须覆盖相等运算符。

额外的陷阱

于 2009-04-08T16:49:31.980 回答
3

那篇文章只是建议不要重写相等运算符(对于引用类型),而不是反对重写 Equals。如果相等检查不仅仅意味着引用检查,您应该在对象(引用或值)中覆盖 Equals。如果你想要一个接口,你也可以实现IEquatable(由泛型集合使用)。但是,如果您确实实现了 IEquatable,则还应该覆盖 equals,正如 IEquatable 备注部分所述:

如果实现 IEquatable<T>,还应该重写 Object.Equals(Object) 和 GetHashCode 的基类实现,以使它们的行为与 IEquatable<T>.Equals 方法的行为一致。如果您确实重写了 Object.Equals(Object),那么在调用类上的静态 Equals(System.Object, System.Object) 方法时也会调用您重写的实现。这确保了 Equals 方法的所有调用都返回一致的结果。

关于是否应该实现 Equals 和/或相等运算符:

实现 Equals 方法

大多数引用类型不应该重载相等运算符,即使它们覆盖了 Equals。

来自实现等式和等式运算符的指南 (==)

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

这仅表示您需要在实现相等运算符时覆盖 Equals。它并没有说在重写 Equals 时需要重写相等运算符。

于 2008-09-19T18:15:27.687 回答
2

对于将产生特定比较的复杂对象,实现 IComparable 并在 Compare 方法中定义比较是一个很好的实现。

例如,我们有“车辆”对象,其中唯一的区别可能是注册号,我们使用它来比较以确保测试中返回的预期值是我们想要的。

于 2008-09-19T18:10:33.110 回答
1

我倾向于使用 Resharper 自动制作的东西。例如,它为我的一种引用类型自动创建了这个:

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

如果您想覆盖==并仍然进行 ref 检查,您仍然可以使用Object.ReferenceEquals.

于 2008-09-19T18:15:37.717 回答
1

微软似乎改变了他们的态度,或者至少有关于不重载相等运算符的相互矛盾的信息。根据这篇题为“如何:为类型定义值相等”的Microsoft 文章:

“== 和 != 运算符可以与类一起使用,即使类没有重载它们。但是,默认行为是执行引用相等检查。在类中,如果重载 Equals 方法,则应该重载== 和 != 运算符,但这不是必需的。”

根据 Eric Lippert 在他对我询问的关于C# 中相等的最小代码的问题的回答中- 他说:

“你在这里遇到的危险是你得到一个为你定义的 == 运算符,它默认引用相等。你很容易陷入重载的 Equals 方法确实值相等而 == 引用相等的情况,然后你不小心在值相等的非引用相等的事物上使用了引用相等。这是一种容易出错的做法,人工代码审查很难发现。

几年前,我研究了一种静态分析算法来统计检测这种情况,我们发现在我们研究的所有代码库中,每百万行代码中大约有两个实例的缺陷率。仅考虑在某处覆盖了 Equals 的代码库时,缺陷率显然要高得多!

此外,考虑成本与风险。如果您已经有 IComparable 的实现,那么编写所有运算符是微不足道的单行代码,不会有错误并且永远不会更改。这是您编写的最便宜的代码。如果要在编写和测试十几个小方法的固定成本与查找和修复使用引用相等而不是值相等的难以发现的错误的无限成本之间进行选择,我知道我会选择哪一个。”

.NET Framework 永远不会将 == 或 != 用于您编写的任何类型。但是,危险是如果其他人这样做会发生什么。因此,如果课程是为第 3 方准备的,那么我将始终提供 == 和 != 运算符。如果该类仅打算由组内部使用,我仍然可能会实现 == 和 != 运算符。

如果实现了 IComparable,我只会实现 <、<=、> 和 >= 运算符。只有在类型需要支持排序时才应实现 IComparable - 例如在排序或在有序通用容器(如 SortedSet)中使用时。

如果集团或公司制定了永远不实施 == 和 != 运算符的政策 - 那么我当然会遵循该政策。如果有这样的策略,那么明智的做法是使用 Q/A 代码分析工具来执行它,该工具在与引用类型一起使用时标记 == 和 != 运算符的任何出现。

于 2016-09-19T20:02:02.673 回答
0

我相信使用 .NET 的设计来获得像检查对象是否相等这样简单的东西有点棘手。

对于结构

1)实施IEquatable<T>。它显着提高了性能。

2)既然你现在有自己的Equals,覆盖GetHashCode,并与各种相等检查覆盖保持一致object.Equals

3) 重载==!=运算符不需要认真地完成,因为如果您无意地将一个结构与另一个等同于 a ,编译器会发出警告==or !=,但这样做是为了与Equals方法保持一致。

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

上课

来自女士:

大多数引用类型不应该重载相等运算符,即使它们覆盖了 Equals。

对我来说==,感觉就像价值平等,更像是Equals方法的语法糖。写作a == b比写作更直观a.Equals(b)。我们很少需要检查引用相等性。在处理物理对象的逻辑表示的抽象级别中,这不是我们需要检查的。我认为具有不同的语义==并且Equals实际上可能会令人困惑。我相信它首先应该是==为了价值平等和Equals参考(或更好的名字,比如IsSameAs)平等。我不想在这里认真对待 MS 指南,不仅因为它对我来说不自然,而且因为超载==不会造成任何重大伤害。这不像不覆盖非泛型Equals或者GetHashCode可以反击,因为框架不会==在任何地方使用,只有当我们自己使用它时。我从不重载==!=中获得的唯一真正好处是我无法控制的整个框架的设计保持一致。这确实是一件大事,所以很遗憾我会坚持下去

使用引用语义(可变对象)

1) 覆盖EqualsGetHashCode

2)实施IEquatable<T>不是必须的,但如果你有一个会很好。

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

具有值语义(不可变对象)

这是棘手的部分。不小心很容易搞砸。。

1) 覆盖EqualsGetHashCode

2)重载==!=匹配Equals确保它适用于 nulls

2)实施IEquatable<T>不是必须的,但如果你有一个会很好。

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

如果您的类可以被继承,请特别注意看看它应该如何处理,在这种情况下,您必须确定基类对象是否可以等于派生类对象。理想情况下,如果没有派生类的对象用于相等性检查,则基类实例可以等于派生类实例,在这种情况下,不需要在基类Type的泛型中检查相等性。Equals

通常注意不要重复代码。我本可以制作一个通用抽象基类(IEqualizable<T>或其他)作为模板,以便更轻松地重用,但遗憾的是在 C# 中这阻止了我从其他类派生。

于 2012-12-16T20:17:47.890 回答
0

上面的所有答案都没有考虑多态性,通常您希望派生引用使用派生 Equals,即使通过基本引用进行比较也是如此。请在此处查看问题/讨论/答案 -平等和多态性

于 2018-07-02T06:30:32.117 回答