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

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

自从我进行了更多的自动化testing之后,我一直在使用这个function(比较参考数据和预期数据)。

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

  1. 什么是比较两种参考types的最佳方法?
  2. 我们应该实现IComparable ? (我也曾经提到这只应该保留给价值types)。
  3. 有一些我不知道的界面吗?
  4. 我们应该推出自己的?!

非常感谢^ _ ^

更新

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

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

它看起来像你在C#中编写的,它有一个称为等于你的类应该实现的方法,如果你想比较两个对象使用一些其他的指标比这两个指针(因为对象句柄就是指针)相同的内存地址?“。

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

 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 == px) && (y == py); } 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 == px) && (y == py); } public override int GetHashCode() { return x ^ y; } } 

Java有非常相似的机制。 equals()方法是Object类的一部分,如果需要这种types的function,则类将重载它。

重载“==”的原因对于对象来说可能是一个坏主意,通常情况下,您仍然希望能够做到“这些指针是相同的”比较。 这些通常依赖于,例如,插入一个元素到列表中,不允许重复,如果这个操作符以非标准的方式被重载,那么你的一些框架的东西可能不起作用。

在.NET中正确有效地实现平等是非常困难的。 例如,你应该实现System.IEquatable<T>并且应该以一致和高效的方式实现所有不同的操作,而不用重复代码。 即使遵循指南,我也发现难以置信的难以记忆,所以我为C#和VB创build了一些片段来填充所有样板代码。 实际的比较逻辑只能在一个地方进行调整。 不幸的是,类和结构需要不同的处理。

