在 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)
{
}
可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 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 文件夾
