第一次看到这章的标题有点懵,啥是合式类型,是一种值类型和引用类型之外的类型么,以前也没有听说过呀?其实并不是,合式类型其实说白了就是合适的类型,如何定义类型,如何操作类型才更好,如何创建合适的值类型和引用类型?
这一章的内容比较杂,基本上类似于基础部分的终结之章,回顾下之前学习的章节,1-5章介绍了结构性编程的基础知识,6-10章来介绍面向对象的内容,加上接下来11章对异常处理的延伸学习后,基本内容部分相当于结束了!
回顾一下万类始祖的Object所具有的虚方法,除了Finalize方法不能直接调用以外,我们有三个虚方法可以用来重写,用来比较对象的Equals和GetHashCode,以及用于返回字符串的ToString。
为啥要重写ToString,因为Object提供的默认ToString方法提供的是对当前类型的完全限定名输出,这个完全没有任何意义啊,我输出这个对象的字符串信息是想知道一些有用的信息,所以一定要重写。
例如我想输出这个坐标对象的具体坐标,就要通过重写的方式来获得:
重写ToString需要注意以下几条原则:
总而言之就是要返回有用信息并且千万别在重写的方法里抛异常。
GetHashCode方法是用来获取和对象对应的哈希码,有两种情况必须重写该方法:
要获得良好的性能实现需要参照以下重写规则(“必须”是指必须满足的要求,“性能”是指为了增强性能而需要采取的措施,“安全性”是指为了保障安全性而需要采取的措施):
)。
补充说明一点:高效的哈希表实现就是指哈希值可以良好的均匀的随机分布。
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。
这里的Longitude和Latitude都是只读自动属性,所以值不会变,如果是会发生改变的值,则应该对哈希码进行缓存,来满足生命周期内哈希码的唯一性原则。
重写Equal和重写GetHashCode有一些区别,这要从对象同一性和相等的对象值说起:
也就是同一个引用只是对象相等的一部分例子,两个对象实例的成员值部分或全部相等,也可以说它们相等。
例如重写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相等需要的场景也有很多,很多时候可以用于查重,如果不通过重写的方式验证相等则只能认定引用同一才是相等,这样通过不同方式创建的数据就都能逃过查重检验了。这里需要注意的两点:
了解了引用同一性和想等性我们来看看重写Equals的步骤吧:
可以通过如下代码实现来验证步骤:
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()检查。要注意如下设计规范:
说了这么多,实际上重写Equal就是为了达到标识数据相等的目的。这里为啥不用GetHashCode,因为这里没有缓存哦,所以不能百分百保证相等的对象就一定返回相等的hash,有可能因为缓存没有处理好导致其不等,实际上却是相等的。
其实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();
}
}
使用元组,所有的底层实现都由元组搞定,只需要标识用来比较的关键成员信息就行了。
实现操作符的过程称为操作符重载,不仅仅包括==和!=,还支持一些其它操作符,当然在使用的时候需要注意以下两点,防止出现误操作:
对于==和!=操作符而言,其操作行为可以直接委托给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#提供的操作符的优化,大多数时候其它封装类都定义好了这些,但是我们需要知道,转换是怎么做的,有什么好的方式。明白原理!