dotnet C# 基礎 為什么 GetHashCode 推薦只取只讀屬性或字段做哈希值


在 C# 里面,所有的對象都繼承 Object 類型,此類型有開放 GetHashCode 用於給開發者重寫。此 GetHashCode 方法推薦是在重寫 Equals 方法時也同時進行重寫,要求兩個對象在 Equals 返回相等時,兩個對象的 GetHashCode 返回值也相等。反過來則不然,允許有兩個不相等的對象的 GetHashCode 是相等的

在重寫 Equals 方法時,大部分時候都是自動生成的,如將類里面的所有字段或屬性都進行一一比較。那在 GetHashCode 方法里面,所輸出的哈希值的計算,是否也需要使用此類型的所有字段或屬性共同計算出來?如果在 GetHashCode 里面使用的字段或屬性非只讀,那么 ReSharper 將會警告你這是不安全的。本文將來告訴大家為什么這是不安全的

在 dotnet 里面,大部分會用到 GetHashCode 的邏輯都在於哈希容器里面,如 Dictionary 字典等。這些哈希容器在設計上都期望類型遵守以下行為:當兩個對象相等的時候,那么獲取 GetHashCode 的值也一定相等

假定有類型的 GetHashCode 返回值是基於非只讀的屬性或字段,將會導致在將對象加入哈希容器的時候,所獲取到的 GetHashCode 的值是不包括未來對非只讀屬性或字段變更的防御的。在未來對此對象的非只讀的屬性或字段進行變更,也許就會影響到此對象再次獲取 GetHashCode 的屬性,從而讓相同的一個對象,在哈希容器里面,因為 GetHashCode 返回值不同,而被認為是不同的對象

假設有如此的代碼邏輯,某個 Foo2 的對象的 GetHashCode 返回值是由此對象的屬性決定的,如下面代碼

    class Foo2
    {
        public int HashCode { set; get; }

        public override int GetHashCode()
        {
            return HashCode;
        }
    }

假定將此 Foo2 的對象加入到字典里面,接着去判斷字典里面是否存在此對象。再修改 Foo2 的 HashCode 屬性,再去判斷字典里面是否存在此對象,代碼如下

                var foo2 = new Foo2();
                Dictionary<Foo2, object> dictionary = new();
                dictionary[foo2] = foo2;
                Console.WriteLine(dictionary.ContainsKey(foo2));
                foo2.HashCode = 2;
                Console.WriteLine(dictionary.ContainsKey(foo2));

有趣的邏輯是第一次返回的符合預期,就是 True 的值。然而第二次,明明沒有從字典里面移除 Foo2 對象,然而字典卻認為找不到此對象

其原因如上文,在字典里面,優先通過 GetHashCode 的值來進行判斷。如上面代碼,更改了 Foo2 的 GetHashCode 返回值,將會讓字典找不到此 HashCode 對應的元素,從而讓字典認為不存在此對象

大部分在設計類型的時候,都不會考慮到某個類型在未來或其他模塊里面,會被存放進哈希容器里面。如果此時在 GetHashCode 里面,使用了非只讀字段或屬性,將會挖一個坑。也許某個邏輯變更了這些非只讀字段或屬性的時候,影響了 GetHashCode 的返回值從而影響了哈希容器的行為

這就是為什么 ReSharper 警告不要在 GetHashCode 里面使用非只讀字段或屬性進行制作哈希值的原因

不過在理解了這個行為,在某些特別的業務里面,也可以利用此行為實現有趣的功能

通過本文也可以了解到,對於 GetHashCode 的返回值也不能為了因為重寫 Equals 方法而被 VS 警告而隨便寫此方法的實現,如下面逗比代碼

    class Foo
    {
        public Foo(string name)
        {
            Name = name;
        }

        public string Name { get; }

        public override int GetHashCode()
        {
            return _random.Next();
        }

        private readonly Random _random = new Random();
    }

上面的代碼在 GetHashCode 隨機返回一個值,這將會讓所有哈希容器炸掉,如下面的代碼,將在字典拿不到值

            try
            {
                Foo[] foo = new Foo[100];
                for (int i = 0; i < 100; i++)
                {
                    foo[i] = new Foo(i.ToString());
                }

                Dictionary<Foo, string> dictionary = foo.ToDictionary(t => t, t => t.Name);

                for (int i = 0; i < foo.Length; i++)
                {
                    Console.WriteLine($"{foo[i].Name}-{dictionary[foo[i]]}"); // KeyNotFoundException
                }
            }
            catch (Exception)
            {
            }

本文所有代碼放在 githubgitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 572e405d383c69929397a583102576e2e140f1fc

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

獲取代碼之后,進入 BelwheaheajeachelYikaidairnay 文件夾


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM