GetHashCode扩展方法
在阅读StackOverflow上关于重写GetHashCode()
所有问题和答案之后,我编写了以下扩展方法,以便轻松方便地覆盖GetHashCode()
:
public static class ObjectExtensions { private const int _seedPrimeNumber = 691; private const int _fieldPrimeNumber = 397; public static int GetHashCodeFromFields(this object obj, params object[] fields) { unchecked { //unchecked to prevent throwing overflow exception int hashCode = _seedPrimeNumber; for (int i = 0; i < fields.Length; i++) if (fields[i] != null) hashCode *= _fieldPrimeNumber + fields[i].GetHashCode(); return hashCode; } } }
(我基本上只重构了有人在那里发布的代码,因为我真的很喜欢它可以一般使用)
我用的是这样的:
public override int GetHashCode() { return this.GetHashCodeFromFields(field1, field2, field3); }
你看到这段代码有什么问题吗?
这看起来像是一种可行的方法。
我唯一的建议是,如果你真的关心它的性能,你可能想要为几种常见情况添加通用版本(即可能是1-4个args)。 这样,对于那些对象(最有可能是小型的,键式复合对象),您将不会有构建数组以传递给方法,循环,任何通用值的装箱等的开销。调用语法将完全相同,但您将针对该情况运行略微更优化的代码。 当然,在你决定是否值得维护权衡之前,我会对此进行一些性能测试。
像这样的东西:
public static int GetHashCodeFromFields(this object obj, T1 obj1, T2 obj2, T3 obj3, T4 obj4) { int hashCode = _seedPrimeNumber; if(obj1 != null) hashCode *= _fieldPrimeNumber + obj1.GetHashCode(); if(obj2 != null) hashCode *= _fieldPrimeNumber + obj2.GetHashCode(); if(obj3 != null) hashCode *= _fieldPrimeNumber + obj3.GetHashCode(); if(obj4 != null) hashCode *= _fieldPrimeNumber + obj4.GetHashCode(); return hashCode; }
我写了一些东西,你可以解决你的问题…(实际上,它可能会改进,包括你的种子……)
无论如何,该项目名为Essence( http://essence.codeplex.com/ ),它使用System.Linq.Expression库生成(基于属性)Equals / GetHashCode / CompareTo / ToString的标准表示,以及因为能够基于参数列表创建IEqualityComparer和IComparer类。 (我还有一些进一步的想法,但希望得到一些社区反馈,然后再继续下去。)
(这意味着它几乎和手写一样快 – 主要的不是CompareTo();因为Linq.Expressions在3.5版本中没有变量的概念 – 所以你有当你没有得到匹配时,在底层对象上调用CompareTo()两次。使用Linq.Expressions的DLR扩展来解决这个问题。我想我可以使用emit il,但当时我没有受到启发。)
这是一个相当简单的想法,但我以前没有看到它。
现在的问题是,我对抛光它感兴趣(这可能包括为codeproject写一篇文章,记录一些代码等),但如果你觉得它会是某种东西我可能会被说服这样做出于兴趣。
(codeplex网站没有可下载的软件包;只需访问源代码并抓住它 – 哦,它是用f#编写的(尽管所有测试代码都在c#中),因为这是我有兴趣学习的东西。)
无论如何,这是项目中测试的c#示例:
// -------------------------------------------------------------------- // USING THE ESSENCE LIBRARY: // -------------------------------------------------------------------- [EssenceClass(UseIn = EssenceFunctions.All)] public class TestEssence : IEquatable, IComparable { [Essence(Order=0] public int MyInt { get; set; } [Essence(Order=1] public string MyString { get; set; } [Essence(Order=2] public DateTime MyDateTime { get; set; } public override int GetHashCode() { return Essence .GetHashCodeStatic(this); } ... } // -------------------------------------------------------------------- // EQUIVALENT HAND WRITTEN CODE: // -------------------------------------------------------------------- public class TestManual { public int MyInt; public string MyString; public DateTime MyDateTime; public override int GetHashCode() { var x = MyInt.GetHashCode(); x *= Essence .HashCodeMultiplier; x ^= (MyString == null) ? 0 : MyString.GetHashCode(); x *= Essence .HashCodeMultiplier; x ^= MyDateTime.GetHashCode(); return x; } ... }
无论如何,该项目,如果有人认为是值得的,需要抛光,但想法是…
我看起来很不错,我只有一个问题:遗憾的是你必须使用一个object[]
来传递值,因为这会将你发送给函数的任何值类型包装起来。 我不认为你有很多选择,除非你像其他人所建议的那样去创建一些通用的重载。
根据一般原则,你应该尽可能地将你unchecked
范围unchecked
尽可能狭窄的范围内,尽管这里并不重要。 除此之外,看起来很好。
public override int GetHashCode() { return this.GetHashCodeFromFields(field1, field2, field3, this); }
(是的,我很迂腐,但这是我看到的唯一问题)
更优化:
- 创建一个代码生成器,它使用reflection来查看业务对象字段,并创建一个新的部分类,它覆盖GetHashCode()(和Equals())。
- 程序在调试模式下启动时运行代码生成器,如果代码已更改,请退出并向开发人员重新编译。
这样做的好处是:
- 使用reflection,您知道哪些字段是值类型,因此是否需要空值检查。
- 没有额外的开销 – 没有额外的函数调用,没有列表构造等。如果您正在进行大量的字典查找,这很重要。
- 长实现(在具有大量字段的类中)隐藏在部分类中,远离重要的业务代码。
缺点:
- 如果你没有对GetHashCode()进行大量的字典查找/调用,那就太过分了。
我应该指出,在实现GetHashCode时,你几乎不应该进行分配(这里有一些 有用的 博客文章)。
params
工作方式(动态生成新数组)意味着这不是一个好的通用解决方案。 您最好使用每个字段的方法调用并将散列状态维护为传递给它们的变量(这样可以更容易地使用更好的散列函数和雪崩)。
除了使用params object[] fields
引起的问题之外,我认为在某些情况下,不使用类型信息也可能是性能问题。 假设两个类A
, B
具有相同类型和数量的字段并实现相同的接口I
现在,如果将A
和B
对象放入Dictionary
具有相同字段和不同类型的Dictionary
对象将最终位于同一个存储桶中。 我可能会插入一些语句,如hashCode ^= GetType().GetHashCode();
Jonathan Rupp接受的答案涉及params数组,但不涉及值类型的装箱。 因此,如果性能非常重要,我可能会声明GetHashCodeFromFields
没有对象而是int
参数,并且不发送字段本身,而是发送字段的哈希码。 即
public override int GetHashCode() { return this.GetHashCodeFromFields(field1.GetHashCode(), field2.GetHashCode()); }
可能出现的一个问题是当乘法命中0时,最终的hashCode始终为0,正如我刚刚体验到具有大量属性的对象,在以下代码中:
hashCode *= _fieldPrimeNumber + fields[i].GetHashCode();
我建议:
hashCode = hashCode * _fieldPrimeNumber + fields[i].GetHashCode();
或类似xor的东西是这样的 :
hashCode = hashCode * _fieldPrimeNumber ^ fields[i].GetHashCode();