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

What is "Best Practice" For Comparing Two Instances of a Reference Type?

提问人:Rob Cooper 提问时间:9/20/2008 最后编辑:CommunityRob Cooper 更新时间:3/16/2019 访问量:34116

问:

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

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

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

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

^_^ 非常感谢

更新

看起来我读错了一些文档(这是漫长的一天),覆盖 Equals 可能是要走的路。

如果要实现引用 类型,您应该考虑覆盖 引用类型的 Equals 方法 如果类型看起来像基类型 例如 Point、String、BigNumber、 等等。大多数引用类型应 不重载相等运算符, 即使它们覆盖 Equals。然而 如果要实现引用 具有价值的类型 语义,例如复数 类型,您应该覆盖等式 算子。

C# .NET 比较 运算符重载 相等

评论

4赞 Flipster 11/19/2010
“大多数引用类型不应使相等运算符重载,即使它们覆盖了 Equals”?哇,我发现有点......呃......奇怪。所以 a.Equals(b) 可能是真的,而 a==b 可能是假的。如果我想知道引用是否相等(老实说,这很少),我会使用 .无论如何,ReferenceEquals(a,b)。我喜欢 a==b 返回与 a.Equals(b) 相同的结果。这难道不是“最佳实践”吗?
0赞 supercat 11/19/2012
@FlipScript:覆盖运算符的一个主要问题是它实际上是两个运算符;当它与存在重写的类型一起使用时,它使用重写;否则,如果操作数是引用类型,则为引用相等性检查。由于是静态绑定而不是虚拟绑定,因此即使与泛型一起使用,此行为也可能导致意外结果。在 vb.net 中,单独的运算符用于可重写的相等性和引用相等性,从而避免了这种歧义。====

答:

2赞 Paul Shannon 9/20/2008 #1

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

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

评论

0赞 Rob Cooper 9/20/2008
谢谢你,保罗。在 IComparable 界面上注明,尽管我认为在这种情况下它可能有点矫枉过正,因为我只想检查是否相等。
23赞 Matt J 9/20/2008 #2

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

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

评论

0赞 Rob Cooper 9/20/2008
好答案,谢谢。我很高兴您添加了关于为什么不重载相等运算符的内容。
4赞 Konrad Rudolph 9/20/2008
这实际上是 C# 的弱点之一。但是,只要实现者遵循准则,这就不是问题,因为 的语义不会因相等引用而更改。尽管如此,我还是发现自己在 C# 的危急情况下使用(VB 代替了)。==object.ReferenceEqualsIs
1赞 nawfal 12/17/2012
你不应该在两个地方写相等逻辑。不知道MS是怎么弄错的。.
3赞 bdukes 9/20/2008 #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 和 Equality 运算符的准则 (==)

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

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

1赞 mattlant 9/20/2008 #4

我倾向于使用 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

评论

0赞 Svish 3/12/2009
如何让 ReSharper 自动制作这些东西?
27赞 Konrad Rudolph 9/20/2008 #5

在 .NET 中正确、高效且不重复代码是很困难的。具体而言,对于具有值语义的引用类型(即将等式视为等式的不可变类型),应实现 System.IEquatable<T> 接口,并应实现所有不同的操作 (、 和 、)。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 创建的代码(发布在另一个答案中)容易受到不良行为的影响,因为并且会以不同的方式对待这种情况。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);
}

评论

