了解了引用同一性和想等性我们来看看重写Equals的步骤吧:
- 检查是否为null--------不为null才能继续哦,否则没有比较的必要
- 如果是引用类型,就检查引用是否相等------引用同一则一定相等
- 检查数据类型是否相同
- 调用一个指定了具体类型的辅助方法,它的操作数是具体要比较的类型而不是object(例如代码清单10.5中的Equals(Coordinate obj)方法)
- 可能要检查哈希码是否相等来短路一次全面的、逐字段的比较---------相等的两个对象不可能哈希码不同,哈希码不同则一定不等,但是有时候散列不均匀或者没有缓存,则可能导致返回的hash值并非独一无二,所以不能依赖它判断亮哥对象是否相等。
- 如基类重写了Equals(),就检查base.Equals()
- 比较每一个标识字段(关键字段),判断是否相等
- 重写GetHashCode()
- 重写==和!=操作符(参见下一节)
可以通过如下代码实现来验证步骤:
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter10.Listing10_05 { using System; public class Program { public static void Main() { //... Coordinate coordinate1 = new Coordinate(new Longitude(48, 52), new Latitude(-2, -20)); // Value types will never be reference equal if(Coordinate.ReferenceEquals(coordinate1, coordinate1)) { throw new Exception( "coordinate1 reference equals coordinate1"); } Console.WriteLine( "coordinate1 does NOT reference equal itself"); } } public struct Coordinate : IEquatable<Coordinate> { public Coordinate(Longitude longitude, Latitude latitude) { Longitude = longitude; Latitude = latitude; } public Longitude Longitude { get; } public Latitude Latitude { get; } public override bool Equals(object obj) { // STEP 1: Check for null if (obj == null) { return false; } // STEP 3: Equivalent data types if (this.GetType() != obj.GetType()) { return false; } return Equals((Coordinate)obj); } public bool Equals(Coordinate obj) { // STEP 1: Check for null if a reference type // (e.g., a reference type) // if (obj == null) // { // return false; // } // STEP 2: Check for ReferenceEquals if this // is a reference type. // if ( ReferenceEquals(this, obj)) // { // return true; // } // STEP 4: Possibly check for equivalent hash codes. // if (this.GetHashCode() != obj.GetHashCode()) // { // return false; // } // STEP 5: Check base.Equals if base overrides Equals(). // System.Diagnostics.Debug.Assert( // base.GetType() != typeof(object) ); // if ( !base.Equals(obj) ) // { // return false; // } // STEP 6: Compare identifying fields for equality // using an overload of Equals on Longitude return ((Longitude.Equals(obj.Longitude)) && (Latitude.Equals(obj.Latitude))); } // STEP 7: Override GetHashCode public override int GetHashCode() { int hashCode = Longitude.GetHashCode(); hashCode ^= Latitude.GetHashCode(); // Xor (eXclusive OR) return hashCode; } public static bool operator ==( Coordinate leftHandSide, Coordinate rightHandSide) { return (leftHandSide.Equals(rightHandSide)); } public static bool operator !=( Coordinate leftHandSide, Coordinate rightHandSide) { return !(leftHandSide.Equals(rightHandSide)); } } public struct Longitude { public Longitude(int x, int y) { } } public struct Latitude { public Latitude(int x, int y) { } } }
该实现的前两个检查很容易理解。但注意如果类型密封,步骤3可以省略,步骤4~6在Equals()的一个重载版本中进行,它获取Coordinate类型的对象作为参数。这样在比较两个Coordinate对象时,就可完全避免执行Equals(object obj)及其GetType()检查。要注意如下设计规范:
- 要一起实现GetHashCode()、Equals()、==操作符和!=操作符,缺一不可。
- 要用相同算法实现Equals()、==和!=。
- 避免在GetHashCode()、Equals()、==和!=的实现中引发异常。
- 避免在可变引用类型(也就是Object)上重载相等性操作符(如重载的实现速度过慢,也不要重载)。
- 要在实现IComparable时实现与相等性相关的所有方法。
说了这么多,实际上重写Equal就是为了达到标识数据相等的目的。这里为啥不用GetHashCode,因为这里没有缓存哦,所以不能百分百保证相等的对象就一定返回相等的hash,有可能因为缓存没有处理好导致其不等,实际上却是相等的。
用元组重写GetHashCode()和Equals()
其实GetHashCode()和Equals()的主要作用就是克服Object呆板简单的判断,但是实现起来却很繁琐,需要对所有关键标识数据进行操作。对于Equals(Coordinate coordinate),可将每个标识(关键)成员合并到一个元组中,并将它们和同类型的目标实参比较:
public class TML : IEquatable<TML> { public TML(string name, string price, string number) { Name = name; Price = price; Number = number; } public string Name { get; } public string Price { get; } public string Number { get; } public bool Equals(TML tml) { return (Name, Price, Number).Equals((tml.Name, tml.Price, tml.Number)); } public override int GetHashCode() { return (Name, Price, Number).GetHashCode(); } }
使用元组,所有的底层实现都由元组搞定,只需要标识用来比较的关键成员信息就行了。
操作符重载
实现操作符的过程称为操作符重载,不仅仅包括==和!=,还支持一些其它操作符,当然在使用的时候需要注意以下两点,防止出现误操作:
- 赋值运算符=不支持重载
- 重载的操作符不能通过IntelliSense呈现,也就是不能智能提示。
- ==默认也只是执行引用相等性检查,所以为了保证和Equals的同一性,一定也要重写该操作符!
对于==和!=操作符而言,其操作行为可以直接委托给Equals:
==和!=重载
public sealed class ProductSerialNumber { public ProductSerialNumber( string productSeries, int model, long id) { ProductSeries = productSeries; Model = model; Id = id; } public string ProductSeries { get; } public int Model { get; } public long Id { get; } public bool Equals(ProductSerialNumber obj) { return ((obj != null) && (ProductSeries == obj.ProductSeries) && (Model == obj.Model) && (Id == obj.Id)); } public static bool operator ==( ProductSerialNumber leftHandSide, ProductSerialNumber rightHandSide) { if (ReferenceEquals(leftHandSide, null)) { return ReferenceEquals(rightHandSide, null); } return (leftHandSide.Equals(rightHandSide)); } public static bool operator !=( ProductSerialNumber leftHandSide, ProductSerialNumber rightHandSide) { return !(leftHandSide == rightHandSide); } }
这里需要注意的是,一定不要用相等性操作符执行空检查(leftHandSide==null)。否则会递归调用方法,造成只有栈溢出才会终止的死循环。相反,应调用ReferenceEquals()检查是否为空。
+和-重载
定义两个对象之间的+和-实际上也就是定义其关键数据的+和-:
public struct Coordinate { public Coordinate(Longitude longitude, Latitude latitude) { Longitude = longitude; Latitude = latitude; } public static Coordinate operator +( Coordinate source, Arc arc) { Coordinate result = new Coordinate( new Longitude( source.Longitude + arc.LongitudeDifference), new Latitude( source.Latitude + arc.LatitudeDifference)); return result; } public static Coordinate operator -( Coordinate source, Arc arc) { Coordinate result = new Coordinate( new Longitude( source.Longitude - arc.LongitudeDifference), new Latitude( source.Latitude - arc.LatitudeDifference)); return result; } }
转型操作符
怎么将值类型转换为一个不相干的引用类型呢?或者将值类型转换为一个不相干的结构,这需要定义一个转换器,例如转换double类型和高度类型Latitude:
public struct Latitude { public Latitude(double decimalDegrees) { DecimalDegrees = Normalize(decimalDegrees); } public double DecimalDegrees { get; } // ... public static implicit operator double(Latitude latitude) { return latitude.DecimalDegrees; } public static implicit operator Latitude(double degrees) { return new Latitude(degrees); } private static double Normalize(double decimalDegrees) { // here you would normalize the data return decimalDegrees; } }
说白了就是通过方法来换值,但是有一个需要注意的就是转换操作符implicit operator(隐式转换)
,explicit operator(显式转换)
,和之前的规范一样,如果判断是有损转换,一定声明为显式的,提醒操作者可能的精度丢失。
小小的总结一下,其实本章这两部分内容都是围绕着优化现有Object以及C#提供的操作符的优化,大多数时候其它封装类都定义好了这些,但是我们需要知道,转换是怎么做的,有什么好的方式。明白原理!