第6章 通用对象操作
6.1 对象的等值性与唯一性
System.Object类型中的Equals方法是比较两个引用,如果指向的是同一个对象则返回true,否则在任何其他情况下都返回false
当重写自己的Equals方法时,必须确保它遵循以下4条规则:
1、Equals方法必须是自反的,也就是说x.Equals(x)必须返回true
2、Equals方法必须是对称的,也就是说x.Equals(y)和y.Equals(x)必须返回同样的值
3、Equals方法必须是可传递的,也就是说如果x.Equals(y)和y.Equals(z)都返回true,x.Equals(z)也必须返回true
4、Equals方法必须是前后一致的,也就是说如果两个对象的值没有发生改变那么多次调用Equals方法的返回值应该相同
6.1.1 为基类没有重写Object.Equals方法的引用类型实现Equals
为基类没有重写Object.Equals方法的引用类型实现Equals方法,应首先判断被比较的对象不为null,再判断比较的两个对象类型相同,之后将被比较的对象类型转换后分别比较两个对象的引用类型字段和值类型字段
比较引用类型字段应调用Object的静态Equals方法,参数为两个Object对象。该方法对出现null的情况做了正确的检测,而如果refobj为null时调用refobj.Equals(other.refobj)将会抛出NullReferenceException异常
比较值类型字段就应该调用该字段类型的Equals方法,而不应该调用Object的静态Equals方法,因为值类型对象的值永远不可能为null,且调用Object的静态Equals方法会对值类型对象执行装箱操作
6.1.2 为基类重写了Object.Equals方法的引用类型实现Equals
与基类没有重写Object.Equals方法相比,基类重写了Object.Equals方法的实现与其基本相同,只是通常需要在比较前首先调用base.Equals方法,先让基类型比较其中的字段
但如果调用base.Equals会导致直接调用Object.Equals方法则不应该再调用它,因为只有在两个引用指向同一个对象时Object.Equals方法才会返回true
6.1.3 为值类型实现Equals方法
System.ValueType.Equals方法在内部首先使用反射机制来得到类型的所有实例字段,再比较它们是否相等。这种比较的过程效率很低,但却是一个所有值类型都能继承的相当不错的默认实现
因为ValueType提供的Equals实现效率不高,所以我们也应该对定义的值类型提供自己的Equals实现。我们还应该为值类型定义一个强类型版本的Equals方法,让其接受自身的值类型作为参数,这样可以避免一些额外的装箱操作
6.1.4 Equals方法与==/!=操作符的实现总结
编译器对它认为的基元类型提供了==和!=操作符实现,也重写了Equals方法
对自己定义的引用类型,我们应重写Equals方法,如果其基类型没有继承Object.Equals方法的实现,那么我们应该调用基类型的Equals方法。如果愿意,我们还可以重载==和!=操作符让它们调用重写后的Equals方法
对值类型,我们应该为其定义一个类型安全的Equals来比较对象的状态,然后在实现“非类型安全”的Equals时在内部调用类型安全的版本,(译注说这样可能有隐式转换的问题,但是由于值类型是密封的,我还没有想到怎样在一个新的类型中定义一个和该类型之间的隐式转换)我们还应该重载==和!=操作符,让它们内部调用类型安全的Equals方法。
(为什么==和!=操作符对值类型是“应该”重载而引用类型是“可以”重载?“应该”重载是只是为了减少装拆箱操作还是有其他原因?“可以”重载表示什么情况下可以不重载?感觉不会用到就可以不重载?)
6.1.5 对象唯一性识别
Object的静态ReferenceEquals方法判断两个引用是否指向同一个对象。C#中我们也可以使用==来代替Object.ReferenceEquals方法,但==操作符只有两边的变量都为System.Object类型时才会正确的比较引用是否相同,所以一般不要用==来判断两个对象引用是否相同
注意对值类型对象x调用ReferenceEquals(x, x),结果会是false,因为x被两次装箱到不同的对象中去了
6.2 对象的散列码
System.Object提供了一个GetHashCode虚方法,使我们可以从任何对象上得到一个Int32类型的散列码
如果我们定义了一个类型并重写了Equals方法,那么我们也应该重写GetHashCode方法,因为System.Collections.Hashtable类型的实现要求任何两个相等的对象都必须有相同的散列码值,所以我们需要确保用来判等的算法和用来计算对象散列码的算法一致
6.3 对象克隆
有时我们会希望得到一个现有对象的拷贝,但对于某些类型如System.Threading.Thread,克隆其对象实例毫无意义,另外对于某些类型,当构造一个实例时该实例会被加入到一个链表或其他某种数据结构中,这时候简单的对象克隆可能会破坏该类型的语义
如果希望自己的实例被克隆,类应该实现ICloneable接口。该接口只定义了一个方法Clone,并且没有显示表明Clone方法应该实现一个浅拷贝还是深拷贝,所以我们必须自己决定。我们可以调用System.Object的受保护方法MemberwiseClone来实现浅拷贝