日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧

發(fā)布時間:2023/12/4 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

一:背景

1. 講故事

曾今在項目中發(fā)現(xiàn)有同事自定義結(jié)構(gòu)體的時候,居然沒有重寫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;}}

這代碼貌似也沒啥什么問題,好像大家平時也是這么寫,沒關(guā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 00007ff8826f5b50Address 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?打出來,說明這些都是引用類型。。。這些引用類型哪里來的?看代碼應(yīng)該是?equals?比較時產(chǎn)生的,一次比較就有2個point被裝箱放到托管堆上,這下慘了,,,而且大家應(yīng)該知道引用對象本身還有(8+8) byte?自帶開銷,這在時間和空間上都是巨大的浪費呀。。。

二: 探究默認(rèn)的Equals實現(xiàn)

1. 尋找ValueType的Equals實現(xiàn)

為什么會這樣呢?我們知道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類型,參數(shù)裝箱一次。

<2>?CanCompareBits,FastEqualsCheck?都是采用object類型,this也需要裝箱一次。

<3> 有兩種比較方式,要么采用?FastEqualsCheck?比較,要么采用反射比較,我去.... 反射就玩大了。

綜合來看確實沒毛病,?equals?會把比較的兩個對象都進(jìn)行裝箱。

2. 改進(jìn)方案

問題找到了,解決起來就簡單了,不走這個通用的 equals 不就行啦,我自定義一個equals方法,然后跑一下代碼。

public bool Equals(Point other){return this.x == other.x && this.y == other.y;}

可以看到走了我的自定義的Equals,????????。貌似問題就這樣簡單粗暴的解決了,真開心,打臉時刻開始。。。

三:真的解決問題了嗎?

1. 遇到問題

很多時候我們會定義各種泛型類,在泛型操作中通常會涉及到T之間的 equals, 比如下面我設(shè)計的一段代碼,為了方便,我把Point的默認(rèn)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;}}

從輸出結(jié)果看,還是走了通用的equals方法,這就尷尬了,為什么會這樣呢?

2. 從FCL的值類型實現(xiàn)上尋找問題

有時候苦思冥想找不出問題,突然靈光一現(xiàn),FCL中不也有一些自定義值類型嗎?比如?int,long,decimal,何不看它們是怎么實現(xiàn)的,尋找尋找靈感, 對吧。。。說干就干,把?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少了接口實現(xiàn),問題應(yīng)該就出在這里,而且最后一個泛型接口IEquatable<int>特別顯眼,看下定義:

public interface IEquatable<T> {bool Equals(T other); }

這個泛型接口也僅僅只有一個equals方法,不過靈感告訴我,貌似。。。也許。。。應(yīng)該。。。就是這個泛型的equals是用來解決泛型情況下的equals比較。

3. 補上 IEquatable?接口

有了這個思路,我也跟FCL學(xué),讓Point實現(xiàn)?IEquatable<T>接口,然后在TProxy<T>代理類中約束下必須實現(xiàn)IEquatable<T>,修改代碼如下:

public struct Point : IEquatable<Point> { ... }public class TProxy<T> where T: IEquatable<T> { ... }

然后將程序跑起來,如下圖:

????????,雖然是成功了,但有一個地方讓我不是很舒服,就是上面的第二行代碼,在?TProxy<T>?處約束了T,因為我翻看List的實現(xiàn)也沒做這樣的泛型約束呀,可能有點強迫癥吧,貼一下代碼給大家看看。

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T> {}

然后我繼續(xù)模仿List,把?TProxy<T>?上的T約束去掉,結(jié)果就出問題了,又回到了?通用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?的源碼,簡化后實現(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; }

原來List是在進(jìn)行?equals比較之前,自己構(gòu)建了一個泛型比器EqualityComparer<T>,????????,然后繼續(xù)追一下代碼。

因為這里的runtimeType實現(xiàn)了IEquatable<T>接口,所以代碼返回了一個泛型比較器:GenericEqualityComparer<T>,然后我們繼續(xù)查看這個泛型比較器是咋樣的。

從圖中可以看到最終還是對T進(jìn)行了IEquatable<T>約束,不過這里給提取出來了,還是挺厲害的,然后我也學(xué)的模仿一下:

可以看到也走了我的自定義實現(xiàn),兩種方式大家都可以用哈????????????。

最后要注意一點的是,當(dāng)你重寫了Equals之后,編譯器會告知你最好也把?GetHashCode重寫一下,只是建議,如果看不慣這個提示,盡可能自定義GetHashCode方法讓hashcode分布的均勻一點。

四:總結(jié)

一定要實現(xiàn)自定義值類型的?Equals方法,人家的?Equals方法是用來兜底的,一次比較兩次裝箱,對你的程序可是雙殺哦????????????。

總結(jié)

以上是生活随笔為你收集整理的自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。