什么是比较参考类型的两个实例的“最佳实践”?

问题描述 投票:44回答:9

我最近遇到过这种情况,直到现在我一直在愉快地重写等于运算符(==)和/或Equals方法,以便查看两个引用类型是否实际包含相同的数据(即两个看起来相同的不同实例)。

我一直在使用它,因为我已经进行了更多的自动化测试(比较参考/预期数据与返回的数据)。

在查看一些coding standards guidelines in MSDN时,我遇到了一个建议反对它的article。现在我理解为什么文章说这个(因为它们不是同一个实例)但它没有回答这个问题:

  1. 比较两种参考类型的最佳方法是什么?
  2. 我们应该实施IComparable吗? (我还看到提到这应该仅为值类型保留)。
  3. 有一些我不知道的界面吗?
  4. 我们应该自己动手吗?!

非常感谢^ _ ^

更新

看起来我错误地阅读了一些文档(这是漫长的一天)并且压倒Equals可能是要走的路。

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

c# .net comparison operator-overloading equality
9个回答
22
投票

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

我从here抓取了一些示例代码:

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类的一部分,如果您想要这种类型的功能,您的类会重载它。

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


26
投票

正确,高效地实现.NET中的相等性并且没有代码重复是很困难的。具体来说,对于具有值语义的引用类型(即immutable types that treat equvialence as equality),您应该实现the System.IEquatable<T> interface,并且您应该实现所有不同的操作(EqualsGetHashCode==!=)。

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

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创建的代码,发布in another answer,在这种情况下很容易受到不良行为的影响,因为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);
}

16
投票

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


摘要

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

理由

IEquatable

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

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

IEquatable.Equals Method

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

Guidelines for Overriding Equals() and Operator == (C# Programming Guide)

  • x.Equals(x)返回true。
  • x.Equals(y)返回与y.Equals(x)相同的值
  • if(x.Equals(y)&& y.Equals(z))返回true,则x.Equals(z)返回true。
  • 连续调用x。只要未修改x和y引用的对象,Equals(y)就会返回相同的值。
  • X。 Equals(null)返回false(仅适用于非可空值类型。有关更多信息,请参阅Nullable Types (C# Programming Guide)。)
  • Equals的新实现不应该抛出异常。
  • 建议任何覆盖Equals的类也会覆盖Object.GetHashCode。
  • 建议除了实现Equals(object)之外,任何类还为自己的类型实现Equals(type),以增强性能。

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

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

== Operator (C# Reference)

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

Object.Equals Method (Object)

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

以下准则用于实现值类型:

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

以下准则用于实现引用类型:

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

额外的陷阱


3
投票

该文章建议不要覆盖等于运算符(对于引用类型),而不是覆盖重写等号。如果相等检查不仅仅意味着检查,那么您应该在对象(引用或值)中覆盖Equals。如果需要接口,还可以实现IEquatable(由泛型集合使用)。但是,如果确实实现了IEquatable,则还应该重写equals,因为IEquatable备注部分指出:

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

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

来自Implementing the Equals Method

大多数引用类型不应重载等于运算符,即使它们重写等于。

来自Guidelines for Implementing Equals and the Equality Operator (==)

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

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


2
投票

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

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


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


1
投票

微软似乎已经改变了他们的调整,或者至少存在关于不重载相等运算符的冲突信息。根据这个题为“如何:为一种类型定义价值平等”的Microsoft article

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

根据Eric Lippert在他的answer中提出的一个问题,我询问了Minimal code for equality in C# - 他说:

“你遇到的危险就是你得到一个为你定义的==运算符,默认情况下引用相等。你可以很容易地在一个重载的Equals方法确实值相等并且==确实引用相等的情况下结束,然后你不小心在非参考相等的价值相等的东西上使用了引用相等。这是一种容易出错的做法,人类代码审查很难发现。

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

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

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

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

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


0
投票

我相信得到一些像检查对象一样简单正确的东西对于.NET的设计来说有点棘手。

对于Struct

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

2)因为你现在拥有自己的Equals,所以覆盖GetHashCode,并与各种相等检查一致,覆盖object.Equals

3)重载==!=运算符不需要虔诚地完成,因为如果你无意中将一个结构与另一个结构与==!=等同,编译器会发出警告,但这样做的好处是与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...");
    }
}

上课

来自MS:

大多数引用类型不应重载等于运算符,即使它们重写等于。

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

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

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)超载qazxsw poi和qazxsw poi以匹配qazxsw poi。确保它适用于空值。

2)实施==不是必须的,但如果你有一个,那就太好了。

!=

如果您的类可以继承,请特别注意它应该如何运行,在这种情况下,您必须确定基类对象是否可以等于派生类对象。理想情况下,如果没有派生类的对象用于相等性检查,那么基类实例可以等于派生类实例,在这种情况下,不需要检查基类的通用Equals中的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..."); } } 左右)作为模板,以便更容易重用,但遗憾的是在C#中阻止我从其他类派生。


0
投票

上面的所有答案都不考虑多态性,即使通过基本引用进行比较,通常也希望派生引用使用派生的Equals。请在这里查看问题/讨论/答案 - Type

© www.soinside.com 2019 - 2024. All rights reserved.