对象判等之Equals和GetHashCode
为每个类重写Equals和GetHashCode,如果暂时未想好怎么写或类不需要重写,还是建议去重写 throw new NotImplementedException();
不做Hash数据结构不需要实现GetHaskCode,若是,必须实现还有Equals。
实现引用类型的判等
| 步骤一:重写Equals() |
public class RefPoint
{
public int X { get; set; }
public int Y { get; set; }
public override bool Equals(object obj)
{
// 如果指向堆中同一个实例,必然相等
if (ReferenceEquals(this, obj))
{
return true;
}
// 如果obj是NULL或不是RefPoint类型或不是RefPoint的派生类,认为不相等
var point = obj as RefPoint;
if (point == null)
{
return false;
}
// obj必须是RefPoint类型,否则认为不相等
if (obj.GetType() != GetType())
{
return false;
}
// 只比较此类型定义的新增字段,不要比较从基类那里派生来的字段,留给base.Equals()去判断
if (X == point.X && Y == point.Y)
{
return true;
// return base.Equals(point); // RefPoint直接继承于Object,所以没必要比较从基类继承来的字段
}
return false;
}
}
这一步解决了同一性和相等性问题,类型能够以自定义逻辑进行判等。
解释下base.Equals(point)
使用base可以调用由于重写被隐藏的基类方法,在Equals()中调用base.Equals()有点递归的感觉,因为base.equals()同样会执行自己的逻辑,开头仍旧是同一性判断,类型判断,最后是调用base.base.Equals()。
值得说明的是,base.Equals()代码逻辑中的this和GetType()都会基于派生类解释,所以都能正常通过,后面的字段判等,其实就是利用基类中现成的方法,去判断派生类中从基类继承来的那部分字段,才是真正的目的。
| 步骤二:重载==和!= |
public class RefPoint
{
public int X { get; set; }
public int Y { get; set; }
public override bool Equals(object obj)
{
// 如果指向堆中同一个实例,必然相等
if (ReferenceEquals(this, obj))
{
return true;
}
// 如果obj是NULL或不是RefPoint类型或不是RefPoint的派生类,认为不相等
var point = obj as RefPoint;
if (point == null)
{
return false;
}
// obj必须是RefPoint类型,否则认为不相等
if (obj.GetType() != GetType())
{
return false;
}
// 比较此类型定义的新增字段
if (X == point.X && Y == point.Y)
{
return true;
// return base.Equals(point); // 直接继承Object,所以没必要比较从基类继承来的字段
}
return false;
}
// 第二个参数的类型个人建议是object而非RefPoint,这样才能和Equals完全等价,即可比较任意对象
public static bool operator ==(RefPoint p1, object p2)
{
return Equals(p1, p2);
}
// 第二个参数的类型个人建议是object而非RefPoint,这样才能和Equals完全等价,即可比较任意对象
public static bool operator !=(RefPoint p1, object p2)
{
return !Equals(p1, p2);
}
}
Object-Equals(object obj),==,!= 的语义相同,完全等价,所以应该遵守规范:若重写了Object-Equals(object obj),务必也重载运算符 == 和 !=。
重载==和!=的方法是静态方法,且至少有一个参数的类型是RefPoint,也就是运算符的宿主类型。在编译时,==和!=根据它的操作数类型确定调用哪一个方法。
| 步骤三:检查 |
自反性:x.Equals(x)肯定返回true
对称性:x.Equals(y)和y.Equals(x)返回相同的值
传递性:x.Equals(y)返回true,y.Equals(z)返回true,那么x.Equals(z)必须也返回true
一致性:x和y未变,那么x.Equals(y)的返回值也不能变
| 总结 |
(1)重写从Object继承的Equals方法
-
如果obj是null,返回false。因为能成功调用Equals的this必然不为null。
-
如果obj和this指向同一个实例,返回true,对于包含大量字段的实例,这一步有助于提升性能。
-
如果obj与this类型不一致,返回false。
-
比较类型定义的新增字段(不包括从基类继承的字段)是否相等,只要有一个字段不相等,就返回false。
-
类型字段=本类型定义的新增字段+基类字段,如果类型不是直接继承Object,则调用基类的Equals来比较继承自基类的字段,如果基类的Equals返回false,就返回false,否则返回true。
(2)重载运算符== 和 !=,内部使用 public static bool Equals(object object1,object object2)
(3)检查Equals是否满足4个特性。