2赞 Zach Burlingame 4/9/2009
对于类,当 System.Object 基类默认提供该功能时,为什么要重写相等和相等运算符来执行引用比较?
1赞 Konrad Rudolph 4/9/2009
始终执行和执行等效操作被认为是最佳实践。这反映在我的代码片段中。显然,只有在这些语义有意义时才使用它。但始终以一致的方式制作和执行。如果他们不这样做,那绝对是一种可用性恐怖。Equals==Equals==
1赞 Zach Burlingame 4/10/2009
为什么你认为 Equals 和 == 应该是一致的?这与 MSDN 文档所述的内容背道而驰,并且还会造成类似的断开连接,其中 == 不再表示引用相等。这会产生类似的可用性问题,因为此行为由 .NET 统一提供。
1赞 Zach Burlingame 4/10/2009
FWIW,我当然可以看到你来自哪里,尤其是我自己来自 C++ 世界。但是,由于 MSDN 文档/指南明确建议反对您正在做的事情,因此我正在寻找支持您立场的可靠论据。也许这值得它自己的问题。
1赞 Konrad Rudolph 10/2/2013
@nawfal我自己不再有代码,我也需要访问我的网络空间...... :(
17赞 Zach Burlingame 4/9/2009 #6

下面我总结了实现 IEquatable 时需要执行的操作,并提供了各种 MSDN 文档页面中的理由。


总结

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

理由

IEquatable(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) 返回 true。
  • x.Equals(y) 返回与 y.Equals(x) 相同的值
  • if (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。
  • x 的连续调用。等于 (y) 返回相同的值,只要 x 和 y 引用的对象未被修改。
  • x. 等于 (null) 返回 false(仅适用于不可为 null 的值类型。有关更多信息,请参见可以为 null 的类型(C# 编程指南)。
  • Equals 的新实现不应引发异常。
  • 建议重写 Equals 的任何类也重写 Object.GetHashCode。
  • 建议除了实现 Equals(object) 之外,任何类也为自己的类型实现 Equals(type),以提高性能。

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

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

== 运算符(C# 参考)

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

Object.Equals 方法 (Object)

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

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

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

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

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

其他陷阱

评论

1赞 supercat 4/4/2013
对 and 使用相同的名称可能是不幸的,因为在许多情况下,由于隐式类型转换,运算符和运算符都无法定义等价关系。如果我设计了 .net,该方法将被命名为 ,并且重写将使用更严格的等效标准。例如,我会指定它应该是 false,但应该是 true,因为这些值在数值上是相等的,即使它们不等Equals(Object)Equals(OwnType)Equals(OwnType)==ObjectEquivalentTo1.0m.EquivalentTo(1.00m)1.0m.Equals(1.00m)1.0m == 1.00m
0赞 nawfal 12/17/2012 #7

我相信获得像检查对象是否相等这样简单的事情有点棘手。NET的设计。

对于结构

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

2)由于您现在拥有自己的覆盖,并且要与各种相等性检查覆盖保持一致。EqualsGetHashCodeobject.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。

对我来说,感觉像是价值平等,更像是方法的句法糖。写作比写作直观得多。我们很少需要检查引用相等性。在处理物理对象的逻辑表示的抽象层次中,这不是我们需要检查的东西。我认为有不同的语义,实际上可能会令人困惑。我认为它应该首先是为了价值平等和参考(或更好的名称)平等。我不想在这里认真对待 MS 指南,不仅因为它对我来说不自然,还因为重载 == 不会造成任何重大伤害。这与不覆盖非泛型或可以反咬的不同,因为框架不会在任何地方使用,但只有在我们自己使用它时才会使用。我不重载 ==!= 获得的唯一真正好处是与我无法控制的整个框架的设计保持一致。这确实是一件大事,所以可悲的是我会坚持下去==Equalsa == ba.Equals(b)==Equals==EqualsIsSameAsEqualsGetHashCode==

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

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)过载和匹配。确保它适用于 null 值==!=Equals

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...");
    }
}

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

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

评论

1赞 supercat 1/4/2013
重写引用类型的运算符的一个主要问题(恕我直言,这是 C# 设计中的一个缺陷)是,在 C# 中实际上有两个不同的运算符,并且使用哪个运算符的决定是在编译时静态做出的。对于值类型,可以重载,以便在编译器接受 [ 并编译并产生 true,但不会编译] 的所有情况下测试值相等性。对于引用类型,这是不可能的;给定 ,s1==s2 和 o1==s1,但 o1!=s2。====4==4.0m4==4.04.0m==4.0var s1="1"; var s2=1.ToString(); Object o1 = s1;
1赞 Bob Bryan 9/20/2016 #8

Microsoft似乎已经改变了他们的调子,或者至少存在关于不重载相等运算符的相互矛盾的信息。根据这篇 Microsoft 文章,标题为“如何:定义类型的值相等性”:

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

根据 Eric Lippert 在回答我提出的关于 C# 中平等的最小代码的问题时,他说:

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

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

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

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

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

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

0赞 kofifus 7/2/2018 #9

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