我有一个班级:
public class Item
{
public string Name { get; set; }
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
重写GetHashCode的目的是我想在Dictionary中只有一个具有指定名称的对象。
但是从字符串中获取哈希代码是否安全? 换句话说,具有不同属性Name的两个对象是否有可能返回相同的哈希码?
答案 0 :(得分:4)
但从字符串中获取哈希码是否安全?
是的,这是安全的。 但是,你正在做的事情不是。您正在使用可变string
字段来生成哈希码。让我们假设您插入Item
作为给定值的键。然后,有人将Name
字符串更改为其他内容。您现在无法再在Item
,Dictionary
或您使用的任何结构中找到相同的HashSet
。
更多 - 你应该只依赖不可变类型。我还建议您实施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 Principle,您最终可能会遇到包含相同哈希码的两个不同字符串。
答案 1 :(得分:1)
您的课程有问题,因为您有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
的情况越多越好({1}}你无法预测它,所以我们希望在结果中有很好的混合位。但至关重要的事情是,两个被视为等于的对象必须具有相等的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()
相当擅长避免冲突,并且因为一个字符串是唯一定义相等的东西,所以你的代码反过来会相当擅长避免冲突。更多的防碰撞代码肯定是可能的,但是代码本身的性能成本*和/或是更多的工作而不是合理的。
*(尽管在64位.NET上的大字符串上比string.GetHashCode()
更快且更具抗冲突性,但我看到https://www.nuget.org/packages/SpookilySharp/虽然看起来{{3}},但是生成这些哈希码的速度较慢32位.NET或字符串很短时。)
答案 2 :(得分:0)
我没有使用GetHashCode
来防止将重复项添加到字典中,这在您已经解释过的情况下存在风险,我建议您使用(自定义)equality comparer作为字典。< / p>
如果键是一个对象,您应该创建一个比较string Name
值的自己的相等比较器。如果密钥是string
本身,则可以使用StringComparer.CurrentCulture
作为示例。
同样在这种情况下,使string
不可变是关键,因为否则您可能会通过更改Name
来使字典无效。