c# 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧
一:背景
1.讲故事
曾今在项目中发现有同事自定义结构体的时候,居然没有重写Equals方法,比如下面这段代码:
staticvoidMain(string[]args) { varlist=Enumerable.Range(0,1000).Select(m=>newPoint(m,m)).ToList(); varitem=list.FirstOrDefault(m=>m.Equals(newPoint(int.MaxValue,int.MaxValue))); Console.ReadLine(); } publicstructPoint { publicintx; publicinty; publicPoint(intx,inty) { this.x=x; this.y=y; } }
这代码貌似也没啥什么问题,好像大家平时也是这么写,没关系,有没有问题,跑一下再用windbg看一下。
0:000>!dumpheap-stat
Statistics:
MTCountTotalSizeClassName
00007ff8826fba201016592ConsoleApp6.Point[]
00007ff8e0055e70635448System.Object[]
00007ff8826f5b50200048000ConsoleApp6.Point0:000>!dumpheap-mt00007ff8826f5b50
AddressMTSize
0000020d00006fe000007ff8826f5b50240:000>!do0000020d00006fe0
Name:ConsoleApp6.Point
Fields:
MTFieldOffsetTypeVTAttrValueName
00007ff8e00585a040000018System.Int321instance0x
00007ff8e00585a04000002cSystem.Int321instance0y
从上面的输出不知道你看出问题了没有?托管堆上居然有2000个Point,而且还可以用!do打出来,说明这些都是引用类型。。。这些引用类型哪里来的?看代码应该是equals比较时产生的,一次比较就有2个point被装箱放到托管堆上,这下惨了,,,而且大家应该知道引用对象本身还有(8+8)byte自带开销,这在时间和空间上都是巨大的浪费呀。。。
二:探究默认的Equals实现
1.寻找ValueType的Equals实现
为什么会这样呢?我们知道equals是继承自ValueType的,所以把ValueType翻出来看看便知:
publicabstractclassValueType { publicoverrideboolEquals(objectobj) { if(CanCompareBits(this)){returnFastEqualsCheck(this,obj);} FieldInfo[]fields=runtimeType.GetFields(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic); for(inti=0;i从上面代码中可以看出有如下三点信息:
<1>通用的equals方法接收object类型,参数装箱一次。
<2>CanCompareBits,FastEqualsCheck都是采用object类型,this也需要装箱一次。
<3>有两种比较方式,要么采用FastEqualsCheck比较,要么采用反射比较,我去....反射就玩大了。
综合来看确实没毛病,equals会把比较的两个对象都进行装箱。
2.改进方案
问题找到了,解决起来就简单了,不走这个通用的equals不就行啦,我自定义一个equals方法,然后跑一下代码。
publicboolEquals(Pointother) { returnthis.x==other.x&&this.y==other.y; }可以看到走了我的自定义的Equals,