class Student
//...别的不写了
public override int GetHashCode() => id;
然后再带回去尝试,我们会发现……
As FUTILE as it ever been.
原来此魔非彼魔
其实,就别说HashSet
了,连object.Equals
和==
都不用HashCode来判断是否相等,如果只重写GetHashCode
的话,我们甚至会发现==
和Equals
完全不受影响。
但是看这位朋友的语气并不像子虚乌有,于是我特地去msdn上又查阅了一下object.GetHashCode()
,结果得到了一个有趣的说法:
重写 GetHashCode() 的派生类还必须重写 Equals(Object),以保证视为相等的两个对象具有相同的哈希代码;否则,Hashtable 类型可能无法正常工作。
看来这位朋友把HashSet
和Hashtable
弄混了。两者虽然都有个Hash
,但其实除了都位于System.Collections
这一大的命名空间下之外,几乎一点关系都没有。
HashSet<T>
是位于System.Collections.Generic
下的泛型容器,而Hashtable
是位于System.Collections
下的非泛型容器(Non-generic Collection)。而且,前者也并非后者的泛型版本,事实上,Hashtable
对应的泛型版本是Dictionary<TK,TV>
。
也就是说,Hashtable
其实类似于Dictionary<object,object>
(尽管实际上不是),这也就意味着,Hashtable
的元素也是KV对(Key-Value Pair,键值对)。
Hashtable
既然类似于Dictionary
,那么Hashtable
也要保证一种唯一性——Key的唯一性,为了保证键值的唯一性,Hashtable
使用GetHashCode
函数的结果是否相等作为判断依据,当有特别的判等需求时,可改写GetHashCode
做适配。
不过实际上msdn还提到了GetHashCode
的重载和Equals
函数之间的关联规范。上面引用的部分也提到了,本文不对此做过多阐述。
也许我们一开始就找错了组合……
感谢@巧克力苏菜在评论的内容,这个评论太长了这里不粘原文了,而且主要内容是代码。
还记得上面那位朋友刚刚说过的那件事么??我们查阅msdn之后得到了有趣的结果:
重写 GetHashCode() 的派生类还必须重写 Equals(Object),以保证视为相等的两个对象具有相同的哈希代码;否则,Hashtable 类型可能无法正常工作。
对,那么……同样的做法对HashSet
类型会不会生效呢??
根据这位朋友的测试结果,是的,这完全可行,并且这位朋友道出了其中的原委:
需要两个方法同时重写才可以,添加时时先调用GetHashCode如果相同再调用Equals只有两者都不同时才加不进去,如果GetHashCode不相同,则会直接添加进去。
我们只是讨论了Equals
和==
的重写以及GetHashCode
的单独重写,但是基于这个说明结果看起来我们忘记了把这两个组合起来考虑,于是把这个结果漏掉了,惭愧惭愧……orz
不过话说回来,我个人建议,在一些场合下,虽然很麻烦,但最好使用独立的比较器IEqualityComparer<T>
,重写Equals
和GetHashCode
是一个需要慎重考虑的决定,因为这两个函数一经重写会影响很多种容器重复性判别或引用判别,有些时候我们只希望对某些情形采取这种考虑方式,在其他的情形下我们可能使用其他的判别模式,而使用这种专用的比较器可以很好的避免改写所带来的污染。
圣剑背后的故事(真实印记番外)
其实,尽管Hashtable
使用GetHashCode()
,但泛型版本的Dictionary<TK,TV>
却依然使用IEqualityComparer<TK>
进行TK
类型的相等性判断。
为什么会这样??
其实,无论是HashSet<T>
还是Dictionary<TK,TV>
,这两个泛型容器实际上都有一个属性叫做Comparer
,类型为IEqualityComparer<T>
。
不过,这并不是因为他们共同继承了什么类,也不是因为他们共同实现了什么接口,这只是这两种容器相仿的唯一性所带来的一个巧合。
HashSet<T>
希望其中的T
类型元素具有唯一性,而Dictionary<TK,TV>
则希望TK
类型的键值具有唯一性,然后很巧合的都使用了泛型版本的相等比较器IEqualityComparer<T>
(实际上这个接口有个非泛型版本,但这里不做介绍)。
而HashSet<T>
的其中一个构造函数:
HashSet<T>(IEqualityComparer<T>);
这个函数的参数实际上就是给了Comparer
属性。
当然,您可能要问:
如果我使用了没有此类型参数版本的构造函数,那这个属性会是null
么??
答案是否定的,实际上用调试器观察会发现,当什么也不给Comparer
的时候,Comparer
被描述为一个类型为System.Collections.Generic.ObjectEqualityComparer<T>
的类型,但是很有趣的是,无论是在msdn上还是在对象浏览器中,都找不到名为ObjectEqualityComparer<T>
的类型,虽然原因不明,但推测是被巨硬写成了private class
。
原来圣剑与生俱来
事实上,关于这一点,msdn上也是有所提示的:
HashSet()
Initializes a new instance of the HashSet class that is empty and uses the default equality comparer for the set type.
就是说,即使用无参的构造函数,HashSet
实例还是会被分配一个默认的IEqualityComparer<T>
,这也就得出了这样的结论:无论哪种情况下,object.GetHashCode()
和HashSet
都是没有什么关系的。
而实际上,IEqualityComparer<T>
接口要求我们必须实现两个函数:Equals
和GetHashCode
,但这两个函数是IEqualityComparer<T>
自己的,和这个也不发生关系。