默认实现如何GetHashCode()
工作?它是否有效,足够好地处理结构,类,数组等?
我正在尝试确定在什么情况下应该打包自己的产品,以及在什么情况下我可以安全地依靠默认实现来做得很好。如果有可能,我不想重新发明轮子。
GetHashCode()
System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
默认实现如何GetHashCode()
工作?它是否有效,足够好地处理结构,类,数组等?
我正在尝试确定在什么情况下应该打包自己的产品,以及在什么情况下我可以安全地依靠默认实现来做得很好。如果有可能,我不想重新发明轮子。
GetHashCode()
System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Answers:
namespace System {
public class Object {
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern int InternalGetHashCode(object obj);
public virtual int GetHashCode() {
return InternalGetHashCode(this);
}
}
}
InternalGetHashCode映射到CLR中的ObjectNative :: GetHashCode函数,如下所示:
FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
CONTRACTL
{
THROWS;
DISABLED(GC_NOTRIGGER);
INJECT_FAULT(FCThrow(kOutOfMemoryException););
MODE_COOPERATIVE;
SO_TOLERANT;
}
CONTRACTL_END;
VALIDATEOBJECTREF(obj);
DWORD idx = 0;
if (obj == 0)
return 0;
OBJECTREF objRef(obj);
HELPER_METHOD_FRAME_BEGIN_RET_1(objRef); // Set up a frame
idx = GetHashCodeEx(OBJECTREFToObject(objRef));
HELPER_METHOD_FRAME_END();
return idx;
}
FCIMPLEND
GetHashCodeEx的完整实现相当大,因此仅链接到C ++源代码就更容易了。
string
覆盖GetHashCode
。另一方面,假设您想统计各种控件处理Paint
事件的次数。您可以使用Dictionary<Object, int[]>
(每个int[]
存储的内容只能容纳一个项目)。
对于一个类,默认值实质上是引用相等,通常很好。如果编写一个结构,则覆盖相等性更为常见(尤其是避免装箱),但是无论如何您都很难编写一个结构!
当重写等式,你应该始终有一个匹配的Equals()
和GetHashCode()
(即两个值,如果Equals()
返回true,他们必须返回相同的哈希码,但反过来不是必需的) -这是常见的也提供==
/ !=
运营商,并经常到实施IEquatable<T>
。
为了生成哈希码,通常使用分解和,因为这样可以避免在成对的值上发生冲突-例如,对于基本的2字段哈希:
unchecked // disable overflow, for the unlikely possibility that you
{ // are compiling with overflow-checking enabled
int hash = 27;
hash = (13 * hash) + field1.GetHashCode();
hash = (13 * hash) + field2.GetHashCode();
return hash;
}
这样做的优点是:
等-如果仅使用未加权总和或xor(^
)等,则可能很常见。
unchecked
。幸运的是,它unchecked
是C#中的默认值,但最好将其显式表示。编辑
ObjectGetHashCode
方法的文档说:“此方法的默认实现不得用作用于哈希目的的唯一对象标识符。” 而ValueType表示“如果调用派生类型的GetHashCode方法,则返回值不太可能适合用作哈希表中的键。” 。
基本数据类型,例如byte
,short
,int
,long
,char
以及string
实现良好的GetHashCode方法。Point
例如,其他一些类和结构实现的GetHashCode
方法可能适合也可能不适合您的特定需求。您只需要尝试一下,看看它是否足够好。
每个类或结构的文档可以告诉您是否覆盖了默认实现。如果未覆盖它,则应使用自己的实现。对于您在需要使用该GetHashCode
方法的地方创建的任何类或结构,应进行自己的使用适当成员计算散列代码的实现。
由于我找不到答案来解释为什么我们应该覆盖GetHashCode
和Equals
定制结构,以及为什么默认实现“不太可能适合用作哈希表中的键”,因此,我将保留此博客的链接post,以真实的案例说明了所发生问题的原因。
我建议阅读整个文章,但这是一个摘要(添加了重点和说明)。
原因是结构的默认哈希很慢而且不是很好:
CLR的设计方式,每次调用
System.ValueType
或System.Enum
类型定义的成员[可能]导致装箱分配 [...]哈希函数的实现者面临两难选择:合理分配哈希函数或使其快速发展。在某些情况下,可以同时实现它们,但是很难在中通用实现
ValueType.GetHashCode
。结构的规范哈希函数“组合”所有字段的哈希码。但是在方法中获取字段的哈希码的唯一
ValueType
方法是使用反射。因此,CLR作者决定在分布上进行交易,默认GetHashCode
版本仅返回第一个非空字段的哈希码,并使用类型id [修改]它[...]这是合理的行为,除非它不是。例如,如果您不够幸运,并且结构的第一个字段在大多数实例中具有相同的值,则哈希函数将始终提供相同的结果。而且,正如您可能想象的那样,如果将这些实例存储在哈希集或哈希表中,将会对性能产生巨大影响。[...] 基于反射的实现速度很慢。非常慢。
[...]两者
ValueType.Equals
并ValueType.GetHashCode
有专门的优化。如果类型不具有“指针”并且已正确打包,则使用更佳的版本:GetHashCode
对一个实例进行迭代,并对4个字节的XOR块Equals
进行比较,方法使用来比较两个实例memcmp
。[...]但是优化非常棘手。首先,很难知道何时启用了优化。其次,内存比较不一定会为您提供正确的结果。这是一个简单的示例:-0.0
和+0.0
相等,但是具有不同的二进制表示形式。
帖子中描述的实际问题:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
我们使用了一个包含具有默认相等实现的自定义结构的元组。而且不幸的是,该结构具有一个可选的第一字段,该字段几乎总是等于[empty string]。性能良好,直到集合中的元素数量显着增加,导致真正的性能问题为止,花几分钟来初始化包含数万个项目的集合。
因此,要回答“在什么情况下我应该打包我自己的包以及在什么情况下我可以安全地依赖默认实现”的问题,至少在structs的情况下,您应该重写Equals
并且GetHashCode
每当将自定义结构用作键入哈希表或Dictionary
。
我也建议IEquatable<T>
在这种情况下实施,以避免装箱。
就像其他答案所说的那样,如果您正在编写一个类,则使用引用相等性的默认哈希值通常是可以的,因此在这种情况下我不会打扰,除非您需要重写Equals
(然后您必须相应地重写GetHashCode
)。
一般来说,如果要覆盖Equals,则要覆盖GetHashCode。这样做的原因是因为两者都用于比较类/结构的相等性。
在检查Foo A,B时使用Equals。
如果(A == B)
由于我们知道指针不太可能匹配,因此我们可以比较内部成员。
Equals(obj o)
{
if (o == null) return false;
MyType Foo = o as MyType;
if (Foo == null) return false;
if (Foo.Prop1 != this.Prop1) return false;
return Foo.Prop2 == this.Prop2;
}
哈希表通常使用GetHashCode。对于给定的类,您的类生成的哈希码应始终相同。
我通常会
GetHashCode()
{
int HashCode = this.GetType().ToString().GetHashCode();
HashCode ^= this.Prop1.GetHashCode();
etc.
return HashCode;
}
有人会说哈希码每个对象生命周期仅应计算一次,但是我不同意这一点(我可能是错误的)。
使用object提供的默认实现,除非您对一个类的引用相同,否则它们将彼此不相等。通过覆盖Equals和GetHashCode,您可以基于内部值而不是对象引用来报告相等性。
如果您只是在处理POCO,则可以使用此实用程序来简化您的生活:
var hash = HashCodeUtil.GetHashCode(
poco.Field1,
poco.Field2,
...,
poco.FieldN);
...
public static class HashCodeUtil
{
public static int GetHashCode(params object[] objects)
{
int hash = 13;
foreach (var obj in objects)
{
hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
}
return hash;
}
}