`
dawuafang
  • 浏览: 1156855 次
文章分类
社区版块
存档分类
最新评论

C# Dictionary中做Key的类应该注意重写getHashCode和Equals

 
阅读更多

Effective C# Item 10: Understand the Pitfalls of GetHashCode() 读后感

下面的内容中有很多一部分是笔者自己的想法,所以有些说法可能会有失偏颇,还望指正。

Wanger说GetHashCode()是他在Effective C#所有的50个建议中唯一一项关于不推荐函数的建议。GetHashCode()这个方法只会用于一个地方:给基于Hash的Collection(比如HashTable和Dictionary)的Key定义Hash值,也就是对象做为Key,而对象的GetHashCode()为该Key获取Hash值。
说到Hash,先说一下Hash查找算法。
我们知道大部分的查找算法,顺序查找,二分查找或者是B-Tree查找,由于查找的关键字和待查找的记录地址之间没有必然的联系,都是基于比较的,无非是在比较的次数上有多有少。最理想的算法当然是不比较,能够直接通过关键字得到待查找的记录的地址,这就是Hash查找了。通过Hash函数,将记录的Key值直接对应到记录的地址,一个Key对应一个地址,由于Key值是某种语言中所有允许标志符的全集,比某个程序中可能用到的地址空间大很多,这样就势必会有不同的Key值对应相同地址的情况,良好的Hash算法要求,经过Hash函数运算由Key得到的地址空间是均匀的,也就是地址空间是随机的。但是不可能完全避免上述情况,于是又有各种侦探和解决地址冲突的算法。
OK,上面的内容是数据结构的基础知识,大家应该都知道,不过做为一个完整的读后感,不提这些好像不完整,如果熟知的话,可以略过以上内容。
对应到HashTable 中,自定义的类就是Key,而GetHashCode()就是那个Hash算法,GethHashCode()得到的值就是HashTable存储的对象的内存地址。
于是就有Wanger提到的一个良好的GetHashCode()必须满足的三个条件。
1.两个对象Equals,则通过GetHashCode()得到的值必须相同。
对象做为Key值,必须对应一个Value对象,如果得到的值不同,则会出现一个Key对应多个Value对象的情况,这违背了Key的原始意义,不过如果你非要违背这个原则,在HashTable的语法中也是没有任何问题的(这个在后面会举例论述)只不过违背了Key的语意。
2.通过GetHashCode()得到的值必须是恒定不变的。
这个很明显,如果在存储以后这个值可以随意变动,在通过Key取Value的时候就会有问题,在语法上会报经典的“未将对象引用设置到对象实例”。
3.通过GetHashCode()得到的值必须在整数的取值范围内是均匀分布的。
这样做的目的是提高HashTable的查找效率。
Wanger随后给出了Object的GetHashCode()和ValueType的GetHashCode()的算法,以及是否满足上述三条原则。
下面简要叙述一下,有兴趣的可以看一下原著。
Object,默认的Equals()是通过Object创建时生成的Identity来比较的,而GetHashCode()是从1开始递增的序列值,所以如果Equals相等,则GetHashCode()必然相等。当然第二条也能满足,因为没办法修改这个GetHashCode的值。第三条就不能满足了,除非是创建大量的Object。
ValueType,Equals()是通过比较各个字段的值,而GetHashCode()是通过比较第一个字段的值来实现的,这样也能保证第一条。第二条就不一定能保证了,如果第一个字段不是Readonly的,那由它得到的HashCode也是变化的。第三条规则取决于第一个字段怎么使用。

下面举个简单的例子。

/// <summary>
/// 做为键的类
/// </summary>
class keyClass
{
private string name;
public string _name
{
get
{
return name;
}
set
{
name = value;
}
}
private string code;
public string _code
{
get
{
return code;
}
set
{
code = value;
}
}
public override bool Equals(object obj)
{
if(null == obj)
return false;
if(obj.GetType()!=this.GetType())
return false;

return(((keyClass)obj)._name.Equals(this._name));
}
public override int GetHashCode()
{
return this._code.GetHashCode();
}


}

测试类

/// <summary>
/// 测试键值的Hashtable
/// </summary>
class HashTableTest
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
keyClass testKey = new keyClass();
testKey._code = "110";
testKey._name = "222";

keyClass testKey2 = new keyClass();
testKey2._code = "111";
testKey2._name = "222";

System.Collections.Hashtable aa = new Hashtable();
aa.Add(testKey,"test");
aa.Add(testKey2,"test2");

Console.WriteLine(aa[testKey].ToString());
Console.WriteLine(aa[testKey2].ToString());

Console.ReadLine();

}
}

单步跟踪一下上述代码就会发现HashTable创建和查找的过程。

创建:

首先根据键对象的GetHashTable()(当然在创建HashTable的时候可以制定其他的Hash函数做为寻址函数,这里不予讨论)得到HashCode,如果在存储桶中该地址没有被占用,则将其存入其中,如果占用了则调用当前Key对象的Equals方法判断占用该地址的对象跟当前Key对象是否是同一对象,如果是则抛出异常,说该项已经存在于HashTable中(我不知道是否有办法在存储桶中存储两个HasCode和Key值都相同的对象,不过理论上应该是不允许的)。如果不是同一对象则在存储桶中另外找个地方把对象存起来。

查找

首先根据键对象的GetHashTable()(当然在创建HashTable的时候可以制定其他的Hash函数做为寻址函数,这里不予讨论)得到HashCode,然后将存储桶中对应的key对象跟当前的Key值通过Equals方法比较,看是否为同一对象,如果不同则继续查找。

靠,这不就是Hash算法的过程嘛!
对啊,我也没说不是啊。
那你直接说跟Hash的查找的算法一样不就行了
唉,不是要凑篇幅嘛!

自从.NET Framework 2.0引入泛型之后,对集合的使用就开创了新的局面。首先我们不用考虑类型是否安全,利用泛型以及对泛型参数的约束完全可以保障这一点;其次,集合元素不会因为频繁的Boxing和Unboxing而影响集合遍历与操作的性能。泛型带来的这两点好处毋庸置疑。在Dictionary<TKey, TValue>中,除了字符串,我们普遍会使用值类型作为它的key,例如int类型。而枚举类型作为一种值类型,在某些时候特别是需要位操作的时候,也会经常用作key。问题就出现在这里。

我们知道,Dictionary的key必须是唯一的标识,因此Dictionary需要对key进行判等的操作,如果key的类型没有实现 IEquatable接口,则默认根据System.Object.Equals()和GetHashCode()方法判断值是否相等。我们可以看看常用作key的几种类型在.NET Framework中的定义:

public sealed class String : IComparable, ICloneable, IConvertible,
IComparable<string>, IEnumerable<string>, IEnumerable,
IEquatable<string>

public struct Int32 : IComparable, IFormattable,
IConvertible, IComparable<int>, IEquatable<int>

public abstractclass Enum : ValueType,
IComparable, IFormattable, IConvertible

注意Enum类型的定义与前两种类型的不同,它并没有实现IEquatable接口。因此,当我们使用Enum类型作为key值时,Dictionary的内部操作就需要将Enum类型转换为System.Object,这就导致了Boxing的产生。没错,我们很难发现这个陷阱,它是导致Enum作为 key值的性能瓶颈。

我们该如何解决这一问题?最简单的方法是将Enum的值先转换为int,然后将其作为key传入Dictionary中。还有一种作法是定义一个实现了IEqualityComparer<T>接口的类。因为Dictionary构造函数的其中一个重载版本,可以接收 IEqualityComparer<T>类型,通过它完成对key的判断。IEqualityComparer<T>接口的定义如下所示:

public interface IEqualityComparer<T>
{
bool Equals(T x, T y);
int GetHashCode(T obj);
}

遗憾的是我们却不能直接提供针对Enum的实现,例如:

class EnumComparer<TEnum> : IEqualityComparer<TEnum>
{
public bool Equals(TEnum x, TEnum y)
{
return (x == y);
}
public int GetHashCode(TEnum obj)
{
return (int)obj;
}
}

因为我们不能直接对泛型类型进行==操作,以及将泛型对象强制转换为int类型。在Code Project上,有一篇名为Accelerating Enum-Based Dictionaries with Generic EnumComparer的文章,利用Reflection.Emit实现Equals()和GetHashCode()方法。不过在该文的评论中,提供了更好的一个方法,就是利用C# 3.0的Lambda表达式:

public class EnumComparer<T> : IEqualityComparer<T> where T : struct
{
public bool Equals(T first, T second)
{
var firstParam = Expression.Parameter(typeof(T),"first");
var secondParam = Expression.Parameter(typeof(T),"second");
var equalExpression = Expression.Equal(firstParam, secondParam);

return Expression.Lambda<Func<T, T, bool>>
(equalExpression, new[] { firstParam, secondParam }).
Compile().Invoke(first, second);
}

public int GetHashCode(T instance)
{
var parameter = Expression.Parameter(typeof(T),"instance");
var convertExpression = Expression.Convert(parameter, typeof(int));

return Expression.Lambda<Func<T, int>>
(convertExpression, new[]{parameter}).
Compile().Invoke(instance);
}
}

此时,我们就可以如此使用Dictionary对象:

public enum DayOfWeek{//...}
var dictionary = new Dictionary<DayOfWeek, int>(new EnumComparer<DayOfWeek>());

采取这样的做法比直接用Enum类型作为Dictionary的key差不多要快8倍。这难道不让人为之惊诧吗?

class LongArray
{
public long[] array;

public LongArray(long[] arrays)
{

array = arrays;
}


public override int GetHashCode()
{
int Result = 0;

for (var i = 0; i < array.Length; i++)
{
Result += (i + 1) * (int)array[i];
}

return Result;
}


public override bool Equals (Object Arrays) //重写Equals方法
{
var data = ((LongArray)Arrays).array;

if (data.Length == array.Length)
{
for (var i = 0; i < array.Length; i++)
{
if (data[i] == array[i])
{
continue;
}
else
{
return false;
}
}
return true;
}

return false;
}
}

本文出自 “晴窗笔记” 博客,请务必保留此出处http://wayfarer.blog.51cto.com/1300239/280088

分享到:
评论

相关推荐

    C# Equals 和 GetHashCode 方法重写

    ### C# Equals 和 GetHashCode 方法重写 在C#编程中,`Equals` 和 `GetHashCode` 方法是非常重要的成员方法,它们对于确保对象的正确比较以及高效地存储和检索对象至关重要。这两个方法通常需要在自定义类中进行...

    C# Dictionary去重算法

    为了实现在Dictionary中去重,我们可以采取一种巧妙的方法,即通过自定义一个包装类并重写 `Equals` 和 `GetHashCode` 方法来实现。下面我们将深入探讨这个方法的原理和步骤。 首先,创建一个名为 `ValueWrapper&lt;T&gt;...

    dotnet C# 实现 GetHashCode 的方法.rar

    在.NET框架中,`GetHashCode`方法是C#编程中一个非常重要的概念,它主要用于哈希表(如字典Dictionary&lt;TKey, TValue&gt;)等数据结构的高效查找操作。`GetHashCode`方法返回对象的哈希码,这个哈希码是一个整数值,用于...

    c#重写HashTable

    在C#编程中,`HashTable`是一个非泛型集合类,它在.NET Framework早期版本中被广泛使用。然而,随着.NET Framework的不断发展,`HashTable`逐渐被更安全、类型安全且性能更高的`Dictionary&lt;TKey, TValue&gt;`所取代。...

    12.0_c# 哈希表示例代码

    C#中的`Dictionary&lt;TKey, TValue&gt;`类就是一种典型的哈希表实现,它提供了O(1)的时间复杂度进行查找、插入和删除操作。 1. **C#中的哈希函数** 在C#中,我们可以使用`GetHashCode()`方法来获取对象的哈希值。这个...

    C#中Dictionary类使用实例

    然而,需要注意的是,如果键不唯一,或者键的 `Equals` 和 `GetHashCode` 方法未正确重写,可能会导致意外的结果。 总的来说,C# 中的 `Dictionary` 类是处理键值对数据的强大工具,通过实例化 `Dictionary` 并利用...

    C#稳定性编程注意事宜.docx

    5. **Dictionary的键**:`Dictionary&lt;TKey, TValue&gt;`依赖于键的`GetHashCode`和`Equals`方法来定位元素。重写这两个方法时,要确保它们满足哈希表的约定,即两个相等的对象必须具有相同的哈希码,并且不等的对象应有...

    数据结构示例程序(C#语言描述).zip

    此外,自定义数据结构通常涉及类或结构的设计,以及重写Equals()、GetHashCode()和ToString()方法来确保正确的行为。 压缩包内的“zyqmv”可能是指一个或多个源代码文件,它们提供了具体的数据结构实现示例。通过...

    c#课程设计-模仿手机通讯录

    可能还需要一些方法,如ToString()以方便显示联系人信息,以及重写Equals()和GetHashCode()方法,用于比较和查找联系人。 5. **用户界面**:设计友好的用户界面是提升用户体验的关键。需要创建多个窗体,如添加联系...

    提高C#编程水平的50个技巧

    39. **选择合适的集合类**:根据需求选择ArrayList、List、Dictionary&lt;TKey, TValue&gt;等。 40. **DataSet与自定义结构**:在需要存储和操作复杂数据时使用DataSet。 41. **属性(Attributes)**:为编译器和运行时...

    在C#中有效地使用列表作为字典键

    在C#编程中,列表(List)作为字典(Dictionary&lt;TKey, TValue&gt;)的键是一种不常见的用法,因为列表本身不是可哈希的,而字典依赖于键的哈希值来实现快速查找。然而,在某些特定场景下,这种技术可以提供灵活的数据...

    提高C#编程水平的50个要点

    10. **注意GetHashCode方法**:GetHashCode用于哈希表的索引,当重写Equals时,通常也需要重写GetHashCode以保持一致性。 11. **优先使用foreach循环**:foreach适用于遍历集合,简洁且易于阅读。 12. **立即初始...

    System System命名空间源码

    System命名空间中的泛型类,如List和Dictionary&lt;TKey, TValue&gt;,提供了类型安全的数据容器。源码分析可以揭示泛型如何实现类型约束和编译时的类型检查。 七、线程安全与并发 .NET框架中的线程安全主要通过System....

    调高C#编程的50个基本技巧

    实现时需要注意一致性原则,即对象的相等性(通过`Equals`方法定义)应该与其哈希码相匹配。 #### 11. 在编写循环时使用 foreach `foreach`循环简化了遍历数组或集合的过程,它自动处理索引管理和边界检查,从而...

Global site tag (gtag.js) - Google Analytics