[英]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
。 现在,您将不再能够在Dictionary
, HashSet
或使用的任何结构内找到相同的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的不同值的两个对象是否有可能返回相同的哈希码?
是的,这可以发生,但是不会经常发生,所以很好。 像Dictionary
和HashSet
这样的基于哈希的集合可以处理一些冲突。 即使哈希码各不相同,也确实会发生冲突,因为哈希码被模化为较小的索引。 只有这种情况经常发生,才会影响性能。
另一个危险是您将使用可变值作为键。 有一个神话,您不应该对哈希码使用可变值,这是不正确的; 如果可变对象的可变属性会影响认为与之相等的对象,那么它必须导致哈希码发生变化。
真正的危险是要突变一个对象,而该对象是哈希集合的关键。 如果要基于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.