一:背景

1. 讲故事

曾今在项目中发现有同事自定义结构体的时候,居然没有重写Equals方法,比如下面这段代码:

static void Main(string[] args){var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();var item = list.FirstOrDefault(m => m.Equals(new Point(int.MaxValue, int.MaxValue)));Console.ReadLine();}public struct Point{public int x;public int y;public Point(int x, int y){this.x = x;this.y = y;}}

这代码貌似也没啥什么问题,好像大家平时也是这么写,没关系,有没有问题,跑一下再用windbg看一下。


0:000> !dumpheap -stat
Statistics:MT    Count    TotalSize Class Name
00007ff8826fba20       10        16592 ConsoleApp6.Point[]
00007ff8e0055e70        6        35448 System.Object[]
00007ff8826f5b50     2000        48000 ConsoleApp6.Point0:000> !dumpheap  -mt 00007ff8826f5b50Address               MT     Size
0000020d00006fe0 00007ff8826f5b50       24     0:000> !do 0000020d00006fe0
Name:        ConsoleApp6.Point
Fields:MT    Field   Offset                 Type VT     Attr            Value Name
00007ff8e00585a0  4000001        8         System.Int32  1 instance                0 x
00007ff8e00585a0  4000002        c         System.Int32  1 instance                0 y

从上面的输出不知道你看出问题了没有?托管堆上居然有2000个Point,而且还可以用 !do 打出来,说明这些都是引用类型。。。这些引用类型哪里来的?看代码应该是 equals 比较时产生的,一次比较就有2个point被装箱放到托管堆上,这下惨了,,,而且大家应该知道引用对象本身还有(8+8) byte 自带开销,这在时间和空间上都是巨大的浪费呀。。。

二: 探究默认的Equals实现

1. 寻找ValueType的Equals实现

为什么会这样呢?我们知道equals是继承自ValueType的,所以把 ValueType 翻出来看看便知:

public abstract class ValueType{public override bool Equals(object obj){if (CanCompareBits(this)) {return FastEqualsCheck(this, obj);}FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);for (int i = 0; i < fields.Length; i++){object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);...}return true;}}

从上面代码中可以看出有如下三点信息:

<1> 通用的 equals 方法接收object类型,参数装箱一次。

<2> CanCompareBits,FastEqualsCheck 都是采用object类型,this也需要装箱一次。

<3> 有两种比较方式,要么采用 FastEqualsCheck 比较,要么采用反射比较,我去.... 反射就玩大了。

综合来看确实没毛病, equals 会把比较的两个对象都进行装箱。

2. 改进方案

问题找到了,解决起来就简单了,不走这个通用的 equals 不就行啦,我自定义一个equals方法,然后跑一下代码。

        public bool Equals(Point other){return this.x == other.x && this.y == other.y;}

可以看到走了我的自定义的Equals,????????。貌似问题就这样简单粗暴的解决了,真开心,打脸时刻开始。。。

三:真的解决问题了吗?

1. 遇到问题

很多时候我们会定义各种泛型类,在泛型操作中通常会涉及到T之间的 equals, 比如下面我设计的一段代码,为了方便,我把Point的默认Equals也重写一下。

class Program{static void Main(string[] args){var p1 = new Point(1, 1);var p2 = new Point(1, 1);TProxy<Point> proxy = new TProxy<Point>() { Instance = p1 };Console.WriteLine($"p1==p2 {proxy.IsEquals(p2)}");Console.ReadLine();}}public struct Point{public int x;public int y;public Point(int x, int y){this.x = x;this.y = y;}public override bool Equals(object obj){Console.WriteLine("我是通用的Equals");return base.Equals(obj);}public bool Equals(Point other){Console.WriteLine("我是自定义的Equals");return this.x == other.x && this.y == other.y;}}public class TProxy<T>{public T Instance { get; set; }public bool IsEquals(T obj){var b = Instance.Equals(obj);return b;}}

从输出结果看,还是走了通用的equals方法,这就尴尬了,为什么会这样呢?

2. 从FCL的值类型实现上寻找问题

有时候苦思冥想找不出问题,突然灵光一现,FCL中不也有一些自定义值类型吗?比如 int,long,decimal,何不看它们是怎么实现的,寻找寻找灵感, 对吧。。。说干就干,把 int32 源码翻出来。


public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
{public override bool Equals(object obj){if (!(obj is int)){return false;}return this == (int)obj;}public bool Equals(int obj){return this == obj;}
}

我去,还是int????????,貌似我的Point就比int少了接口实现,问题应该就出在这里,而且最后一个泛型接口IEquatable<int>特别显眼,看下定义:


public interface IEquatable<T>
{bool Equals(T other);
}

这个泛型接口也仅仅只有一个equals方法,不过灵感告诉我,貌似。。。也许。。。应该。。。就是这个泛型的equals是用来解决泛型情况下的equals比较。

3. 补上 IEquatable 接口

有了这个思路,我也跟FCL学,让Point实现 IEquatable<T>接口,然后在TProxy<T>代理类中约束下必须实现IEquatable<T>,修改代码如下:

public struct Point : IEquatable<Point> { ...  }public class TProxy<T> where T: IEquatable<T> { ... }

然后将程序跑起来,如下图:

????????,虽然是成功了,但有一个地方让我不是很舒服,就是上面的第二行代码,在 TProxy<T> 处约束了T,因为我翻看List的实现也没做这样的泛型约束呀,可能有点强迫症吧,贴一下代码给大家看看。


public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{}

然后我继续模仿List,把 TProxy<T> 上的T约束去掉,结果就出问题了,又回到了 通用Equals

