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>自己的,和这个也不发生关系。