1. 程式人生 > >自定義值型別一定不要忘了重寫Equals,否則效能和空間雙雙堪憂

自定義值型別一定不要忘了重寫Equals,否則效能和空間雙雙堪憂

## 一:背景 ### 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,