4. 从List的Contains源码中寻找答案

好奇心再次驱使我寻找List中是如何做到的,为了能看到List中原生方法,修改代码如下,从Contains方法入手。

var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();var item = list.Contains(new Point(int.MaxValue, int.MaxValue));---------- outout ---------------
我是自定义的Equals
我是自定义的Equals
我是自定义的Equals
...

我也是太好奇了,翻看下 Contains 的源码,简化后实现如下。


public bool Contains(T item){...EqualityComparer<T> @default = EqualityComparer<T>.Default;for (int j = 0; j < _size; j++){if (@default.Equals(_items[j], item)) {return true;}}return false;
}

原来List是在进行 equals比较之前,自己构建了一个泛型比器EqualityComparer<T>,????????,然后继续追一下代码。

因为这里的runtimeType实现了IEquatable<T>接口,所以代码返回了一个泛型比较器:GenericEqualityComparer<T>,然后我们继续查看这个泛型比较器是咋样的。

从图中可以看到最终还是对T进行了IEquatable<T>约束,不过这里给提取出来了,还是挺厉害的,然后我也学的模仿一下:

可以看到也走了我的自定义实现,两种方式大家都可以用哈????????????。

最后要注意一点的是,当你重写了Equals之后,编译器会告知你最好也把 GetHashCode重写一下,只是建议,如果看不惯这个提示,尽可能自定义GetHashCode方法让hashcode分布的均匀一点。

四:总结

一定要实现自定义值类型的 Equals方法,人家的 Equals方法是用来兜底的,一次比较两次装箱,对你的程序可是双杀哦????????????。

自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧相关推荐

  1. .NET中的值类型与引用类型

    .NET中的值类型与引用类型 这是一个常见面试题,值类型(Value Type)和引用类型(Reference Type)有什么区别?他们性能方面有什么区别? TL;DR(先看结论) 值类型 引用类型 ...

  2. C#之值类型和引用类型

    本文意在巩固基础知识,并不是对其进行深入剖析,还望理解. 本文是对此项知识的整理文,有些内容来源于网络,其他为博主原创,所以难免会有一些小得瑕疵,敬请谅解. 所有示例均是博主测试过的,如有转载请标明出 ...

  3. .NET引用类型与值类型

    值类型和引用类型的基类 引用类型和值类型都继承自System.Object类.不同的是,几乎所有的引用类型都直接从System.Object继承,而值类型则继承其子类,即直接继承System.Valu ...

  4. 值类型和引用类型及其区别!

    值类型和引用类型的基类 引用类型和值类型都继承自System.Object类.不同的是,几乎所有的引用类型都直接从System.Object继承,而值类型则继承其子类,即直接继承System.Valu ...

  5. C# - 值类型、引用类型走出误区,容易错误的说法

    C# - 值类型.引用类型&走出误区,容易错误的说法 1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信 ...

  6. java——API—— Object类——重写equals方法

    重写Object类的equals方法 如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法. Object类的equals方法,默认比较的是两个 ...

  7. boost::spirit模块实现演示自定义的、用户定义的类型如何作为标记值类型轻松地与词法分析器集成

    boost::spirit模块实现演示自定义的.用户定义的类型如何作为标记值类型轻松地与词法分析器集成 实现功能 C++实现代码 实现功能 boost::spirit模块实现演示自定义的.用户定义的类 ...

  8. [转载] 向集合中添加自定义类型--建议在自定义类型的时候要重写equals方法

    参考链接: Java重写equals方法 package com.bjpowernode.t01list; import java.util.ArrayList; /* * 向集合中添加自定义类型 * ...

  9. 从自定义string类型理解右值引用

    理解右值引用 前言 问题复现 自定义string(CMyString) 遇到问题 图示理解 右值引用 什么是右值 添加右值引用参数的成员方法 结果对比 解决遗留问题 前言 在之前,我写过一篇: 通过自 ...

最新文章

  1. MSSQL Sql加密函数 hashbytes 用法简介
  2. 解决android模拟器访问本地服务器问题: W/System.err(1042): Caus...
  3. 18春东师计算机应用基础,东师计算机应用基础18春在线作业31.docx
  4. 图形图像技术在手游中的潜在应用
  5. smartgwt_高级SmartGWT教程,第1部分
  6. PS教程第七课:如何查看图层
  7. oracle高水位线
  8. SSM框架中的前后端分离
  9. nginx缓存、压缩配置
  10. 黑马程序员_面向对象的三大特征
  11. [每日一题] OCP1z0-047 :2013-07-12 多表插入
  12. 线性表顺序存储的基本操作方法(C语言)
  13. 企业网站建设方案策划书
  14. react小书没读完的记录
  15. HDU - 3533 Escape
  16. 【前端】静态网页和动态网页
  17. 【Java基础系列教程】第十五章 Java 正则表达式详解
  18. java基于springboot的股票交易模拟系统
  19. 有源电力滤波器并联三相apf matlab simulink仿真 谐波检测谐波补偿
  20. mysql去重合并字符串_Mysql将近两个月的记录合并为一行显示

热门文章

  1. php函数的初步使用
  2. ⑥又是星期五,小试牛刀(编写定制标签)
  3. 怎样去掉桌面图标和字的蓝色阴影
  4. 优秀编程网站收录集锦
  5. 程序代码初学者_初学者:如何使用热键在Windows中启动任何程序
  6. 您可能没有注意到的7个Ubuntu File Manager功能
  7. 网格自适应_ANSYS 非线性自适应(NLAD)网格划分及应用举例
  8. some demos
  9. 《简明 PHP 教程》00 开篇
  10. Netty1:初识Netty