自定義值類型一定不要忘了重寫Equals,否則性能和空間雙雙堪憂_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

一:背景

1. 講故事

曾今在項目中發現有同事自定義結構體的時候,居然沒有重寫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;
        }
    }

這代碼貌似也沒啥什麼問題,好像大家平時也是這麼寫,沒關係,有沒有問題,跑一下再用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.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 翻出來看看便知:


    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也需要裝箱一次。

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

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

2. 改進方案

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

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

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

三:真的解決問題了嗎?

1. 遇到問題

很多時候我們會定義各種泛型類,在泛型操作中通常會涉及到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方法,這就尷尬了,為什麼會這樣呢?

2. 從FCL的值類型實現上尋找問題

有時候苦思冥想找不出問題,突然靈光一現,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比較。

3. 補上 IEquatable 接口

有了這個思路,我也跟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

4. 從List的Contains源碼中尋找答案

好奇心再次驅使我尋找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方法是用來兜底的,一次比較兩次裝箱,對你的程序可是雙殺哦。

如您有更多問題與我互動,掃描下方進來吧~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」