【问题标题】:What is "Best Practice" For Comparing Two Instances of a Reference Type?比较引用类型的两个实例的“最佳实践”是什么?
【发布时间】:2010-09-11 09:00:14
【问题描述】:

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

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

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

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

非常感谢^_^

更新

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

如果您正在实施参考 类型,您应该考虑覆盖 引用类型上的 Equals 方法 如果您的类型看起来像基本类型 例如 Point、String、BigNumber、 等等。大多数引用类型应该 不要重载 equality 运算符, 即使 如果它们覆盖 Equals。然而, 如果您正在实施参考 旨在具有价值的类型 语义,例如复数 类型,你应该覆盖平等 运算符。

【问题讨论】:

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

标签: c# .net comparison operator-overloading equality


【解决方案1】:

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

【讨论】:

  • 对于类,当 System.Object 基类默认提供该功能时,为什么要覆盖相等和不等运算符来执行引用比较?
  • 最佳实践是让Equals== 始终执行相同的操作。这反映在我的代码 sn-p 中。显然,只有在这种语义有意义的情况下才使用它。但始终让Equals== 以一致的方式执行。如果他们不这样做,那将是绝对的可用性恐怖。
  • 为什么你认为 Equals 和 == 应该是一致的?这违背了 MSDN 文档的规定,并且它还创建了一个类似的断开连接,其中 == 不再意味着引用相等。这会产生类似的可用性问题,因为这种行为是由 .NET 统一提供的。
  • FWIW,我当然可以看到你来自哪里,尤其是我自己来自 C++ 世界。但是,由于 MSDN 文档/指南明确建议您反对您正在做的事情,因此我正在寻找一个支持您立场的可靠论据。也许这值得它自己的问题..
  • @nawfal 我自己没有代码了,我也需要访问我的网络空间...... :(
【解决方案2】:

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

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

【讨论】:

  • 很好的答案,谢谢。我很高兴您添加了关于为什么 not 重载相等运算符的内容。
  • 这其实是C#的弱点之一。不过,只要实现者遵循指南,这不是问题,因为 == 的语义不会因相等的引用而改变。尽管如此,我发现自己在 C# 的危急情况下使用 object.ReferenceEquals(VB 使用 Is 代替)。
  • 你不应该在两个地方写相等逻辑。不知道 MS 是怎么弄错的..
【解决方案3】:

下面我总结了您在实现 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 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) 相同的值
  • 如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。
  • 连续调用 x。只要 x 和 y 引用的对象没有被修改,等号 (y) 就会返回相同的值。
  • x。 Equals (null) 返回 false(仅适用于不可为空的值类型。有关更多信息,请参阅Nullable Types (C# Programming Guide)。)
  • Equals 的新实现不应引发异常。
  • 建议任何覆盖 Equals 的类也覆盖 Object.GetHashCode。
  • 建议任何类除了实现 Equals(object) 外,还为自己的类型实现 Equals(type),以提高性能。

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

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

== Operator (C# Reference)

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

Object.Equals Method (Object)

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

以下指南用于实现值类型

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

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

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

其他陷阱

【讨论】:

  • Equals(Object)Equals(OwnType) 使用相同的名称可能是不幸的,因为在许多情况下,由于隐式类型转换,Equals(OwnType)== 运算符都不能定义等价关系。如果我设计了.net,Object 方法将被命名为EquivalentTo,并且覆盖将使用更严格的等效标准。例如,我会指定1.0m.EquivalentTo(1.00m) 应该为假,但1.0m.Equals(1.00m)1.0m == 1.00m 应该为真,因为这些值在数值上 相等,即使它们不等效 i>.
【解决方案4】:

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

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

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

来自Implementing the Equals Method

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

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

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

这只是说你需要在实现相等运算符时重写 Equals。它确实 not 说在覆盖 Equals 时需要覆盖相等运算符。

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

    • 谢谢你,保罗。注意到 IComparable 接口虽然我认为在这种情况下它可能会有点矫枉过正,因为我只想检查是否相等。
    【解决方案6】:

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

    【讨论】:

    • 如何让 ReSharper 自动生成这些东西?
    【解决方案7】:

    微软似乎改变了他们的态度,或者至少有关于不重载相等运算符的相互矛盾的信息。根据 Microsoft article 标题为 How to: Define Value Equality for a Type:

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

    根据 Eric Lippert 在他的 answer 中对我提出的关于 Minimal code for equality in C# 的问题 - 他说:

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

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

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

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

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

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

    【讨论】:

      【解决方案8】:

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

      对于结构

      1) 实现IEquatable&lt;T&gt;。它显着提高了性能。

      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。

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

      带有引用语义(可变对象)

      1) 覆盖EqualsGetHashCode

      2) 实现IEquatable&lt;T&gt; 不是必须的,但如果你有一个就好了。

      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确保它适用于空值

      2) 实现IEquatable&lt;T&gt; 不是必须的,但如果你有一个就好了。

      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 中的Type 相等性.

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

      【讨论】:

      • 一个 major 问题与覆盖引用类型的== 运算符(由于恕我直言,C# 设计中的一个缺陷)是在 C# 中实际上有两个不同的运算符,以及使用哪个运算符的决定是在编译时静态做出的。对于值类型,可以重载 == 以便它测试值相等在所有情况下编译器都会接受 [4==4.0m4==4.0 编译并产生 true,但 4.0m==4.0 不会'编译]。这对于引用类型是不可能的;给定var s1="1"; var s2=1.ToString(); Object o1 = s1;,s1==s2 和 o1==s1,但是 o1!=s2。
      【解决方案9】:

      以上所有答案均未考虑多态性,通常您希望派生引用使用派生 Equals,即使通过基本引用进行比较也是如此。请在此处查看问题/讨论/答案 - Equality and polymorphism

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-25
        • 1970-01-01
        • 1970-01-01
        • 2011-03-04
        • 1970-01-01
        相关资源
        最近更新 更多