为什么要为值类型重定义相等性

原因主要有以下几点:

  • 值类型默认无法使用 == 操作符,除非对它进行重写
  • 再就是性能原因,因为值类型默认的相等性比较会使用装箱和反射,所以性能很差
  • 根据业务需求,其实际相等性的意义和默认的比较结果可能会不同,但是这种情况可能不较少

所以建议是:所有供外部使用的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# - 为值类型重定义相等性的更多相关文章

  1. C++中的类型重定义

    发现重复定义是由于从两个不同的路径包含了同一个头文件而引起的,同事也建议从另外一个路径打开工程试试, 这才慢慢发现了原因.这个原因可能有些拗口,而事实上要出现这种错误也有些"曲折" ...

  2. sockaddr struct 类型重定义

    windows.h和winsock2.h有类型重定义我是知道的,本来就一个库来说没问题,把winsock2放到windows.h前或先定义WIN32_LEAN_AND_MEAN都能解决问题但现的出了问 ...

  3. error C2011: “timespec”:“struct”类型重定义

    error C2011: “timespec”:“struct”类型重定义 C++ pthread pthread.h 中的 timespec 和time.h 中的 结构定义重复了 ,同时两个头文件中 ...

  4. error C2011: “Picture”:“struct”类型重定义

    今天引用外来库时出现问题,也许是版本问题. 错误如下: .....\oursun\cincludes\quickdraw.h(309): error C2011: “Picture”:“struct” ...

  5. 多.h项目出现的问题:使用了预编译头依然出现error LNK2005:***obj已在***obj中定义与c++ error C2011: “xxx”:“class”类型重定义解决办法

    使用了预编译头依然出现error LNK2005:***obj已在***obj中定义 造成该问题的可能性比较多,本人将在今后遇到时添加进来,今天先放出本人遇到的一种情况. 多重包含含有变量定义的.h文 ...

  6. “sockaddr”: “struct”类型重定义的错误的解决办法《转》

    原帖地址:https://blog.csdn.net/clever101/article/details/100163301 windows.h和winsock2.h存在有类型重定义,往往体现在VC程 ...

  7. C# - 为引用类型重定义相等性

    通常情况下引用类型的相等性是不应该被重定义/重写的. 例如两个引用类型的变量 x 和 y,如果这样写:if(x == y) {...},那么大家都明白,这个比较的是引用的相等性. 但是有少数情况下,也 ...

  8. struct 类型重定义

    类型定义的那个头文件只需要在功能源文件里#include 开始在主函数源文件里也#include,所以出现了重定义

  9. class类型重定义,防止头文件重复加载

    今天调用自己写的一个类,出现了class类型重定义问题,上网查了相关资料,发现是头文件重复include引起的问题. 防止头文件重复加载: 系统那些头文件,无论怎么include都没事,因为一般都用了 ...

随机推荐

  1. jqery对于select级联操作

    问题:今天在做一个需求的时候,有一个级联操作也就是选中下拉框的一列就显示对对应的数据 处理:我在做级联的时候在option的列里面绑定click的事件发现这个事件行不通:查资料发现select触发的是 ...

  2. 图片与base64的互转

    /// <summary>        /// 把图片转换到文本信息        /// </summary>        /// <param name=&quo ...

  3. Flask入门之Virtualvenv的安装及使用(windows)

    Virtualvenv 提供一个特定的Python虚拟环境(沙盒),以便于那些要求特定版本的模块的脚本能够顺利运行. 因为在Virtualvenv中,我们可以使用 pip install -r req ...

  4. ImportError: numpy.core.multiarray failed to import

    1. ImportError: numpy.core.multiarray failed to import pip install -U numpy http://stackoverflow.com ...

  5. java 回调函数解读

    模块间调用 在一个应用系统中,无论使用何种语言开发,必然存在模块之间的调用,调用的方式分为几种: (1)同步调用 同步调用是最基本并且最简单的一种调用方式,类A的方法a()调用类B的方法b(),一直等 ...

  6. OOP编程七大原则

    OCP(Open-Closed Principle),开放封闭原则:软件实体应该扩展开放.修改封闭.实现:合理划分构件,一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里:一种可变性不应 ...

  7. Linux用户登录日志查询

    # 1 utmp.wtmp.btmp文件 Linux用户登录信息放在三个文件中: 1 /var/run/utmp:记录当前正在登录系统的用户信息,默认由who和w记录当前登录用户的信息,uptime记 ...

  8. [ Java面试题 ]泛型篇

    1.Java中的泛型是什么 ? 使用泛型的好处是什么? 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 好处: 1.类型安全,提供编译期间的类 ...

  9. Whitelabel Error Page 专题

    Spring boot为错误视图提供了如下错误属性:timestamp:错误发生的时间status:HTTP状态码error:错误原因exception:异常的类名message:异常消息(如果这个错 ...

  10. mvn -DskipTests和-Dmaven.test.skip=true区别

    在使用mvn package进行编译.打包时,Maven会执行src/test/java中的JUnit测试用例,有时为了跳过测试,会使用参数-DskipTests和-Dmaven.test.skip= ...