自定義值型別一定不要忘了重寫Equals,否則效能和空間雙雙堪憂
阿新 • • 發佈:2020-05-31
## 一:背景
### 1. 講故事
曾今在專案中發現有同事自定義結構體的時候,居然沒有重寫Equals方法,比如下面這段程式碼:
``` C#
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看一下。
![](https://img2020.cnblogs.com/other/214741/202005/214741-20200531083918119-51310012.png)
``` C#
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` 翻出來看看便知:
``` C#
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`也需要裝箱一次。
![](https://img2020.cnblogs.com/other/214741/202005/214741-20200531083918375-2079005314.png)
<3> 有兩種比較方式,要麼採用 `FastEqualsCheck` 比較,要麼採用`反射`比較,我去.... 反射就玩大了。
綜合來看確實沒毛病, `equals` 會把比較的兩個物件都進行裝箱。
### 2. 改進方案
問題找到了,解決起來就簡單了,不走這個通用的 equals 不就行啦,我自定義一個equals方法,然後跑一下程式碼。
``` C#
public bool Equals(Point other)
{
return this.x == other.x && this.y == other.y;
}
```
![](https://img2020.cnblogs.com/other/214741/202005/214741-20200531083918658-344113624.png)
可以看到走了我的自定義的Equals,