【问题标题】:Is it safe to override GetHashCode and get it from string property?覆盖 GetHashCode 并从字符串属性中获取它是否安全?
【发布时间】:2015-08-10 23:04:33
【问题描述】:

我有一堂课:

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

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

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

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

【问题讨论】:

  • 哈希码保证是唯一的。考虑到它返回一个int,但可能的字符串远不止 2^32 个。
  • 必须必须 不可变值计算哈希码 -否则您的对象将无法在任何字典或哈希表中正常工作。

标签: c# .net gethashcode


【解决方案1】:

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

是的,它是安全的。 但是,你所做的不是。您正在使用一个可变的 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);
    }
}

是否有可能两个具有不同属性值的对象 名称会返回相同的哈希码吗?

是的,这种事情发生的概率是统计上的。哈希码不保证唯一性。他们力求统一分配。为什么?因为你的上限是Int32,也就是 32 位。给定Pigenhole Principle,您最终可能会遇到两个包含相同哈希码的不同字符串。

【讨论】:

  • 如果你生成两个字符串,你可能会碰巧得到相同的哈希码。如果你运气不好。您正在使用的字符串数量会改变它发生的可能性,而不是可能性。
  • @Damien_The_Unbeliever 你是对的。我的回答中是否有任何其他方面的意思?
  • 您已经对最具暗示性的句子进行了编辑。
  • @Damien_The_Unbeliever 那么好 :) 谢谢。
【解决方案2】:

您的课程有问题,因为您有 GetHashCode 覆盖,但没有 Equals 覆盖。您也不会考虑Name 为空的情况。

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);
  }
}

换句话说,两个具有不同属性名称值的对象是否有可能返回相同的哈希码?

是的,这可能会发生,但不会经常发生,所以没关系。 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 或当字符串很短时)。

【讨论】:

  • 对,我忘了提它,因为我没有意识到它很重要,但是是的,我正在实现 IEquatable 接口并且我正在覆盖 Equals(Item other) 方法也是。那么,即使哈希码发生冲突,当 Equals 会返回 false(因为名称不同)时,Dictionary 会正确处理吗?
  • @Szybki 是的。我会在我的答案中添加一些内容。
  • 感谢您的精彩回答。但还有一个我什至不知道的问题。这是关于用作键的可变属性。虽然其他人说我不应该使用可变字符串作为键,但你说这是一个神话,所以它让我有点困惑。我不太了解密钥可变性的危险,因为我知道用户将对象重命名为字典中已经存在的名称。
  • 这是一个基于真实危险的神话;如果您将某物用作密钥,然后更改该密钥,使其不再与用作密钥时的相同,那么应该如何找到它?
  • 想象一下,如果你在一个按字母顺序排列的文件柜里有关于人的记录,如果他们要改名的人会以某种方式神奇地改变。您在“S”下提交了“爱丽丝史密斯小姐”。她将自己的名字改为“爱丽丝琼斯夫人”。现在,如果你无论如何都找不到她;如果您查找“Mrs Alice Jones”,您找不到她(她在“S”下归档,而您在“J”下查找)如果您尝试查找“Miss Alice Smith”,您找不到她(有“S”下不再有此类记录)。键不必是完全不可变的,但它们必须在用作键时不发生变异
【解决方案3】:

我建议不要使用GetHashCode 来防止将重复项添加到字典中,这在您的情况下是有风险的,如前所述,我建议您为字典使用(自定义)equality comparer

如果键是一个对象,您应该创建一个自己的相等比较器来比较string Name 值。如果 key 是 string 本身,你可以使用 StringComparer.CurrentCulture 例如。

在这种情况下,使string 不可变也很关键,否则您可能会通过更改Name 使您的字典无效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-17
    • 2012-07-13
    相关资源
    最近更新 更多