第一次看到这章的标题有点懵,啥是合式类型,是一种值类型和引用类型之外的类型么,以前也没有听说过呀?其实并不是,合式类型其实说白了就是合适的类型,如何定义类型,如何操作类型才更好,如何创建合适的值类型和引用类型?
这一章的内容比较杂,基本上类似于基础部分的终结之章,回顾下之前学习的章节,1-5章介绍了结构性编程的基础知识,6-10章来介绍面向对象的内容,加上接下来11章对异常处理的延伸学习后,基本内容部分相当于结束了!
重写Object的成员
回顾一下万类始祖的Object所具有的虚方法,除了Finalize方法不能直接调用以外,我们有三个虚方法可以用来重写,用来比较对象的Equals和GetHashCode,以及用于返回字符串的ToString。
重写ToSring
为啥要重写ToString,因为Object提供的默认ToString方法提供的是对当前类型的完全限定名输出,这个完全没有任何意义啊,我输出这个对象的字符串信息是想知道一些有用的信息,所以一定要重写。
例如我想输出这个坐标对象的具体坐标,就要通过重写的方式来获得:
重写ToString需要注意以下几条原则:
- 如需返回有用的、面向开发人员的诊断字符串,就要重写ToString()。
- 要使ToString()返回的字符串简短。
- 不要从ToString()返回空字符串来代表“空”(null)。
- 避免ToString()引发异常或造成可观察到的副作用(改变对象状态)。
- 如果返回值与语言文化相关或要求格式化(例如DateTime),就要重载ToString(string format)或实现IFormattable。
- 考虑从ToString()返回独一无二的字符串以标识对象实例。
总而言之就是要返回有用信息并且千万别在重写的方法里抛异常。
重写GetHashCode()
GetHashCode方法是用来获取和对象对应的哈希码,有两种情况必须重写该方法:
- 重写Equals()方法一定要重写GetHashCode(),否则编译器会有警告
- 将类作为hash表集合的键使用的时候也要重写GetHashCode()。
要获得良好的性能实现需要参照以下重写规则(“必须”是指必须满足的要求,“性能”是指为了增强性能而需要采取的措施,“安全性”是指为了保障安全性而需要采取的措施):
)。
- 必须:相等的对象必然有相等的哈希码(若a.Equals(b),则a.GetHashCode()==b.GetHashCode())。也就是相等的哈希码是对象相等性的必要不充分条件,对象相等则哈希码一定相等,哈希码相等对象不一定相等,还需要其它条件来满足
- 必须:在特定对象的生存期内,GetHashCode()始终返回相同的值,即使对象的数据发生了改变。许多时候应缓存方法的返回值,从而确保这一点。
- 必须:GetHashCode()不应引发任何异常;GetHashCode()总是成功返回一个值。
- 性能:哈希码应尽可能唯一。但由于哈希码只是返回一个int,所以只要一种对象包含的值比一个int能够容纳得多(这就几乎涵盖所有类型了),那么哈希码肯定存在重复。一个很容易想到的例子是long,因为long的取值范围大于int,所以假如规定每个int值都只能标识一个不同的long值,那么肯定剩下大量long值没法标识。
- 性能:可能的哈希码值应当在int的范围内平均分布。例如,创建哈希码时如果没有考虑到字符串在拉丁语言中的分布主要集中在初始的128个ASCII字符上,就会造成字符串值的分布非常不平均,所以不能算是好的GetHashCode()算法。
- 性能:GetHashCode()的性能应该优化。GetHashCode()通常在Equals()实现中用于“短路”一次完整的相等性比较(哈希码都不同,自然没必要进行完整的相等性比较了)。所以,当类型作为字典集合中的键类型使用时,会频繁调用该方法。
- 性能:两个对象的细微差异应造成哈希值的极大差异。理想情况下,1 bit的差异应造成哈希码平均16 bits的差异。这有助于确保不管哈希表如何对哈希值进行“装桶”(bucketing),也能保持良好的平衡性。
- 安全性:攻击者应难以伪造具有特定哈希码的对象。攻击手法是向哈希表中填写大量哈希为同一个值的数据。如哈希表的实现不高效,就易于受到DOS(拒绝服务)攻击。
补充说明一点:高效的哈希表实现就是指哈希值可以良好的均匀的随机分布。
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter10.Listing10_02
{
public struct Coordinate
{
public Coordinate(Longitude longitude, Latitude latitude)
{
Longitude = longitude;
Latitude = latitude;
}
public Longitude Longitude { get; }
public Latitude Latitude { get; }
public override int GetHashCode()
{
int hashCode = Longitude.GetHashCode();
// As long as the hash codes are not equal
if(Longitude.GetHashCode() != Latitude.GetHashCode())
{
hashCode ^= Latitude.GetHashCode(); // eXclusive OR
}
return hashCode;
}
public override string ToString()
{
return string.Format("{0} {1}", Longitude, Latitude);
}
}
public struct Longitude { }
public struct Latitude { }
}
这里使用了异或运算,如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
- 首先向来自相关类型的哈希码应用XOR操作符,并确保操作数不相近或相等(否则结果全零)
- 在操作数相近或相等的情况下,考虑改为使用移位(bit shift)和加法(add)操作。其他备选的操作符——AND和OR——具有类似的限制,但这些限制会发生得更加频繁。多次应用AND,会逐渐变成全为0;而多次应用OR,会逐渐变成全为1。
这里的Longitude和Latitude都是只读自动属性,所以值不会变,如果是会发生改变的值,则应该对哈希码进行缓存,来满足生命周期内哈希码的唯一性原则。
重写Equals()
重写Equal和重写GetHashCode有一些区别,这要从对象同一性和相等的对象值说起:
- 两个引用假如引用同一个实例,就说这两个引用是同一的。object(因而延展到所有派生类型)提供名为**ReferenceEquals()**的静态方法来显式检查对象同一性
- 引用同一性只是“相等性”的一个例子。两个对象实例的成员值部分或全部相等,也可以说它们相等。
也就是同一个引用只是对象相等的一部分例子,两个对象实例的成员值部分或全部相等,也可以说它们相等。
例如重写Equals方法后,就可以认为对象相等:
public class Program
{
public static void Main()
{
TML tml1 = new TML("PV", "1000", "09187234");
TML tml2 = tml1;
TML tml3 = new TML("PV", "1000", "09187234");
// 对象是不是引用同一
if (!TML.ReferenceEquals(tml1, tml2))
{
throw new Exception("serialNumber1 does NOT " + "reference equal serialNumber2");
}
// 不引用同一总相等吧
else if (!tml1.Equals(tml2))
{
throw new Exception("serialNumber1 does NOT equal serialNumber2");
}
else
{
Console.WriteLine(
"serialNumber1 reference equals serialNumber2");
Console.WriteLine(
"serialNumber1 equals serialNumber2");
}
// 对象是不是引用同一
if (TML.ReferenceEquals(tml1, tml3))
{
throw new Exception("serialNumber1 DOES reference " + "equal serialNumber3");
}
// 不引用同一总相等吧
else if (!tml1.Equals(tml3) ||tml1!= tml3)
{
throw new Exception("serialNumber1 does NOT equal serialNumber3");
}
Console.WriteLine("serialNumber1 equals serialNumber3");
}
}
public class 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; }
}
输出如下:
serialNumber1 reference equals serialNumber2
serialNumber1 equals serialNumber2
serialNumber1 equals serialNumber3
其中serialNumber1 和serialNumber2是引用同一,serialNumber1 和serialNumber3是重写Equals方法和!=操作符后的相等性验证通过,这个验证接下来会说到。注意这里的serialNumber1 和serialNumber3相等需要的场景也有很多,很多时候可以用于查重,如果不通过重写的方式验证相等则只能认定引用同一才是相等,这样通过不同方式创建的数据就都能逃过查重检验了。这里需要注意的两点:
- 只有引用类型才能使用ReferenceEquals方法判断,值类型的调用永远是false,因为值类型要调用该方法一定要装箱为object,而各自装箱产生的引用一定不是同一个。
- Object.Equals()的实现只是简单调用了一下ReferenceEquals方法,所以用处很有限。大多数情况下需要重写。
了解了引用同一性和想等性我们来看看重写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#提供的操作符的优化,大多数时候其它封装类都定义好了这些,但是我们需要知道,转换是怎么做的,有什么好的方式。明白原理!