【问题标题】:Right way to implement GetHashCode for this struct为此结构实现 GetHashCode 的正确方法
【发布时间】:2021-03-17 18:58:20
【问题描述】:

我想使用一个日期范围(从一个日期到另一个日期)作为字典的键,所以我编写了自己的结构:

   struct DateRange
   {
      public DateTime Start;
      public DateTime End;

      public DateRange(DateTime start, DateTime end)
      {
         Start = start.Date;
         End = end.Date;
      }

      public override int GetHashCode()
      {
         // ???
      }
   }

实现 GetHashCode 的最佳方法是什么,这样不同范围的两个对象都不会生成相同的哈希?我希望哈希冲突尽可能地降低,尽管我理解 Dictionary 仍会检查我还将实现的相等运算符,但不想过多地污染示例代码。谢谢!

【问题讨论】:

  • 也许这对你很感兴趣 - GetHashCode 的一个非常好的介绍:blogs.msdn.com/b/ericlippert/archive/2011/02/28/…
  • 除了 GetHashCode:可变结构和公共字段通常都是一个坏主意。
  • 你认为我应该改用public DateTime Start { get; set; } 的课程吗?
  • 不,您不应该允许字段发生变异根本。将它们设为privatereadonly,通过构造函数设置它们(就像你已经做的那样),并为这些字段提供只读的“get”属性。
  • @johnny5:公共字段通常公开类型的实现,而不是要使用的 API 表面。偶尔它是合适的 - 例如。 ValueTuple - 但通常不会。

标签: c# .net hash


【解决方案1】:

您可以使用有效 Java 中的方法,如 Jon Skeet 显示的 here。对于您的特定类型:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        hash = hash * 23 + Start.GetHashCode();
        hash = hash * 23 + End.GetHashCode();
        return hash;
    }
}

【讨论】:

  • 能否请您解释一下,为什么使用 17 和 23?
  • @MaxKvt 因为它们是素数
  • @Martin 与素数相乘的好处在哪里?
  • @TheincredibleJan 我的猜测是它在应用模数后改善了值的分布(在这种情况下,模数在 int 溢出时是隐式的)。我不确定这一点,但我认为(x * prime) mod y 不会对 0 和 y 之间的 x 值产生冲突。
  • 如果每个哈希都有 17 和 23 的因数,那么它们肯定可以在所有哈希之间被分解出来,这与将开始和结束的哈希码加在一起并完成是吗?
【解决方案2】:

C# 7 你可以这样做:

public override int GetHashCode() => (Start, End).GetHashCode();

ValueTuple.NET Framework 4.7.NET Core 中提供,或通过NuGet 提供。

不确定它的性能如何,但如果有任何自定义代码能够击败它,我会感到惊讶。

【讨论】:

  • 我不确定这个解决方案有多新,但是当我尝试 Visual Studio 时给了我System.HashCode.Combine(Start, End) 的代码提示。
  • 注意:System.HashCode 仅在 .Net Core 中可用
【解决方案3】:

我会相信微软在元组中的 GetHashCode() 实现,并使用这样的东西,而不需要任何愚蠢的魔法:

public override int GetHashCode()
{
    Tuple.Create(x, y).GetHashCode();
}