作为一个例子,插入代码片段后的下面的类:

 class Point { #region Implementation of equality public bool Equals(Point other) { if (object.ReferenceEquals(other, null)) return false; // TODO Implement comparison logic here. Eg: // return X.Equals(other.X) && Y.Equals(other.Y); throw new Exception("The method or operation is not implemented."); } public override bool Equals(object obj) { return Equals(obj as Point); } public static bool operator ==(Point lhs, Point rhs) { return object.ReferenceEquals(lhs, rhs) || !object.ReferenceEquals(lhs, null) && lhs.Equals(rhs); } public static bool operator !=(Point lhs, Point rhs) { return !(lhs == rhs); } public override int GetHashCode() { // TODO Implement comparison logic here. Eg: // return X.GetHashCode() ^ Y.GetHashCode(); throw new Exception("The method or operation is not implemented."); } #endregion } 

片段可供下载 。 要使用它们,在类/结构体代码中inputcequals<tab> (对于类)或sequals<tab>来获得结构体。 下载是未签名的VSI社区组件安装程序包 。 如果您不信任内容,只需将文件扩展名更改为.zip然后手动打开/复制包含的文件。

重要

此代码有意(!)试图等同于派生类types的偶数对象。 通常情况下,这可能不是所希望的,因为基类和派生类之间的平等没有明确定义。 不幸的是,.NET和编码指南在这里不是很清楚。 Resharper创build的代码如下所示,在这种情况下容易受到不希望的行为,因为Equals(object x)Equals(SecurableResourcePermission x) 将以不同的方式处理这种情况。

为了改变这个,在上面的强typesEquals方法中必须插入一个额外的types检查:

 public bool Equals(Point other) { if (object.ReferenceEquals(other, null)) return false; if (other.GetType() != GetType()) return false; // TODO Implement comparison logic here. Eg: // return X.Equals(other.X) && Y.Equals(other.Y); throw new Exception("The method or operation is not implemented."); } 

下面我总结了在实现IEquatable时需要做什么,并从各种MSDN文档页面提供了理由。


概要

  • 当需要testing值相等时(比如在集合中使用对象时),应该为类实现IEquatable接口,重写Object.Equals和GetHashCode。
  • 当需要testing引用相等时,应该使用operator ==,operator!=和Object.ReferenceEquals 。
  • 您应该只覆盖运算符==和运算符!= ValueTypes和不可变引用types。

理由

IEquatable

System.IEquatable接口用于比较对象的两个实例是否相等。 基于类中实现的逻辑对对象进行比较。 比较结果是一个布尔值,表示对象是否不同。 这与System.IComparable接口相反,它返回一个指示对象值如何不同的整数。

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

IEquatable.Equals方法

  • 你应该为你的对象实现IEquatable来处理它们将被存储在数组或generics集合中的可能性。
  • 如果你实现了IEquatable,你也应该覆盖Object.Equals(Object)和GetHashCode的基类实现,以使它们的行为与IEquatable.Equals方法的行为一致

覆盖等于()和运算符的指导原则==(C#编程指南)

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

默认情况下,运算符==通过确定两个引用是否指示相同的对象来testing引用相等。 因此,引用types不必为了获得这个function而实现operator ==。 当一个types是不可变的,也就是说,实例中包含的数据是不能改变的,重载operator ==来比较值相等而不是引用相等可以是有用的,因为作为不可变对象,它们可以被认为是长因为它们具有相同的价值。 在非不可变types中重写operator ==不是一个好主意。

  • 重载的operator ==实现不应该抛出exception。
  • 任何重载operator ==的types都应该重载operator!=。

==运算符(C#参考)

  • 对于预定义的值types,如果操作数的值相等,则相等运算符(==)返回true,否则返回false。
  • 对于string以外的引用types,如果其两个操作数引用同一个对象,则==返回true。
  • 对于stringtypes,==比较string的值。
  • 在运算符==覆盖内使用==比较testingnull时,请确保使用基础对象类运算符。 如果你不这样做,将发生无限recursion导致一个计算器。

Object.Equals方法(对象)

如果您的编程语言支持运算符重载,并且您select为给定types重载相等运算符,则该types必须重写Equals方法。 Equals方法的这种实现必须返回与相等运算符相同的结果

以下准则用于实现值types

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

以下准则用于实现参考types

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

额外的陷阱

  • 重写GetHashCode()时,确保在使用哈希代码之前先testingNULL的引用types。
  • 我碰到了一个基于接口的编程和运算符重载的问题,这里描述: 在C#中使用基于接口的编程重载运算符

该文章只是build议不要覆盖等于运算符(对于引用types),而不是对等于覆盖等值。 如果相等性检查意味着超过引用检查,那么您应该在您的对象(引用或值)中覆盖“等于”。 如果你想要一个接口,你也可以实现IEquatable (被generics集合使用)。 但是,如果您实现了IEquatable,则还应该覆盖equals,如IEquatable注释部分所述:

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

关于你是否应该实施平等和/或平等运营商:

实施等值法

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

从实施平等和平等运行的指导原则(==)

每当你实现相等运算符(==)时重写Equals方法,并使它们做同样的事情。

这只是说,当你实现相等运算符时,你需要覆盖Equals。 当你覆盖Equals时, 并不是说你需要重写equals操作符。

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

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

我倾向于使用Resharper自动生成的内容。 例如,它为我的参考types之一autocreated这个:

 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; } } 

如果你想覆盖==并仍然执行引用检查,你仍然可以使用Object.ReferenceEquals

微软似乎已经改变了他们的调子,或者至less有关于不重载平等运算符的信息有冲突。 根据这篇微软文章标题为:如何为一种types定义价值平等:

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

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

“在这里遇到的危险是,你得到了一个为你定义的==运算符,默认情况下引用是相等的,当重载的Equals方法的值相等,==引用相等时,你不小心使用引用相等的方式来处理值不相等的东西,这是一个很容易出错的做法,很难被人类代码审查发现。

几年前,我使用静态分析algorithm来统计检测这种情况,我们发现在所有我们研究的代码库中,每百万行代码有大约两个实例的缺陷率。 当考虑只是在某些地方超过Equals的代码库时,缺陷率明显要高得多!

此外,考虑成本与风险。 如果你已经有了IComparable的实现,那么编写所有的操作符是不重要的一行,不会有错误,将永远不会改变。 这是你将要写的最便宜的代码。 如果在固定的写作成本和testing十几个微小的方法之间进行select,而在使用引用平等而不是价值平等的情况下find并修复一个难以察觉的错误的无限成本,我知道我会select哪一个。

.NET框架将不会使用==或!=与您编写的任何types。 但是,危险是如果有其他人会发生什么事情。 所以,如果class级是第三方,那么我总是会提供==和!=运算符。 如果这个类只打算在组内使用,我仍然可能实现==和!=运算符。

如果实现了IComparable,我只会实现<,<=,>和> =运算符。 IComparable应该只在types需要支持sorting的时候才能实现,比如SortedSet之类的sorting或通用容器。

如果这个团体或者公司制定了一个不执行==和!=操作符的策略 – 那么我当然会遵循这个策略。 如果这样的政策已经到位,那么使用Q / A代码分析工具强制执行它是明智的,该工具在与引用types一起使用时标记任何出现的==和!=运算符。

我相信,像.NET的devise一样,简单得像检查对象是否平等一样简单。

对于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:

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

对我来说==感觉像价值的平等,更像是一个Equals方法的语法糖。 写a == b比写a.Equals(b)更直观。 很less我们需要检查参考平等。 在处理物理对象的逻辑表示的抽象层次中,这不是我们需要检查的东西。 我认为==Equals有不同的语义,实际上可能会让人困惑。 我相信它应该是==为价值平等和平等参考(或更好的名字像IsSameAs )平等首先。 我不想在这里认真对待MS的指导原则,不仅仅因为这对我来说不是很自然,也因为重载==没有造成任何重大的伤害。 这不像不覆盖非genericsEqualsGetHashCode可以反弹,因为框架不使用==任何地方,但只有当我们自己使用它。 我从不重载==!=获得的唯一真正的好处将是我无法控制的整个框架的devise的一致性。 这确实是一件大事, 所以我会坚持下去

通过引用语义(可变对象)

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

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

请特别注意,如果您的类可以被inheritance,应该如何处理,在这种情况下,您将必须确定基类对象是否可以等于派生类对象。 理想情况下,如果没有派生类的对象用于相等性检查,那么基类实例可以等于派生类实例,在这种情况下,不需要在基类的generics等式中检查Type相等。

一般来说不要重复代码。 我可以做一个通用的抽象基类( IEqualizable<T>左右)作为模板,以便重新使用更容易,但令人遗憾的是在C#阻止我从其他类派生。