C#如何重写Equals
C# SortedMap类代码示例 - 纯净天空 (vimsky.com)
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进行查找时,不能找到对应的值。

浙公网安备 33010602011771号