12

在阅读了 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);
    }

你看到这段代码有什么问题吗?

4

9 回答 9

3

这看起来是一种可靠的方法。

我唯一的建议是,如果您真的关心它的性能,您可能希望为几种常见情况添加通用版本(即可能 1-4 args)。这样,对于那些对象(很可能是小型的、键式复合对象),您将不会有构建数组以传递给方法、循环、任何通用值的装箱等的开销。调用语法将完全相同,但您将针对这种情况运行稍微优化的代码。当然,在您决定是否值得进行维护权衡之前,我会对此进行一些性能测试。

像这样的东西:

public static int GetHashCodeFromFields<T1,T2,T3,T4>(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;
}
于 2009-04-18T19:03:38.487 回答
3

不久前我写了一些东西,你可能会解决你的问题......(实际上,它可能会被改进以包含你拥有的种子......)

无论如何,该项目称为 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<TestEssence>, IComparable<TestEssence>
    {
        [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<TestEssence>.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<TestEssence>.HashCodeMultiplier;
            x ^= (MyString == null) ? 0 : MyString.GetHashCode();
            x *= Essence<TestEssence>.HashCodeMultiplier;
            x ^= MyDateTime.GetHashCode();
            return x;
        }
    ...
    }

无论如何,这个项目,如果有人认为值得,需要打磨,但想法是存在的......

于 2009-07-11T09:48:50.770 回答
1

我看起来不错,我只有一个问题:很遗憾,您必须使用 anobject[]来传递值,因为这会将您发送给函数的任何值类型装箱。我认为您没有太多选择,除非您像其他人建议的那样走创建一些通用重载的路线。

于 2009-04-18T16:37:06.733 回答
0

一般来说,你应该unchecked尽可能地缩小你的范围,尽管这并不重要。除此之外,看起来还不错。

于 2009-04-18T16:40:58.410 回答
0
public override int GetHashCode() {
    return this.GetHashCodeFromFields(field1, field2, field3, this);
}

(是的,我很迂腐,但这是我看到的唯一问题)

于 2009-04-18T16:47:15.070 回答
0

更优化:

  1. 创建一个代码生成器,它使用反射来查看您的业务对象字段并创建一个覆盖 GetHashCode()(和 Equals())的新部分类。
  2. 当您的程序以调试模式启动时运行代码生成器,如果代码已更改,请退出并通知开发人员重新编译。

这样做的好处是:

  • 使用反射您可以知道哪些字段是值类型,因此它们是否需要空检查。
  • 没有开销 - 没有额外的函数调用,没有列表构造等。如果您要进行大量字典查找,这一点很重要。
  • 长实现(在具有大量字段的类中)隐藏在部分类中,远离您的重要业务代码。

缺点:

  • 如果您没有对 GetHashCode() 进行大量字典查找/调用,那就大材小用了。
于 2009-04-19T01:33:40.860 回答
0

我应该指出,在实现 GetHashCode 时您几乎不应该进行分配(这里有一些 有用的 博客文章)。

工作方式params(动态生成新数组)意味着这确实不是一个好的通用解决方案。您最好使用每个字段的方法调用并将散列状态作为传递给它们的变量来维护(这使得使用更好的散列函数和雪崩变得容易)。

于 2009-04-19T09:22:34.943 回答
0

除了 using 引起的问题外params object[] fields,我认为在某些情况下不使用类型信息也可能是性能问题。假设有两个类AB具有相同类型和数量的字段并实现相同的接口I。现在,如果您将AB对象放入Dictionary<I, anything>具有相同字段和不同类型的对象中,最终将在同一个存储桶中。我可能会插入一些声明,例如hashCode ^= GetType().GetHashCode();

Jonathan Rupp 接受的答案处理参数数组,但不处理值类型的装箱。因此,如果性能非常重要,我可能会声明GetHashCodeFromFields没有对象而是int参数,并且发送的不是字段本身,而是字段的哈希码。IE

public override int GetHashCode() 
{
    return this.GetHashCodeFromFields(field1.GetHashCode(), field2.GetHashCode());
}
于 2011-12-25T10:49:38.163 回答
0

可能出现的一个问题是当乘法达到 0 时,最终的 hashCode 始终为 0,正如我刚刚在以下代码中遇到的具有很多属性的对象一样:

hashCode *= _fieldPrimeNumber + fields[i].GetHashCode();

我建议:

hashCode = hashCode * _fieldPrimeNumber + fields[i].GetHashCode();

或与 xor 类似的东西,如下所示

hashCode = hashCode * _fieldPrimeNumber ^ fields[i].GetHashCode();
于 2015-01-08T00:16:32.607 回答