曾今在項目中發現有同事自定義結構體的時候,竟然沒有重寫Equals方法,好比下面這段代碼:ide
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看一下。this
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
是繼承自ValueType
的,因此把 ValueType
翻出來看看便知:3d
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類型,參數裝箱一次。code
<2> CanCompareBits,FastEqualsCheck
都是採用object類型,this
也須要裝箱一次。orm
<3> 有兩種比較方式,要麼採用 FastEqualsCheck
比較,要麼採用反射
比較,我去.... 反射就玩大了。對象
綜合來看確實沒毛病, equals
會把比較的兩個對象都進行裝箱。blog
問題找到了,解決起來就簡單了,不走這個通用的 equals 不就行啦,我自定義一個equals方法,而後跑一下代碼。繼承
public bool Equals(Point other) { return this.x == other.x && this.y == other.y; }
能夠看到走了個人自定義的Equals,🐮👃。 貌似問題就這樣簡單粗暴的解決了,真開心,打臉時刻開始。。。
不少時候咱們會定義各類泛型類,在泛型操做中一般會涉及到T之間的 equals, 好比下面我設計的一段代碼,爲了方便,我把Point
的默認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; } }
從輸出結果看,仍是走了通用的equals方法,這就尷尬了,爲何會這樣呢?
有時候苦思冥想找不出問題,忽然靈光一現,FCL中不也有一些自定義值類型嗎? 好比 int,long,decimal
,何不看它們是怎麼實現的,尋找尋找靈感, 對吧。。。說幹就幹,把 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少了接口實現,問題應該就出在這裏,並且最後一個泛型接口IEquatable<int>
特別顯眼,看下定義:
public interface IEquatable<T> { bool Equals(T other); }
這個泛型接口也僅僅只有一個equals
方法,不過靈感告訴我,貌似。。。也許。。。應該。。。就是這個泛型的equals
是用來解決泛型狀況下的equals
比較。
有了這個思路,我也跟FCL學,讓Point實現 IEquatable<T>
接口,而後在TProxy<T>
代理類中約束下必須實現IEquatable<T>
,修改代碼以下:
public struct Point : IEquatable<Point> { ... } public class TProxy<T> where T: IEquatable<T> { ... }
而後將程序跑起來,以下圖:
🐮👃,雖然是成功了,但有一個地方讓我不是很舒服,就是上面的第二行代碼,在 TProxy<T>
處約束了T
,由於我翻看List
的實現也沒作這樣的泛型約束呀,可能有點強迫症吧,貼一下代碼給你們看看。
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T> {}
而後我繼續模仿List,把 TProxy<T>
上的T約束去掉,結果就出問題了,又回到了 通用Equals
。
好奇心再次驅使我尋找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
的源碼,簡化後實現以下。
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是在進行 equals
比較以前,本身構建了一個泛型比較器EqualityComparer<T>
,🐮👃,而後繼續追一下代碼。
由於這裏的runtimeType
實現了IEquatable<T>
接口,因此代碼返回了一個泛型比較器:GenericEqualityComparer<T>
,而後咱們繼續查看這個泛型比較器是咋樣的。
從圖中能夠看到最終仍是對T
進行了IEquatable<T>
約束,不過這裏給提取出來了,仍是挺厲害的,而後我也學的模仿一下:
能夠看到也走了個人自定義實現,兩種方式你們均可以用哈😁😁😁。
最後要注意一點的是,當你重寫了Equals
以後,編譯器會告知你最好也把 GetHashCode
重寫一下,只是建議,若是看不慣這個提示,儘量自定義GetHashCode
方法讓hashcode
分佈的均勻一點。
必定要實現自定義值類型的 Equals
方法,人家的 Equals
方法是用來兜底的,一次比較兩次裝箱,對你的程序但是雙殺哦😁😁😁。