自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧
一:背景
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.Point
0:000> !dumpheap -mt 00007ff8826f5b50
Address 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,否则性能和空间双双堪忧的更多相关文章
- [C#] 类型学习笔记三:自定义值类型
既前两篇之后,这一篇我们讨论通过struct 关键字自定义值类型. 在第一篇已经讨论过值类型的优势,节省空间,不会触发Gargage Collection等等. 在对性能要求比较高的场景下,通过str ...
- Java常见面试题02-方法重写和方法重载的区别?方法重载能改变返回值类型吗?
方法重写和方法重载的区别?方法重载能改变返回值类型吗? A:Override方法重写和Overload方法重载的区别? Overload是否可以改变返回值类型?可以 方法重写 • 子类中 ...
- CLR via C#深解笔记三 - 基元类型、引用类型和值类型 | 类型和成员基础 | 常量和字段
编程语言的基元类型 某些数据类型如此常用,以至于许多编译器允许代码以简化的语法来操纵它们. System.Int32 a = new System.Int32(); // a = 0 a = 1 ...
- NET中的引用类型和值类型 zt
.NET中的类型分为值类型和引用类型,他们在内存布局,分配,相等性,赋值,存储以及一些其他的特性上有很多不同,这些不同将会直接影响到我们应用程序 的效率.本文视图对.NET 基础类型中的值类型和引用类 ...
- 八、C# 值类型
结构.枚举.装箱.拆箱 自定义值类型 如何利用结构来定义新的值类型,并使之具有与大多数预定义 类型相似的行为,这里的关键在于,任何 新定义的值类型都有它们自己的数据和方法. 一般用枚举来定义常量值集合 ...
- C#中的基元类型、值类型和引用类型
C# 中的基元类型.值类型和引用类型 1. 基元类型(Primitive Type) 编译器直接支持的类型称为基元类型.基元类型可以直接映射到 FCL 中存在的类型.例如,int a = 10 中的 ...
- [No0000B9]C# 类型基础 值类型和引用类型 及其 对象复制 浅度复制vs深度复制 深入研究2
接上[No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1 对象复制 有的时候,创建一个对象可能会非常耗时,比如对象需要从远程数据库中获取数据来填充,又或者创建对象需要读取硬 ...
- <NET CLR via c# 第4版>笔记 第5章 基元类型、引用类型和值类型
5.1 编程语言的基元类型 c#不管在什么操作系统上运行,int始终映射到System.Int32; long始终映射到System.Int64 可以通过checked/unchecked操作符/语句 ...
- 重温CLR(四)基元类型、引用类型、值类型
编程语言的基元类型 编译器直接支持的数据类型称为基元类型(primitive type).基元类型直接映射到framework类型(fcl)中存在的类型. 下表列出fcl类型 从另一个角度,可以认为C ...
随机推荐
- 过滤idea一些不需要的文件和文件夹的显示,在使用svn的时候可以很方便的过滤不需要提交的文件
*.classpath;*.gitignore;*.hprof;*.idea;*.iml;*.lst;*.project;*.pyc;*.pyo;*.rbc;*.settings;*.sh;*.yar ...
- 自动化API之一 生成开源ERP Odoo App 的RestFul API
1.在服务器上安装开源ERP Odoo 安装步骤请自行百度,本文重点不在于指导安装,以下是安装后PC端效果. Odoo: 2.在Uniconnector平台上注册Odoo App 移动端应用 3.配置 ...
- andorid jar/库源码解析之Butterknife
目录:andorid jar/库源码解析 Butterknife: 作用: 用于初始化界面控件,控件方法,通过注释进行绑定控件和控件方法 栗子: public class MainActivity e ...
- dp cf 20190614
C. Hard problem 这个题目一开始看还感觉比较复杂,但是还是可以写,因为这个决策很简单就是对于这个字符串倒置还是不倒置. 然后我不会一维去转移,直接用二维,第二维用01来表示转移和不转移, ...
- webpack-基础知识
一.webpack介绍 webpack是一个前端模块化工具,简单解释:webpack就是处理多个文件,根据设置的规则,对文件进行合并和修改. 正式说:webpack是一个模块化打包工具.从入口模块出发 ...
- apache反向代理和负载均衡
正向代理:正如我们用的游戏加速代理,大多的个人PC把请求发给正向代理服务器,代理服务器通常配置高端的带宽,替我们请求相应的服务 负载均衡中的反向代理:通常意义上,是一个请求转发的代理.类似一个收发室的 ...
- 02_互联网基本原理和HTML入门
上节课的知识复习 互联网的原理:服务器.浏览器.HTTP.知道网页文件是真实的物理存在,用HTTP请求这个文件. 要知道网址的含义:http://www.iqianduan.cn/aaa 请求哪个文件 ...
- spark是怎么从RDD升级到DataFrame的?
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是spark专题的第五篇,我们来看看DataFrame. 用过Python做过机器学习的同学对Python当中pandas当中的Data ...
- 设计者模式之GOF23命令模式
命令模式Command 将一个请求封装为一个对象,从而使我们可用不同的请求对客户参数化:对请求排队或者记录请求日志,以及支持可撤销的操作.也称之为:动作Action模式,事务transaction模式 ...
- [hdu5270]按位统计,容斥,归并
题意:给两个序列[a, a + n), [b, b + n),求所有数(ai + bj)的异或和,i,j∈[0,n). 思路:这个题思路比较巧妙,不难但是难想到.BC上的题解讲得非常清楚了,我就直接c ...