c# 自定義值類型一定不要忘了重寫(xiě)Equals,否則性能和空間雙雙堪憂
一:背景
1. 講故事
曾今在項(xiàng)目中發(fā)現(xiàn)有同事自定義結(jié)構(gòu)體的時(shí)候,居然沒(méi)有重寫(xiě)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; } }
這代碼貌似也沒(méi)啥什么問(wèn)題,好像大家平時(shí)也是這么寫(xiě),沒(méi)關(guān)系,有沒(méi)有問(wèn)題,跑一下再用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 00007ff8826f5b50
Address MT Size
0000020d00006fe0 00007ff8826f5b50 240: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
從上面的輸出不知道你看出問(wèn)題了沒(méi)有? 托管堆上居然有2000個(gè)Point,而且還可以用 !do
打出來(lái),說(shuō)明這些都是引用類型。。。這些引用類型哪里來(lái)的? 看代碼應(yīng)該是 equals
比較時(shí)產(chǎn)生的,一次比較就有2個(gè)point被裝箱放到托管堆上,這下慘了,,,而且大家應(yīng)該知道引用對(duì)象本身還有(8+8) byte
自帶開(kāi)銷,這在時(shí)間和空間上都是巨大的浪費(fèi)呀。。。
二: 探究默認(rèn)的Equals實(shí)現(xiàn)
1. 尋找ValueType的Equals實(shí)現(xiàn)
為什么會(huì)這樣呢? 我們知道equals是繼承自ValueType的,所以把 ValueType 翻出來(lái)看看便知:
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; } }
從上面代碼中可以看出有如下三點(diǎn)信息:
<1> 通用的 equals
方法接收object類型,參數(shù)裝箱一次。
<2> CanCompareBits,FastEqualsCheck
都是采用object類型,this
也需要裝箱一次。
<3> 有兩種比較方式,要么采用 FastEqualsCheck
比較,要么采用反射比較,我去.... 反射就玩大了。
綜合來(lái)看確實(shí)沒(méi)毛病, equals
會(huì)把比較的兩個(gè)對(duì)象都進(jìn)行裝箱。
2. 改進(jìn)方案
問(wèn)題找到了,解決起來(lái)就簡(jiǎn)單了,不走這個(gè)通用的 equals 不就行啦,我自定義一個(gè)equals方法,然后跑一下代碼。
public bool Equals(Point other) { return this.x == other.x && this.y == other.y; }
可以看到走了我的自定義的Equals,🐮👃。 貌似問(wèn)題就這樣簡(jiǎn)單粗暴的解決了,真開(kāi)心,打臉時(shí)刻開(kāi)始。。。
三:真的解決問(wèn)題了嗎?
1. 遇到問(wèn)題
很多時(shí)候我們會(huì)定義各種泛型類,在泛型操作中通常會(huì)涉及到T之間的 equals, 比如下面我設(shè)計(jì)的一段代碼,為了方便,我把Point
的默認(rèn)Equals也重寫(xiě)一下。
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; } }
從輸出結(jié)果看,還是走了通用的equals方法,這就尷尬了,為什么會(huì)這樣呢?
2. 從FCL的值類型實(shí)現(xiàn)上尋找問(wèn)題
有時(shí)候苦思冥想找不出問(wèn)題,突然靈光一現(xiàn),F(xiàn)CL中不也有一些自定義值類型嗎? 比如 int,long,decimal
,何不看它們是怎么實(shí)現(xiàn)的,尋找尋找靈感, 對(duì)吧。。。說(shuō)干就干,把 int32 源碼翻出來(lái)。
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少了接口實(shí)現(xiàn),問(wèn)題應(yīng)該就出在這里,而且最后一個(gè)泛型接口IEquatable<int>特別顯眼,看下定義:
public interface IEquatable<T> { bool Equals(T other); }
這個(gè)泛型接口也僅僅只有一個(gè)equals
方法,不過(guò)靈感告訴我,貌似。。。也許。。。應(yīng)該。。。就是這個(gè)泛型的equals
是用來(lái)解決泛型情況下的equals
比較。
3. 補(bǔ)上 IEquatable 接口
有了這個(gè)思路,我也跟FCL學(xué),讓Point實(shí)現(xiàn) IEquatable<T>接口,然后在TProxy<T>代理類中約束下必須實(shí)現(xiàn)IEquatable<T>,修改代碼如下:
public struct Point : IEquatable<Point> { ... } public class TProxy<T> where T: IEquatable<T> { ... }
然后將程序跑起來(lái),如下圖:
🐮👃,雖然是成功了,但有一個(gè)地方讓我不是很舒服,就是上面的第二行代碼,在 TProxy<T>
處約束了T
,因?yàn)槲曳?code>List的實(shí)現(xiàn)也沒(méi)做這樣的泛型約束呀,可能有點(diǎn)強(qiáng)迫癥吧,貼一下代碼給大家看看。
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T> {}
然后我繼續(xù)模仿List,把 TProxy<T>
上的T約束去掉,結(jié)果就出問(wèn)題了,又回到了 通用Equals
。
4. 從List的Contains源碼中尋找答案
好奇心再次驅(qū)使我尋找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
的源碼,簡(jiǎn)化后實(shí)現(xiàn)如下。
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; }
原來(lái)List是在進(jìn)行 equals
比較之前,自己構(gòu)建了一個(gè)泛型比較器EqualityComparer<T>
,🐮👃,然后繼續(xù)追一下代碼。
因?yàn)檫@里的runtimeType
實(shí)現(xiàn)了IEquatable<T>
接口,所以代碼返回了一個(gè)泛型比較器:GenericEqualityComparer<T>
,然后我們繼續(xù)查看這個(gè)泛型比較器是咋樣的。
從圖中可以看到最終還是對(duì)T進(jìn)行了IEquatable<T>約束,不過(guò)這里給提取出來(lái)了,還是挺厲害的,然后我也學(xué)的模仿一下:
可以看到也走了我的自定義實(shí)現(xiàn),兩種方式大家都可以用哈😁😁😁。
最后要注意一點(diǎn)的是,當(dāng)你重寫(xiě)了Equals
之后,編譯器會(huì)告知你最好也把 GetHashCode
重寫(xiě)一下,只是建議,如果看不慣這個(gè)提示,盡可能自定義GetHashCode
方法讓hashcode
分布的均勻一點(diǎn)。
四:總結(jié)
一定要實(shí)現(xiàn)自定義值類型的 Equals
方法,人家的 Equals
方法是用來(lái)兜底的,一次比較兩次裝箱,對(duì)你的程序可是雙殺哦😁😁😁。
以上就是c# 自定義值類型一定不要忘了重寫(xiě)Equals,否則性能和空間雙雙堪憂的詳細(xì)內(nèi)容,更多關(guān)于c# 自定義值類型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
DevExpress實(shí)現(xiàn)GridControl單元格編輯驗(yàn)證的方法
這篇文章主要介紹了DevExpress實(shí)現(xiàn)GridControl單元格編輯驗(yàn)證的方法,很實(shí)用的功能,需要的朋友可以參考下2014-08-08C#8.0默認(rèn)接口實(shí)現(xiàn)的詳細(xì)實(shí)例
Microsoft使用C#8.0發(fā)布了許多新功能,他們引入的主要功能之一是默認(rèn)接口方法。這篇文章主要給大家介紹了關(guān)于C#8.0默認(rèn)接口實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2021-05-05C#與C++動(dòng)態(tài)鏈接庫(kù)DLL參數(shù)互傳方式
這篇文章主要介紹了C#與C++動(dòng)態(tài)鏈接庫(kù)DLL參數(shù)互傳方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11c#根據(jù)文件類型獲取相關(guān)類型圖標(biāo)的方法代碼
c#根據(jù)文件類型獲取相關(guān)類型圖標(biāo)的方法代碼,需要的朋友可以參考一下2013-05-05C#自定義控件實(shí)現(xiàn)TextBox禁止粘貼的方法
這篇文章主要介紹了C#自定義控件實(shí)現(xiàn)TextBox禁止粘貼的方法,結(jié)合具體實(shí)例形式分析了C#自定義控件的創(chuàng)建、使用方法及TextBox禁止粘貼的實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-06-06詳解C#實(shí)例化對(duì)象的三種方式及性能對(duì)比
這篇文章主要介紹了C#實(shí)例化對(duì)象的三種方式及性能對(duì)比,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Winform窗體縮放下使用剪切板功能出現(xiàn)頁(yè)面閃動(dòng)解決分析
這篇文章主要介紹了Winform窗體縮放下使用剪切板功能出現(xiàn)頁(yè)面閃動(dòng)解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11