重写GetHashCode并从string属性获取它安全吗?

本文关键字:安全 获取 属性 GetHashCode 并从 string 重写 | 更新日期: 2023-09-27 18:07:11

我有一个类:

public class Item
{
    public string Name { get; set; }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

重写GetHashCode的目的是我希望在Dictionary中只出现一次指定名称的对象。

但是从字符串中获取哈希码是安全的吗?换句话说,两个具有不同属性Name值的对象是否有可能返回相同的哈希码?

重写GetHashCode并从string属性获取它安全吗?

但是从字符串中获取哈希码是安全的吗?

是的,它是安全的。但是,你正在做的不是。您正在使用一个可变的string字段来生成散列代码。假设您插入了一个Item作为给定值的键。然后,有人将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会返回相同的哈希码吗?

是的,这样的事情有统计学上的可能性发生。哈希码不能保证唯一性。他们争取统一形式的分配。为什么?因为你的上限是32位的Int32。根据Pigenhole原理,您最终可能会得到两个包含相同散列码的不同字符串。

您的类有bug,因为您有GetHashCode覆盖,但没有Equals覆盖。您也不考虑Name为空的情况。

GetHashCode的规则很简单:

如果a.Equals(b),那么a.GetHashCode() == b.GetHashCode() .

对于任何给定的SomeValue(你无法预测它),!a.Equals(b)然后a.GetHashCode() != b.GetHashCode()越好,!a.Equals(b)然后a.GetHashCode() % SomeValue != b.GetHashCode() % 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被改变。

来自注释:

那么,即使在哈希码中存在冲突,当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)的查找,而是随着冲突的百分比越来越大,查找接近O(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而使字典无效。