【讨论】:

  • 不!您绝对不能在 GetHashCode() 中创建堆对象。此代码最终会破坏应用程序的性能。
  • 元组类将添加到堆中。相反(C# 8 以上),只使用 (x, y).GetHashCode(),它在堆栈中是一个值类型。
【解决方案4】:

由于 DateTime.GetHashCode 内部是基于 Ticks 的,那么这个呢:

    public override int GetHashCode()
    {
        return unchecked((int)(Start.Ticks ^ End.Ticks));
    }

或者,由于您似乎对日期部分(年、月、日)感兴趣,而不是全部,因此此实现使用两个日期之间的天数,并且应该几乎没有冲突:

        public override int GetHashCode()
        {
            return unchecked((int)Start.Date.Year * 366 + Start.Date.DayOfYear + (End.Date - Start.Date).Days);
        }

【讨论】:

    【解决方案5】:

    不是为了让死者复活,但我来这里是为了寻找一些东西,对于更新的 C# 版本,你可以这样做

    public override int GetHashCode()
    {
        return HashCode.Combine(Start, End);
    }
    

    目前可以在此处找到源:https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/HashCode.cs

    在我的初步测试中(使用 Jon Skeets 微基准测试框架),在性能方面,它似乎与公认的答案非常相似。

    【讨论】:

    • 很想知道它在性能方面与公认的答案相比如何......
    • 我检查过了。在 10000000 次运行中,它似乎非常相似。
    • 酷,我敢打赌代码基本相同,编译器内联函数..
    • 你不需要为哈希集和字典实现一个接口吗?
    【解决方案6】:

    就像它可能对将来使用 Visual Studio Pro 的人有所帮助(不确定这是否也存在于社区版中)

    • 选择所需的属性(在您的情况下为全部)
    • 按重构(CTRL + . 或右键单击“快速操作和重构”)
    • 现在您可以选择实现 Equals 或 GetHashcode(并且可能总是采用最知名的 MS 方法来实现)

    【讨论】:

      【解决方案7】:

      类似这样的东西:) 使用不同的质数:)

      public override int GetHashCode()
      {
          unchecked  
          {
              int hash = 23;
              // Suitable nullity checks etc, of course :)
              hash = hash * 31 + Start.GetHashCode();
              hash = hash * 31 + End.GetHashCode();
              return hash;
          }
      }
      

      这不是最快的实现,但它产生了一个很好的哈希码。 Joshua bloch 指出,你也计算性能,^ 通常更快。如果我错了,请纠正我。

      请参阅 Jon Skeets impl 了解 c#:

      【讨论】:

      • -1:虽然这很好,但它非常接近从这里直接提升:stackoverflow.com/questions/263400/… 没有署名。
      • 你的四次报复性反对票完全没有必要。
      • 当人们的答案基本相同时,为什么人们不赞成这个答案并赞成 Mark Byers 的答案?
      • 我投了反对票,因为虽然最初可能在 Joshua Bloch 的书中进行了讨论,但所提供的 C# 代码显然是 Jon Skeet 的实现(包括评论),它没有归功于它。 Mark Byers 的回答 DID 归功于代码源。
      • 现在开始了。删除我的反对票。下一次,不要让欧比旺比我们想象的更强大。
      【解决方案8】:

      结合 Jon Skeet 的 answer 和对 question 的评论(所以请不要对此投票,只是合并):

      struct DateRange
      {
          private readonly DateTime start;
      
          private readonly DateTime end;
      
          public DateRange(DateTime start, DateTime end)
          {
              this.start = start.Date;
              this.end = end.Date;
          }
      
          public DateTime Start
          {
              get
              {
                  return this.start;
              }
          }
      
          public DateTime End
          {
              get
              {
                  return this.end;
              }
          }
      
          public static bool operator ==(DateRange dateRange1, DateRange dateRange2)
          {
              return dateRange1.Equals(dateRange2);
          }
      
          public static bool operator !=(DateRange dateRange1, DateRange dateRange2)
          {
              return !dateRange1.Equals(dateRange2);
          }
      
          public override int GetHashCode()
          {
              // Overflow is fine, just wrap
              unchecked
              {
                  var hash = 17;
      
                  // Suitable nullity checks etc, of course :)
                  hash = (23 * hash) + this.start.GetHashCode();
                  hash = (23 * hash) + this.end.GetHashCode();
                  return hash;
              }
          }
      
          public override bool Equals(object obj)
          {
              return (obj is DateRange)
                  && this.start.Equals(((DateRange)obj).Start)
                  && this.end.Equals(((DateRange)obj).End);
          }
      }
      

      【讨论】:

      • 当然,现在看,似乎也覆盖 'Equals()'、'operator==' 和 'operator!=' 是个好主意。
      猜你喜欢
      • 1970-01-01
      • 2014-01-30
      • 1970-01-01
      • 2011-02-13
      • 2010-10-05
      • 1970-01-01
      • 2012-02-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多