什么是"最佳做法",为比较两个实例中引用类型的?
-
01-07-2019 - |
题
我碰到这个最近,直到现在我已经快乐的压倒一切的平等操作员(==)和/或 等于 方法,以便看看如果两个参考文献的类型实际上包含相同的 数据 (即两个不同的实例中,看起来相同)。
我一直使用这个甚至更多,因为我已经获得更多来自动化测试(比较基准/期望的数据,返回).
虽然寻找一些的 编码标准的指导方针在MSDN 我遇到了一个 文章 建议反对它。现在我明白了 为什么 该条是这样说的(因为他们是不一样的 实例),但是它没有回答这个问题:
- 什么是最好的方式比较两个参考的类型?
- 我们应该实现 类?(我也看见提及,这应该是保留价值的类型)。
- 有一些接口我不知道的吗?
- 我们应该只是滚了我们自己的?!
多谢了^_^
更新
看起来像我有误读的一些文件(这是一个漫长的一天)和压倒一切的 等于 可能会去..
如果你正在实施的参考 类型时,应当考虑压倒一切的 等方法上的基准类型 如果你看起来像一个基本类型 如一点,String,BigNumber, 等等。大多数参照类型应该 不过载 平等 操作员, 甚至 如果他们替代等于.但是, 如果你正在实施一个参考 类型,旨在以具有价值 语义,例如一个复杂的多 类型,你应该复盖的平等 操作员。
解决方案
看起来你正在使用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 类的一部分,如果你想要这种类型的功能,你的类会重载它。
重载'=='的原因对于对象来说可能是一个坏主意,通常,你仍然希望能够做出“这些是同一个指针”吗?比较。这些通常依赖于,例如,将元素插入到不允许重复的列表中,如果此运算符以非标准方式过载,则某些框架内容可能无效。
其他提示
正确,高效地实现.NET中的相等性和无代码重复很难实现。具体来说,对于具有值语义的引用类型(即将equatilence视为平等的不可变类型),你应该实现 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(指向其他)
中的第二行和 GetHashCode()
方法。其他代码应保持不变。
对于不表示不可变值的引用类,不要实现运算符 ==
和!=
。相反,使用它们的默认含义,即比较对象标识。
代码故意等同于派生类类型的偶数对象。通常,这可能不合适,因为基类和派生类之间的相等性没有明确定义。不幸的是,.NET和编码指南在这里并不十分清楚。 Resharper创建的代码,在另一个答案中,在这种情况下很容易出现意外行为,因为 Equals (对象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);
}
下面我总结出来你需要做什么时实现就不能修改和提供的理由各种MSDN文件的网页。
摘要
- 在测试对于价值的平等,是需要(例如在使用的对象在集)应该执行的就不能修改的接口,复盖的对象。平等和GetHashCode为您类。
- 当测试的参考平等是希望你应该使用算==,操!= 和 对象。ReferenceEquals.
- 你应该仅仅复盖操作者==和操作者!= 对于 Valuetype 和不可变的基准类型。
理由
该系统。就不能修改的接口是用来比较两个实例的对象的平等。的对象,比较的基础上实现的逻辑。比较结果在布尔的价值表示,如果对象是不同的。这是相对于该系统。型接口,返回的一个整数指示如何对象的价值观是不同的。
这就不能修改的界面宣布的两种方法必须重写。等方法包含实施,以执行实际的比较和返回的真实的,如果对象的价值是相等的,或假如他们不是。GetHashCode方法应该返回一个独特的散列值,可以使用的唯一标相同的对象包含不同的价值观。这种类型的散列算法的使用是实现特定的。
- 你应该实现就不能修改为对象来处理这种可能性,他们将被保存在一系列或一般收集。
- 如果实现就不能修改还应该复盖基类实现的对象。平等(目的)和GetHashCode使他们的行为是一贯的,就不能修改.平等的方法
- x.等(x)返回正确的。
- x.等(y)返回相同的价值为y。等(x)
- 如果(x。等(y)&&y。等(z))返回真的,那么x。等(z)返回正确的。
- 连续调用的x。等(y)返回相同的价值,只要该物体所引用的x和y不作修改。
- x.等(null)returns false(为非空值类型只。更多信息,请参阅 空类型(C#编程指南).)
- 新的执行等于不应该扔例外情况。
- 它建议的任何类替代等于也复盖的对象。GetHashCode.
- 是的是建议,除了实现平等(目标),任何一类,也实现平等(类型),为他们自己的类型,以提高性能。
默认情况下,操作者==试验供参考的平等,通过确定是否两个参考文献表示同样的对象。 因此,参照类型没有实施操作员==为了获得这一功能。当一个类型是不可改变的,也就是说,数据中包含的实例不能更改,载操作员==比较价值的平等,而不是参照平等可能是有用的,因为不可改变的对象,他们可以被认为是相同的,只要他们有同样的价值。 它不是一个好主意,以复盖操作者==非不可改变类型。
- 超负载操作员==的实现不应该扔例外情况。
- 任何类型的重载操作员==还应载操作者!=.
- 对于预先定义的价值的类型、平等操作员(==)返回真正的如果它的操作数都是平等的,假。
- 为参照其他类型比string,==返回真正的如果它的两个操作数指相同的对象。
- 为串的类型,==比较的价值观。
- 测试时,对使用空==比较内的运营商=="替代",确保使用的基本物流运营商。如果你不限递归将发生的导致一个计算器.
如果编程语言支持操作人员负载过重,如果你选择超载的平等操作者对于给定的类型,此类必须复盖等方法。这种实现平等的方法必须返回相同的结果的平等操作员
以下准则的实施 值的类型:
- 考虑的首要等到增强的性能,提供的默认实现平等的关ValueType.
- 如果复盖等和语言支持操作人员负载过重,必须载的平等操作人员对于你的价值的类型。
以下准则的实施 基准类型:
- 考虑的首要等上一个基准类型,如果该义的类型是基于这样的事实,表示一些值(s)。
- 大多数参照类型必须不超载的平等的操作者,即使他们复盖平等的。然而,如果实现的基准类型,目的是有价值的语义、例如一个复杂的多种类型,你必须复盖的平等操作员。
额外的陷阱
- 当压倒一切的GetHashCode()确保测试基准类型空前使用他们,在哈希码。
- 我遇到了一个问题与基于接口编程和操作人员超载的描述: 操作员负载过重,与基于接口编程在C#
该文章建议不要覆盖等于运算符(对于引用类型),而不是覆盖重写等号。如果相等检查不仅仅意味着检查,那么您应该在对象(引用或值)中覆盖Equals。如果你想要一个界面,你也可以实现 IEquatable (由通用集合)。但是,如果确实实现了IEquatable,则还应该重写equals,因为IEquatable备注部分指出:
如果实现IEquatable&lt; T&gt;,则还应覆盖Object.Equals(Object)和GetHashCode的基类实现,以使它们的行为与IEquatable&lt; T&gt; .Equals方法的行为一致。如果您重写了Object.Equals(Object),则在调用类上的静态Equals(System.Object,System.Object)方法时也会调用重写的实现。这确保了Equals方法的所有调用都返回一致的结果。
关于是否应该实现Equals和/或相等运算符:
来自实施平等方法
大多数引用类型不应重载相等运算符,即使它们重写等于。
每当实现相等运算符(==)时重写Equals方法,并使它们执行相同的操作。
这只表示每当实现相等运算符时都需要重写Equals。 不表示在重写Equals时需要覆盖相等运算符。
对于将产生特定比较的复杂对象,然后实现IComparable并在Compare方法中定义比较是一个很好的实现。
例如,我们有“车辆”。唯一差异可能是注册号的对象,我们使用它来进行比较,以确保测试中返回的期望值是我们想要的值。
我倾向于使用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
。
Microsoft似乎已经改变了他们的调整,或者至少存在关于不重载相等运算符的冲突信息。根据微软文章标题为如何:定义价值平等类型:
&quot; ==和!=运算符可以与类一起使用,即使该类没有超载它们。但是,默认行为是执行引用相等性检查。在类中,如果重载Equals方法,则应该重载==和!=运算符,但这不是必需的。“
根据Eric Lippert的回答我问过的问题 C#中平等的最小代码 - 他说:
&quot;你遇到的危险是你得到一个为你定义的==运算符,它默认引用相等。您可以很容易地在一个重载的Equals方法确实值相等并且==确实引用相等的情况下结束,然后您不小心在值相等的非引用相等的事物上使用引用相等。这是一种容易出错的做法,人工代码审查很难发现。
几年前,我使用静态分析算法来统计检测这种情况,我们发现在我们研究的所有代码库中,每百万行代码中有大约两个实例的缺陷率。当只考虑具有某些被覆盖的Equals的代码库时,缺陷率显然要高得多!
此外,考虑成本与风险。如果您已经拥有IComparable的实现,那么编写所有运算符都是微不足道的单行程序,它们不会有错误并且永远不会被更改。这是你要编写的最便宜的代码。如果在编写的固定成本和测试十几种微小方法之间做出选择,而不是找到并修复难以看到的错误的无限成本,其中使用引用相等而不是值相等,我知道我会选择哪一个。 ;
.NET Framework永远不会使用您编写的任何类型的==或!=。但是,如果其他人这样做会发生危险。因此,如果该类用于第三方,那么我将始终提供==和!=运算符。如果该类仅用于组内部使用,我仍然可能实现==和!=运算符。
如果实现了IComparable,我只会实现&lt;,&lt; =,&gt;和&gt; =运算符。只有当类型需要支持排序时才应该实现IComparable - 比如在排序或在SortedSet等有序通用容器中使用时。
如果集团或公司制定了一项政策,以便不实施==和!=运营商 - 那么我当然会遵循该政策。如果这样的策略到位,那么使用Q / A代码分析工具强制执行它是明智的,该工具在与引用类型一起使用时标记==和!=运算符的任何出现。
我认为在.NET的设计中,获取像检查对象一样简单正确的东西是有点棘手的。
对于Struct
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
方法的语法糖。编写 a == b
比编写 a.Equals(b)
更直观。我们很少需要检查参考平等。在处理物理对象的逻辑表示的抽象级别中,这不是我们需要检查的。我认为为 ==
和 Equals
设置不同的语义实际上可能令人困惑。我认为它应该是 ==
用于值相等,而 Equals
用于引用(或者更好的名称,如 IsSameAs
)首先是相等的。 我很想在此不认真对待MS指南,不仅因为它对我来说不自然,而且因为重载 ==
不会造成任何重大伤害。这与不重写非泛型 Equals
或 GetHashCode
不同,因为框架不会在任何地方使用 ==
,但仅限于我们自己用它。我从没有重载 ==
和!=
获得的唯一真正的好处将是整个框架的设计与我没有的一致性控制。这确实是一件大事,很遗憾我会坚持下去。
使用引用语义(可变对象)
1)覆盖 Equals
和 GetHashCode
。
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)覆盖 Equals
和 GetHashCode
。
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...");
}
}
如果您的类可以继承,请特别注意它应该如何运行,在这种情况下,您必须确定基类对象是否可以等于派生类对象。理想情况下,如果没有派生类的对象用于相等性检查,那么基类实例可以等于派生类实例,在这种情况下,不需要在泛型<中检查 Type
相等性代码>基类的等于。
一般注意不要重复代码。我可以创建一个通用的抽象基类( IEqualizable&lt; T&gt;
左右)作为模板,以便更容易重用,但遗憾的是在C#中阻止我从其他类派生。
上面的所有答案都没有考虑多态性,即使通过基准引用进行比较,通常也希望派生引用使用派生的Equals。请在此处查看问题/讨论/答案 - 平等和多态