重写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值的对象是否有可能返回相同的哈希码?
但是从字符串中获取哈希码是安全的吗?
是的,它是安全的。但是,你正在做的不是。您正在使用一个可变的string
字段来生成散列代码。假设您插入了一个Item
作为给定值的键。然后,有人将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会返回相同的哈希码吗?
是的,这样的事情有统计学上的可能性发生。哈希码不能保证唯一性。他们争取统一形式的分配。为什么?因为你的上限是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值的对象是否有可能返回相同的哈希码?
是的,这可能会发生,但不会经常发生,所以没关系。像Dictionary
和HashSet
这样的基于哈希的集合可以处理一些冲突;事实上,即使哈希码都不一样,也会有碰撞,因为它们都取模到一个更小的索引。只有当这种情况经常发生时,它才会影响性能。
另一个危险是您将使用可变值作为键。有一种谬论认为你不应该对哈希码使用可变值,这是不正确的;如果一个可变对象的可变属性影响到它被认为是相等的对象,那么它必须导致对哈希码的更改。
真正的危险是改变一个对象,它是一个散列集合的键。如果您基于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
而使字典无效。