C# - 为值类型重定义相等性
为什么要为值类型重定义相等性
原因主要有以下几点:
- 值类型默认无法使用 == 操作符,除非对它进行重写
- 再就是性能原因,因为值类型默认的相等性比较会使用装箱和反射,所以性能很差
- 根据业务需求,其实际相等性的意义和默认的比较结果可能会不同,但是这种情况可能不较少
所以建议是:所有供外部使用的struct都实现相等性。
实现步骤
- 重写object.Equals()方法
- 实现IEquatable<T>.Equals()接口方法
- 重写 == 和 != 操作符
- 重写object.GetHashCode()
具体来说:
重写object.Equals()方法,是避免了反射,因为System.ValueType里面对object.Equals()方法的重写实现如下:
这里用到了反射。
而实现IEquatable<T>.Equals()接口方法,可以避免装箱,并且保证类型安全。
而实现==和!=,也就允许值类型使用该操作符了,写起来更方便直观,易于理解。而且这两个操作符必须一同实现。
而重写object.GetHashCode(),则是一个最佳实践。
所有为值类型重定义相等性,一共分4步,每步都是必须的。
实现
先看实例struct:
有构造函数,涉及到一个enum,并重写了ToString()方法。
实现IEquatable<T>接口
首先来实现IEquatable<T>接口。
(如果你使用resharper或者Rider,那么实现该接口的时候它会自动把object的Equals和GetHashCode方法都重写了,并且自动完成了有意义的代码)
这里面我对三个属性进行了比较,使用了==操作符。其中==对于string来说就是比较值,而enum其实就是int,DateTime也是值类型,并且已经实现了相等性判断的功能。
重写object.Equals()方法
这个代码是resharper生成的。
代码很简单,首先检查是否为null,然后检查这个object是不是一个Person,这里使用了 is 操作符,并把它转型为Person,赋给了一个叫做other的变量。最后调用的这个Equals()方法,是我们上面写的那个强类型的方法,因为other变量的类型是Person。
但是这个方法仍然涉及到装箱操作,所以还是IEquatable<T>的实现方法更快一些。
如果只重写了object.Equals()方法,而没有重写GetHasCode()方法,那么resharper会有提示:
实现 == 和 != 操作符
这个很简单,直接调用强类型的Equals()方法即可,而且由于Person是值类型,所以不用检查null,值类型不会为null。
如果只实现了其中一个操作符,那么会报错的。
实现object.GetHashCode()
GetHashCode()这个方法会返回一个32位的哈希码,它代表着对象内容的哈希值。
而类型里拥有GetHashCode()方法(返回Hash)的真正目的是,允许该类型在内部使用HashTable的集合中可以作为Key,因为HashTable需要这些哈希码。例如Dictionary<TK, TV>。
为了让HashTable可以正确的工作,Hash码有一个要求:如果两个实例被认为是相等的,那么它们必须返回相同的hash码。如果没有实现这个要求,那么你可能会发现这个类型作为Dictionary的Key的时候,会有一些意想不到的结果。
所以如果重写了object.Equals()方法,那么就得重写object.GetHashCode()方法。
看一下resharper自动实现的代码:
这里使用了unchecked,防止抛出溢出异常。
Name是引用类型,可能为null,所以判断一下。
然后其它两个int和DateTime类型,微软都做好了其GetHashCode()的实现。
这里对它们进行异或操作。之所以使用397这个数,可能因为397是一个足够大的质数,可以导致溢出,并混淆各位,之所以使用质数,是因为用质数相乘会得到比用其他任意数相乘更均匀的结果。
检验
结果如预期,OK。
总结
在这几个动作里,实际的逻辑写在了IEquatable<T>.Equals()方法里,object.Equals()就是检查类型然后调用IEquatable<T>.Equals(),== 和 != 操作符也是调用IEquatable<T>.Equals(),而GetHashCode()则使用了按位异或。
最后再重复一次,为值类型定义相等性一定要实现上述4各步骤的5个方法。
- C/C++中 size_t 与C#中类似的类型定义
- C#与.NET程序员面试宝典 3.1.2 面试题21:C#支持哪几个预定义的引用类型
- C#预定义类型(Predefined types)
- 红皮书c#高级编程(第6版)_第2章_c#基础_预定义数据类型
- C#定义类型后带问号
- 深入讲解C#编程中嵌套类型和匿名类型的定义与使用
- C#高级编程学习日记之C#基础之变量和预定义数据类型
- C#定义泛型方法错误-类型“T”必须是引用类型才能用作泛型类型或方法“System.Data.Linq.Table<TEntity>”中的参数“TEntity”
- CSharp预定义引用类型—C#基础回顾
- C#系统预定义委托类型Action,Func
- 轻松学习C#的预定义数据类型
- C#中预定义的属性类型
- C#与Java对比学习:类型判断、类与接口继承、代码规范与编码习惯、常量定义
- c# 数据类型的默认值及如何定义空值
- C# 预定义基本类型 与 CTS对应
- C#变量定义中类型后带一个问号
- C#15个预定义类型—C#基础回顾
- C#枚举类型的定义,说明和使用
- clr via c#学习体会——clr如何调用一个类型中定义的方法(上)
- C#与Java对比学习:类型判断、类与接口继承、代码规范与编码习惯、常量定义