C#如何重写Equals
在自定义struct的时候,经常需要重写Equals方法,原因是一方面ValueType内部使用反射进行各字段相等判断,效率比较低;另一方面在调用基类Equals的时候,会造成值类型的装箱,(详细可以参考《值类型的拆箱与装箱》),除非能够确定类型不会进行相等性判断,否则建议进行Equals重写来提高性能。
同一性与相等性
同一性是指两个变量引用的是内存中的同一个对象,C#使用Object.ReferenceEquals(obj,obj)进行判断;
相等性是指两个变量所包含的数值相等,一般来说是对各个字段进行值比较。从概念上来说,同一性比相等性更加严格,满足同一性必然满足相等性,但符合相等性不一定满足同一性。
什么时候需要重写Equals
- 自定义结构体,处于性能考虑,避免不必要的装箱和反射。
- class类型需要相等性而非同一性的比较,例如System.String类型,虽然是引用类型,但是两个string变量进行比较时,是进行相等性比较,而非引用。
重写Equals遵守的规则
- Equals必须自反:x.Equals(x)肯定返回true。
- Equals必须对称:x.Equals(y)和y.Equals(x)必须返回相同的值。
- Equals必须可传递:x.Equals(y)返回true,y.Equals(z)返回true,则x.Equals(z)肯定返回true。
- Equals必须一致。比较的两个值不变,Equals返回也不能变。
重写Object.Equals流程
- 重写GetHashCode方法。
- 实现System.IEquatable< T >接口的Equals方法,该接口允许定义类型安全的Equals方法。
- 重写Object.Equals,并在内部调用类型安全的Equals方法。
- 重载==和!=操作符,并在内部调用类型安全的Equals方法。
引用类型重写
这里实现Person类型的相等性重写,即如果Person.Id相等,则认为Person是相同的,如果需要进行同一性判断,需要使用Object.ReferenceEquals(obj,obj)进行判断。
internal class Program
{
public static void Main(string[] args)
{
Person p1= new Person("1");
Person p2= new Person("1");
//True
Console.WriteLine(p1==p2);
//True
Console.WriteLine(p1.Equals(p2));
//False
Console.WriteLine(ReferenceEquals(p1,p2));
Console.Read();
}
}
public class Person : IEquatable<Person>
{
private string _Id;
public Person(string id)
{
_Id = id;
}
public bool Equals(Person other)
{
//this非空,obj如果为空,则返回false
if (ReferenceEquals(null, other)) return false;
//如果为同一对象,必然相等
if (ReferenceEquals(this, other)) return true;
//对比各个字段值
if(!string.Equals(_Id, other._Id, StringComparison.InvariantCulture))
return false;
//如果基类不是从Object继承,需要调用base.Equals(other)
//如果从Object继承,直接返回true
return true;
}
public override bool Equals(object obj)
{
//this非空,obj如果为空,则返回false
if (ReferenceEquals(null, obj)) return false;
//如果为同一对象,必然相等
if (ReferenceEquals(this, obj)) return true;
//如果类型不同,则必然不相等
if (obj.GetType() != this.GetType()) return false;
//调用强类型对比
return Equals((Person) obj);
}
//实现Equals重写同时,必须重写GetHashCode
public override int GetHashCode()
{
return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
}
//重写==操作符
public static bool operator ==(Person left, Person right)
{
return Equals(left, right);
}
//重写!=操作符
public static bool operator !=(Person left, Person right)
{
return !Equals(left, right);
}
}
值类型重写
值类型必须实现相等性重写,因为两个值类型变量,不可能指向内存同一个对象,即Object.Reference(ValueType,ValueType)返回false。
internal class Program
{
public static void Main(string[] args)
{
CustomStruct p1 = new CustomStruct("1");
CustomStruct p2 = new CustomStruct("1");
//True
Console.WriteLine(p1 == p2);
//True
Console.WriteLine(p1.Equals(p2));
//Always False
Console.WriteLine(ReferenceEquals(p1,p2));
Console.Read();
}
}
public struct CustomStruct : IEquatable<CustomStruct>
{
private readonly string _Id;
public CustomStruct(string id)
{
_Id = id;
}
public bool Equals(CustomStruct other)
{
return string.Equals(_Id, other._Id, StringComparison.InvariantCulture);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is CustomStruct && Equals((CustomStruct) obj);
}
public override int GetHashCode()
{
return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
}
public static bool operator ==(CustomStruct left, CustomStruct right)
{
return left.Equals(right);
}
public static bool operator !=(CustomStruct left, CustomStruct right)
{
return !left.Equals(right);
}
}
关于GetHashCode
在重写Equals方法,而不重写GetHashCode方法,编译器会提示警告信息:“重写Object.Equals(object o)但不重写Object.GetHashCode()”,在使用Dictionary、HashMap类是,可能发生一些意想不到的Bug,还是针对上面的Person类(为了演示,先注释掉GetHashCode),编写一个示例来演示这种Bug。
using System;
using System.Collections.Generic;
internal class Program
{
public static void Main(string[] args)
{
Dictionary<Person,string> dic = new Dictionary<Person, string>();
Person p1 = new Person("Mike");
Person p2 = new Person("Mike");
dic.Add(p1,"p1 info");
//True
Console.WriteLine(p1==p2);
//True - Mike在dic当中
Console.WriteLine(dic.ContainsKey(p1));
//False - Mike不在dic当中
//这其实是不正确的,因为p1的值等于p2的值,将该值存入到dic当中,应该能够在利用该值再次读取出来
Console.WriteLine(dic.ContainsKey(p2));
Console.Read();
}
}
public class Person : IEquatable<Person>
{
private string _Id;
public Person(string id)
{
_Id = id;
}
public bool Equals(Person other)
{
//this非空,obj如果为空,则返回false
if (ReferenceEquals(null, other)) return false;
//如果为同一对象,必然相等
if (ReferenceEquals(this, other)) return true;
//对比各个字段值
if(!string.Equals(_Id, other._Id, StringComparison.InvariantCulture))
return false;
//如果基类不是从Object继承,需要调用base.Equals(other)
//如果从Object继承,直接返回true
return true;
}
public override bool Equals(object obj)
{
//this非空,obj如果为空,则返回false
if (ReferenceEquals(null, obj)) return false;
//如果为同一对象,必然相等
if (ReferenceEquals(this, obj)) return true;
//如果类型不同,则必然不相等
if (obj.GetType() != this.GetType()) return false;
//调用强类型对比
return Equals((Person) obj);
}
//实现Equals重写同时,必须重写GetHashCode
//这里先注释掉
/*
public override int GetHashCode()
{
return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
}
*/
//重写==操作符
public static bool operator ==(Person left, Person right)
{
return Equals(left, right);
}
//重写!=操作符
public static bool operator !=(Person left, Person right)
{
return !Equals(left, right);
}
}
造成这种问题的原因在于,在基于Key-Value的集合当中,会根据Key值来查找Value值,CLR内部会优化这种查找,实际上,最终是根据Key值得HashCode来查找Value值,示例当中p1虽然和p2值相等,但是Person类有没重写GetHashCode,所以最终调用System.Object的GetHashCode实现,默认System.Object的GetHashCode为不同对象返回不同的值,所以在使用p2进行查找时,不能找到对应的值。