您的位置:首页 > 其它

HashCode的存在的意义(什么是Hash Code,它的作用是什么?)

2007-07-09 10:23 302 查看
引言:
-------------------------------------------------------------------------------------------------------------------------------
现实生活中要想查字典中的数据就需要一个方法 即通过“拼音”查“汉字”等。
而在计算机字典中 要想获取字典中数据的位置是很难的(我们用的时候很容易 因为M$已经做了一个很复杂的算法) 而GetHashCode()方法就是返回一个对象在字典中的存储位置 即散列码
假如A.Equals(B) 返回true 则A.GetHashCode()与B.GetHashCode()返回的散列码就应相等
-------------------------------------------------------------------------------------------------------------------------------

用作特定类型的哈希函数,适合在哈希算法和数据结构(如哈希表)中使用。

[Visual Basic]
<Serializable>
<ClassInterface(ClassInterfaceType.AutoDual)>
Overridable Public Function [b]GetHashCode() As [/b]Integer

[C#]
[Serializable]
[ClassInterface(ClassInterfaceType.AutoDual)]
public virtual int [b]GetHashCode();[/b]

[C++]
[Serializable]
[ClassInterface(ClassInterfaceType.AutoDual)]
public: virtual int [b]GetHashCode();[/b]

[JScript]
public
Serializable
ClassInterface(ClassInterfaceType.AutoDual)
function [b]GetHashCode() : [/b]int;

返回值

当前 Object 的哈希代码。

备注

此方法可由派生类重写。值类必须重写此方法,以提供适合该类并且确保哈希表中有更好的分布的哈希函数。在哈希表中可以用作键的类也必须重写此方法,因为在哈希表中用作键的对象对于通过此方法生成类自己的哈希代码是必需的。

Object.[b]GetHashCode[/b] 的此实现只能保证将为相同的实例返回相同的哈希代码;它无法保证不同的实例将具有不同的哈希代码,或者两个引用相同值的对象将具有相同的哈希代码。.NET 框架的不同版本对于相同的实例也可能会生成不同的哈希代码。因此,不要将哈希代码保持到文件或通过网络发送它们。若要保证相同的对象具有相同的哈希代码,必须使用 System.Collections.IHashCodeProvider 接口定义您自己的不可变的哈希函数并且始终使用它。

默认实现返回由公共语言运行库确定的对象的索引。该索引对于执行引擎实例的 AppDomain 中的对象实例而言是唯一的。但是,由于在垃圾回收过程中回收了对象后可以重用该索引,所以对于两个不同的对象可能获得相同的哈希代码。另外,两个表示相同值的对象只有是完全相同的对象时,它们才具有相同的哈希代码。此实现对哈希运算并不是特别有用;因此,派生类应重写 [b]GetHashCode[/b]。

对实施者的说明:

哈希函数用于快速生成一个与对象的值相对应的数字(哈希代码)。哈希函数通常特定于每个 Type 并且应至少将一个实例字段用作输入。

哈希函数必须具有下列属性:

如果两个类型相同的对象表示相同的值,则哈希函数必须为两个对象返回相同的常数值。
为了获得最佳性能,哈希函数应该为所有输入生成随机分布。
哈希函数应基于不可变的数据成员。不论对该对象进行什么样的更改,哈希函数都应返回完全相同的值。将哈希函数基于可变数据成员会导致严重的问题,包括永远无法访问哈希表中的该对象。

例如,String 类提供的 GetHashCode 实现为唯一的字符串值返回唯一的哈希代码。因此,如果两个 String 对象表示相同的字符串值,则它们返回相同的哈希代码。另外,该方法使用字符串中的所有字符生成相当随机的分布式输出,即使当输入集中在某些范围内时(例如,许多用户可能有只包含低位 128 个 ASCII 字符的字符串,即使字符串可以包含 65,535 个 Unicode 字符中的任何字符)。

[b]GetHashCode[/b] 对于对象的给定实例必须总是返回相同的值。确保这一点的一种方法是将哈希代码基于不可变的数据成员。对于 Object 的派生类,当且仅当此派生类将值相等定义为引用相等并且类型不是值类型时,[b]GetHashCode[/b] 才可以委托给 Object.[b]GetHashCode[/b] 实现。

在类上提供好的哈希函数可以显著影响将这些对象添加到哈希表的性能。在具有好的哈希函数实现的哈希表中,搜索元素所用的时间是固定的(例如 O(1) 操作)。而在具有不好的哈希函数实现的哈希表中,搜索性能取决于哈希表中的项数(例如 O(n) 操作,其中的 n 是哈希表中的项数)。哈希函数的计算成本也应该不高。

[b]GetHashCode[/b] 的实现必须不会导致循环引用。例如,如果 ClassA.[b]GetHashCode[/b] 调用 ClassB.[b]GetHashCode[/b],ClassB.[b]GetHashCode[/b] 必须不直接或间接调用 ClassA.[b]GetHashCode[/b]。

[b]GetHashCode[/b] 的实现必须不引发异常。

重写 [b]GetHashCode[/b] 的派生类还必须重写 Equals,以保证被视为相等的两个对象具有相同的哈希代码;否则,Hashtable 可能不会正常工作。

示例

[C#, JScript] 在某些情况下,[b]GetHashCode[/b] 的实现只返回整数值。下列代码示例阐释了返回整数值的 GetHashCode 的实现。

[C#]
using System;
public struct Int32 {
public int value;
//other methods...
public override int GetHashCode() {
return value;
}
}

[C#, JScript] 一个类型常具有多个可以参与生成哈希代码的数据字段。生成哈希代码的一种方法是使用 XOR (eXclusive OR) 运算合并这些字段,如下列代码示例所示。

[C#]
using System;
public struct Point {
public int x;
public int y;
//other methods
public override int GetHashCode() {
return x ^ y;
}
}

[C#, JScript] 下列代码示例阐释了另一种情况:使用 XOR (eXclusive OR) 合并该类型的字段以生成哈希代码。注意,在该代码示例中,字段表示用户定义的类型,每个类型都实现 [b]GetHashCode[/b] 和 Equals。

[C#]
using System;
public class SomeType {
public override int GetHashCode() {
return 0;
}
}
public class AnotherType {
public override int GetHashCode() {
return 1;
}
}
public class LastType {
public override int GetHashCode() {
return 2;
}
}
public class MyClass {
SomeType a = new SomeType();
AnotherType b = new AnotherType();
LastType c = new LastType();
public override int GetHashCode () {
return a.GetHashCode() ^ b.GetHashCode() ^ c.GetHashCode();
}
}

[JScript]
import System;
public class SomeType {
public override function GetHashCode(): int  {
return 0;
}
}
public class AnotherType {
public override function GetHashCode(): int {
return 1;
}
}
public class LastType {
public override function GetHashCode(): int {
return 2;
}
}
public class MyClass {
var a: SomeType  = new SomeType();
var b: AnotherType = new AnotherType();
var c: LastType = new LastType();
public override function GetHashCode () : int {
return a.GetHashCode() ^ b.GetHashCode() ^ c.GetHashCode();
}
}

[C#, JScript] 如果派生类的数据成员比 Int32 大,则可以使用 XOR (eXclusive OR) 运算合并该值的高序位和低序位,如下列代码示例所示。

[C#]
using System;
public struct Int64 {
public long value;
//other methods...
public override int GetHashCode() {
return ((int)value ^ (int)(value >> 32));
}
}

[JScript]
import System;
public class Int64 {
var value : long;
//other methods...
public override function GetHashCode() : int {
return (int(value) ^ int(value >> 32));
}
function Int64(myValue : long) {
value = myValue;
}
}

[Visual Basic, C++] 没有可用于 Visual Basic 或 C++ 的示例。若要查看 C# 或 JScript 示例,请单击页左上角的语言筛选器按钮 。
------------------------------------------------------------------------------------------------------------------------------
理解GetHashCode()方法的缺陷:
------------------------------------------------------------------------------------------------------------------------------

本条款讨论函数GetHashCode()的缺陷,这种情况在全书中是唯一的。幸运的是,GetHashCode()函数只应用在一个地方:为一个基于散列(hash)的集合定义键的散列值,典型的集合为Hashtable或Dictionary容器。因为基类的GetHashCode()实现有很多问题。对引用类型来讲,它可以正常工作,但是效率很低。对于值类型来讲,基类中的实现通常是不正确的。更为糟糕的是,我们编写的GetHashCode()不可能既有效率又正确。没有哪个函数能比GetHashCode()产生更多的讨论和混淆。下面的内容可以帮助大家理清这些混淆。

如果我们定义的类型在容器中不会被当作键来使用,那就没什么问题。例如,表示窗口控件、Web页面控件或者数据库连接的类型就不太可能被当作键在集合中使用。在这些情况下,我们无需做任何事情。所有的引用类型都有一个正确的散列码,尽管效率非常低下。值类型应该具有常量性(参见条款7),这时候默认的实现也可以正常工作,虽然效率也不高。在我们创建的大多数类型中,最好的做法就是完全避免自己实现GetHashCode()。

如果有一天,我们创建的类型要被当作散列表(hash table)中的键使用,我们就需要自己实现GetHashCode()。基于散列的容器使用散列码来优化查询。每一个对象都会产生一个称做散列码的整数值。基于散列码的值,对象会被装进一些“散列桶(bucket)”中。要搜索一个对象,我们会请求它的键,并在其对应的“散列桶”中搜索。在.NET中,每一个对象都有一个散列码,其值由System.Object.GetHashCode()决定。任何GetHashCode()的重载[21]版本都必须遵循以下三条规则:

1. 如果两个对象相等(由operator==定义),它们必须产生相同的散列码。否则,这样的散列码不能用来查找容器中的对象[22]

2. 对于任何一个对象A,A.GetHashCode()必须是一个实例不变式(invariant)。即不管在A上调用什么方法,A.GetHashCode()都必须总是返回相同的值。这可以确保放在“散列桶”中的对象总是位于正确的“散列桶”中。

3. 对于所有的输入,散列函数应该在所有整数中产生一个随机的分布。这样,我们才能从一个散列容器上获得效率的提升。

要编写一个正确且有效率的散列函数,我们需要对类型有充分的知识才能确保遵循第3条规则。System.Object和System.ValueType中定义的GetHashCode()没有这方面的优势,因为它们必须在对具体类型几乎一无所知的情况下,提供最合适的默认行为。Object.GetHashCode()使用System.Object中的一个内部字段来产生散列值。系统创建的每一个对象在创建时都会有一个唯一的对象键(一个整数值)。这些键从1开始,每创建一个新的对象(任何类型),便会随之增长。对象标识字段会在System.Object构造器中设置,并且之后不能被更改。对于一个给定的对象,Object.GetHashCode()会返回该值作为散列码。

现在让我们对照上述三条规则,逐一检查Object.GetHashCode()方法。在没有重写operator==的情况下,如果两个对象相等[23],那么Object.GetHashCode()会返回同样的散列值。System.Object的operator==()判断的是对象标识。GetHashCode()会返回内部的对象标识字段。这样做是可行的。但是,如果我们实现了自己的operator==,那就也必须实现GetHashCode(),以确保遵循第一条规则。有关相等判断,参见条款9。

Object.GetHashCode()默认会满足第二条规则:在对象创建之后,其散列码永远不会改变。

第三条规则:对于所有的输入,散列函数应该在所有整数中产生一个随机的分布,Object.GetHashCode()不能被满足该规则。除非我们创建了数量非常多的对象,否则一个数值序列在所有整数中不会是一个随机的分布。Object.GetHashCode()返回的散列码会集中在整数范围的低端。

这意味着Object.GetHashCode()的实现是正确的,但是效率不够好。如果我们基于自己实现的一个引用类型创建了一个散列表,System.Object的默认行为可以使我们的散列表正常工作,但是会比较慢。当我们创建的引用类型要用于散列键时,我们应该重写其GetHashCode()方法,使其产生的散列值在所有整数范围内有一个良好的分布。

在谈论如何实现我们自己的GetHashCode()方法之前,我们先来对照上述三条规则,看看ValueType.GetHashCode()方法。System.ValueType重写了GetHashCode()方法,为所有值类型提供了一种默认的实现。默认的实现会返回类型中定义的第一个字段的散列码。看下面的例子:

public struct MyStruct

{

private string _msg;

private int _id;

private DateTime _epoch;

}

MyStruct对象返回的散列码实际等同于_msg字段的散列码。下面的代码将总是返回true:

MyStruct s = new MyStruct();

return s.GetHashCode() == s._msg.GetHashCode();

第一条规则为:两个相等(由operator==()定义)的对象也必须有相同的散列码。在绝大多数情况下,我们的值类型会满足该规则。但是我们也可能会破坏该规则,就像我们在引用类型中所做的那样。默认情况下,ValueType.operator==()会比较结构中的第一个字段和其他每个字段。这将可以满足第一条规则。如果我们在自己的结构中定义的operator==使用了第一个字段,那么也可以满足第一条规则。但是,如果结构的第一个字段没有参与类型的“相等判断”,那就违背了第一条规则,从而会破坏GetHashCode()方法[24]

第二条规则要求散列码必须是一个实例不变式。只有当结构中的第一个字段是一个常量字段时,该规则才会被满足。如果结构的第一个字段发生变化,那么结构的散列码也会随之改变。这就打破了该规则。是的,只要结构中的第一个字段在对象的生存期中发生了改变,那么GetHashCode()方法将遭到破坏。这是“尽可能实现具有常量性的值类型”的又一个理由(参见条款7)。

第三条规则依赖于第一个字段的类型,以及其使用情况。如果第一个字段会在所有的整数中产生一个随机的分布,同时第一个字段也会随机分布于结构的所有实例中,那么GetHashCode()将会为结构产生一个比较均匀的分布。但是,如果第一个字段经常都是相同的值,那就将违反第三条规则。下面对前面的代码做了一个很小的更改:

public struct MyStruct

{

private DateTime _epoch;

private string _msg;

private int _id;

}

如果_epoch字段被设置为当前的日期(不包括时间),那么在一个特定日期中创建的MyStruct对象将拥有相同的散列码。这就会破坏散列码的均匀分布。

条款10:理解GetHashCode()方法的缺陷  67

我们来总结一下Object.GetHashCode()方法的默认行为:对于所有的引用类型,该方法会正常工作,虽然它并不必然会产生一个高效的分布。(如果我们重写了Object.operator ==(),将会破坏GetHashCode()方法。)只有在结构类型的第一个字段是只读的情况下,ValueType类型的GetHashCode()才会正常工作。只有当第一个字段包含的值分布于“其输入的一个有意义的子集上”时,ValueType.GetHashCode()才会产生一个比较高效的散列码。

如果我们要创建一个更好的散列码,就需要为类型添加一些约束。我们再来对照上述三条规则,看看如何创建一个能够良好工作的GetHashCode()实现。

首先,如果两个对象相等(由operator==()定义),它们必须返回相同的散列码。任何用来产生散列码的属性或者数据值,都必须参与到类型的相等判断中。很明显,这意味着会有相同的属性同时用于相等判断和散列码计算中。但并非所有参与相等判断的属性,都会用来做散列码计算。System.ValueType的默认行为就是这样。但是,这又通常意味着会违反第三条规则。同样的数据元素应该同时参与到两种计算中。

第二条规则是GetHashCode()的返回值必须是一个实例不变式。假设我们定义了如下的引用类型:

public class Customer

{

private string _name;

private decimal _revenue;

public Customer( string name )

{

_name = name;

}

public string Name

{

get { return _name; }

set { _name = value; }

}

public override int GetHashCode()

{

return _name.GetHashCode();

}

}

再假设我们执行了如下的代码:

Customer c1 = new Customer( "Acme Products" );

myHashMap.Add( c1, orders );

// Name错了:

c1.Name = "Acme Software";

在上面的代码中,c1会在myHashMap中的某个地方丢失。当我们将c1放入myHashMap中时,散列码会根据字符串"Acme Products"来产生。但在我们将Name更改为"Acme Software"之后,散列码也会更改。新的散列码会根据"Acme Software"来产生。这时候c1仍然存储在由"Acme Products"定义的“散列桶”中,虽然实际上它应该存储在由"Acme Software"定义的“散列桶”中。我们将会在集合中失去c1这个Customer。丢失的原因在于散列码不再是一个对象不变式。因为在存储对象之后,我们更改了它的“散列桶”。

只有在Customer是一个引用类型时,上述问题才会出现。值类型的行为有所不同,但是仍然会引起问题。如果Customer是一个值类型,将有一个c1的副本被存储在myHashMap中。最后一行对Name的改变将不会对存储在myHashMap中的副本产生任何效果。由于装箱和拆箱都会导致复制,因此“想在一个值类型对象被添加到一个集合中之后,再改变其成员”几乎是不可能的。

解决第二条规则的唯一方式就是让散列码函数根据对象中的常量属性(一个或多个)返回一个值。System.Object使用对象标识来遵循该规则,因为对象标识不可能改变。System.ValueType希望我们类型中的第一个字段不会改变。如果不将类型实现为常量类型,我们将无法很好地满足该规则。当我们定义的值类型要用作一个散列集合中的键时,它必须是一个常量类型。如果不遵循这种推荐,将类型作为键的用户将有可能破坏散列表。下面的代码对Customer类进行了修正,让我们能够在改变它的同时维持其Name属性的常量性:

条款10:理解GetHashCode()方法的缺陷  68

public class Customer

{

private readonly string _name;

private decimal _revenue;

public Customer( string name ) :

this ( name, 0 )

{

}

public Customer( string name, decimal revenue )

{

_name = name;

_revenue = revenue;

}

public string Name

{

get { return _name; }

}

// 改变Name,返回一个新对象:

public Customer ChangeName( string newName )

{

return new Customer( newName, _revenue );

}

public override int GetHashCode()

{

return _name.GetHashCode();

}

}

将Name实现为常量属性后,我们改变Customer.Name属性的方式必须做如下的变动:

Customer c1 = new Customer( "Acme Products" );

myHashMap.Add( c1,orders );

// Name错了:

Customer c2 = c1.ChangeName( "Acme Software" );

Order o = myHashMap[ c1 ] as Order;

myHashMap.Remove( c1 );

myHashMap.Add( c2, o );

我们必须删除原来的Customer,改变其名字,然后再将新的Customer对象添加到散列表中。这看起来要比第一个版本麻烦,但是它可以正常工作。前一个版本会导致程序员编写不正确的代码。通过将“用于散列码计算的属性”强制实现为常量属性,我们可以确保正确的行为。类型的用户将不可能在这个问题上犯错。是的,这个版本需要的编码更多。我们强制让开发人员编写更多的代码,因为这是编写正确代码的唯一方式。我们要确保任何用于散列码计算的数据成员都不会改变。

第三条规则要求GetHashCode()对于所有的输入,应该在所有整数中产生一个随机的分布。满足这条规则依赖于所创建的具体类型。如果有这样的万能公式存在,那么System.Object就会实现它了,可惜事实上没有。一个常用且成功的算法是对一个类型中的所有字段调用GetHashCode()返回的值进行XOR(异或)。如果我们的类型中含有可变字段,那么应该在计算时排除它们。

GetHashCode()有非常特殊的需求:相等的对象必须产生相同的散列码;散列码必须是对象不变式;必须产生一个均匀的分布以获得比较好的效率。只有具有常量性的类型,这三个需求才可能全部被满足。对于其他类型,我们要依赖默认的行为,但是要清楚理解其缺陷。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: