Правильный способ реализации GetHashCode для этой структуры

Я хочу использовать диапазон дат (от одной даты до другой даты) в качестве ключа для словаря, поэтому я написал свою собственную структуру:

   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 < > все равно проверит оператор равенства, который я также буду реализовывать, но не хотел слишком сильно загрязнять код примера. Спасибо!

Ответ 1

Вы можете использовать этот метод из Effective Java, поскольку Jon Skeet показывает здесь. Для вашего конкретного типа:

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;
    }
}

Ответ 2

Я бы доверял Microsoft реализацию GetHashCode() в кортежах и использовал что-то вроде этого без какой-либо глупой магии:

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

Ответ 3

Так как 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);
        }

Ответ 4

Что-то вроде этого:) с другим простым числом:)

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 для С#:

Ответ 5

В C# 7 вы можете сделать это:

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

ValueTuple доступен в .NET Framework 4.7 и .NET Core или через NuGet.

Не уверен, насколько хорошо он работает, но я был бы удивлен, если бы какой-то пользовательский код побил его.

Ответ 6

Сочетание Jon Skeet ответ и комментарий к вопросу (так что, пожалуйста, ни одно голосование по этому поводу, просто консолидации):

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);
    }
}