簡體   English   中英

重寫GetHashCode並從string屬性獲取它是否安全?

[英]Is it safe to override GetHashCode and get it from string property?

我有一堂課:

public class Item
{
    public string Name { get; set; }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

重寫GetHashCode的目的是,我只希望出現一次字典中具有指定名稱的對象。

但是從字符串獲取哈希碼是否安全? 換句話說,具有屬性Name的不同值的兩個對象是否有可能返回相同的哈希碼?

但是從字符串獲取哈希碼是否安全?

是的,這很安全。 但是 ,您正在做的不是。 您正在使用可變string字段來生成您的哈希碼。 假設您插入了一個Item作為給定值的鍵。 然后,有人將Name字符串更改為其他Name 現在,您將不再能夠在DictionaryHashSet或使用的任何結構內找到相同的Item

而且,您應該僅依賴於不可變類型。 我也建議您也實現IEquatable<T>

public class Item : IEquatable<Item>
{
    public Item(string name)
    {
        Name = name;
    }

    public string Name { get; }

    public bool Equals(Item other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Name, other.Name);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Item) obj);
    }

    public static bool operator ==(Item left, Item right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Item left, Item right)
    {
        return !Equals(left, right);
    }

    public override int GetHashCode()
    {
        return (Name != null ? Name.GetHashCode() : 0);
    }
}

具有屬性Name的不同值的兩個對象是否有可能返回相同的哈希碼?

是的,統計上有可能發生這種情況。 哈希碼不能保證唯一性。 他們爭取統一發行。 為什么? 因為您的上限是Int32 ,即32位。 根據Pigenhole原理 ,您可能最終會遇到兩個包含相同哈希碼的不同字符串。

您的課程有問題,因為您有GetHashCode覆蓋,但沒有Equals覆蓋。 您也不考慮Name為null的情況。

GetHashCode的規則很簡單:

如果a.Equals(b)則必須是a.GetHashCode() == b.GetHashCode()

如果更多!a.Equals(b)然后a.GetHashCode() != b.GetHashCode()越好,實際上!a.Equals(b)然后a.GetHashCode() % SomeValue != b.GetHashCode() % SomeValue更好,對於任何給定的SomeValue (您無法預測),因此我們希望在結果中很好地混合各個位。 但是至關重要的是,被認為相等的兩個對象必須具有相等的GetHashCode()結果。

目前情況並非如此,因為您僅覆蓋了其中之一。 但是,以下幾點是明智的:

public class Item
{
  public string Name { get; set; }

  public override int GetHashCode()
  {
      return Name == null ? 0 : Name.GetHashCode();
  }
  public override bool Equals(object obj)
  {
    var asItem = obj as Item;
    return asItem != null && Name == obj.Name;
  }
}

以下內容甚至更好,因為它允許更快地進行強類型相等比較:

public class Item : IEquatable<Item>
{
  public string Name { get; set; }

  public override int GetHashCode()
  {
      return Name == null ? 0 : Name.GetHashCode();
  }
  public bool Equals(Item other)
  {
    return other != null && Name == other.Name;
  }
  public override bool Equals(object obj)
  {
    return Equals(obj as Item);
  }
}

換句話說,具有屬性Name的不同值的兩個對象是否有可能返回相同的哈希碼?

是的,這可以發生,但是不會經常發生,所以很好。 DictionaryHashSet這樣的基於哈希的集合可以處理一些沖突。 即使哈希碼各不相同,也確實會發生沖突,因為哈希碼被模化為較小的索引。 只有這種情況經常發生,才會影響性能。

另一個危險是您將使用可變值作為鍵。 有一個神話,您不應該對哈希碼使用可變值,這是不正確的; 如果可變對象的可變屬性會影響認為與之相等的對象,那么它必須導致哈希碼發生變化。

真正的危險是要突變一個對象,而該對象是哈希集合的關鍵。 如果要基於Name定義相等性,並且具有這樣的對象作為字典的鍵,則在將Name用作此類鍵時, 一定不能更改Name 確保Name不變的最簡單方法是,如果可能的話,這絕對是個好主意。 但是,如果不可能,則僅在允許更改Name時需要小心。

來自評論:

因此,即使哈希碼發生沖突,當Equals返回false(因為名稱不同)時,Dictionary是否會正確處理?

是的,雖然不是很理想,但可以處理。 我們可以用這樣的類進行測試:

public class SuckyHashCode : IEquatable<SuckyHashCode>
{
  public int Value { get; set; }
  public bool Equals(SuckyHashCode other)
  {
    return other != null && other.Value == Value;
  }
  public override bool Equals(object obj)
  {
    return Equals(obj as SuckyHashCode);
  }
  public override int GetHashCode()
  {
    return 0;
  }
}

現在,如果我們使用它,它將起作用:

var dict = Enumerable.Range(0, 1000).Select(i => new SuckyHashCode{Value = i}).ToDictionary(shc => shc);
Console.WriteLine(dict.ContainsKey(new SuckyHashCode{Value = 3})); // True
Console.WriteLine(dict.ContainsKey(new SuckyHashCode{Value = -1})); // False

但是,顧名思義,它並不理想。 字典和其他基於散列的集合都具有處理沖突的手段,但這些手段意味着我們不再具有出色的近O(1)查找,而是隨着沖突百分比的增加,查找方法變得更加復雜(N)。 在上面的情況中, GetHashCode在沒有實際引發異常的情況下盡可能糟糕,其查找將是O(n),與將所有項目放入無序集合然后通過查找找到它們相同每一個都看是否匹配(實際上,由於開銷的不同,實際上比這差)。

因此,出於這個原因,我們始終希望盡可能避免沖突。 實際上,不僅要避免沖突,還要避免在對結果進行模降低以生成較小的哈希碼之后進行沖突(因為這是字典內部發生的事情)。

在您的情況下,盡管string.GetHashCode()在避免沖突方面相當擅長,並且因為唯一定義相等性的是一個字符串,所以您的代碼在避免沖突方面也相當不錯。 當然,可以使用更多的抗沖突代碼,但是這會損害代碼本身的性能*,並且/或者工作量超出合理范圍。

*(盡管我的代碼比https://www.nuget.org/packages/SpookilySharp/更快,但在64位.NET上的大字符串上比string.GetHashCode()更快,並且更耐碰撞,盡管速度較慢以在32位.NET或字符串短時生成那些哈希碼)。

我建議不要使用GetHashCode來防止將重復項添加到字典中(這對您來說是危險的,正如已經說明的那樣),我建議為您的字典使用(自定義) 相等比較器

如果鍵是對象,則應創建一個自己的相等比較器,以比較string Name值。 如果鍵是string本身,則可以使用StringComparer.CurrentCulture例如。

同樣在這種情況下,使string不可變也是關鍵,因為否則您可能會通過更改Name來使字典無效。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM