Что было бы хорошим хэш-кодом для класса DateRange - PullRequest
1 голос
/ 20 августа 2010

У меня есть следующий класс

public class DateRange
{
    private DateTime startDate;
    private DateTime endDate;
    public override bool Equals(object obj)
    {
        DateRange other = (DateRange)obj;
        if (startDate != other.startDate)
            return false;
        if (endDate != other.endDate)
            return false;
        return true;
    }
    ...
}

Мне нужно сохранить некоторые значения в словаре с ключом DateRange, например:

Dictionary<DateRange, double> tddList;

Как переопределить метод GetHashCode()DateRange класса?

Ответы [ 5 ]

6 голосов
/ 20 августа 2010

Я использую этот подход из Effective Java для объединения хэшей:

unchecked
{
    int hash = 17;
    hash = hash * 31 + field1.GetHashCode();
    hash = hash * 31 + field2.GetHashCode();
    ...
    return hash;
}

Нет причины, по которой в этой ситуации не должно работать нормально.

5 голосов
/ 20 августа 2010

Это зависит от того, с какими значениями я ожидаю увидеть его использование.

Если оно чаще всего будет иметь разные значения дня, а не разное время в один и тот же день, и они были в пределах столетияТеперь я бы использовал:

unchecked
{
    int hash = startDate.Year + endDate.Year - 4007;
    hash *= 367 + startDate.DayOfYear;
    return hash * 367 + endDate.DayOfYear;
}

Это хорошо распределяет биты с ожидаемыми значениями, уменьшая количество бит, потерянных при сдвиге.Обратите внимание, что хотя бывают случаи, когда зависимость от простых чисел может быть на удивление плохой при столкновениях (особенно когда хеш-код подается во что-то, использующее модуль одного и того же простого числа в попытке избежать коллизий при создании еще меньшего хеш-кода для распределения по его сегментамЯ выбрал простые числа выше более очевидных вариантов, поскольку они только выше и, таким образом, все еще довольно "плотные" для распределения битов.Я не очень беспокоюсь об использовании одного и того же простого числа дважды, так как они настолько «плотные» в этом смысле, но будет больно, если у вас есть основанная на хэше коллекция с 367 сегментами.Это хорошо (но не так хорошо) относится к датам, относящимся к прошлому или будущему, но ужасно, если предположение о том, что в течение одного и того же дня будет несколько диапазонов (или различий по времени), неверно, поскольку эта информация полностью потеряна.

Если бы я ожидал (или писал для общего пользования другими сторонами, и не мог предположить иначе), я бы пошел на:

int startHash = startDate.GetHashCode();
return (((startHash >> 24) & 0x000000FF) | ((startHash >> 8) & 0x0000FF00) | ((startHash << 8) & 0x00FF0000) | (unchecked((int)((startHash << 24) & 0xFF000000)))) ^ endDate.GetHashCode();

Где первый метод работает в предположениичто универсальный GetHashCode в DateTime не так хорош, как хотелось бы, этот зависит от того, хорош ли он, но смешивается ли он в кусочках одного значения.

Это хорошо при работе с более очевидными хитрыми случаяминапример, два значения одинаковы или общее расстояние друг от друга (например, много диапазонов 1 дня или 1 часа).Это не так хорошо в тех случаях, когда первый пример работает лучше всего, но первый полностью отстой, если есть много диапазонов, использующих один и тот же день, но в разное время.


Редактировать: Чтобы дать большеподробный ответ на озабоченность Дура:

Дур правильно указывает на то, что некоторые ответы на этой странице теряют данные.Дело в том, что все они теряют данные.

Класс, определенный в вопросе, имеет 8,96077483 × 10 37 различных допустимых состояний (или 9,95641648 × 10 36 , если мыне заботится о DateTimeKind каждой даты), и вывод GetHashCode имеет 4294967296 возможных состояний (одно из которых - ноль - также будет использоваться в качестве хеш-кода нулевого значения, которое обычно можно сравнить с реальнымкод).Что бы мы ни делали, мы уменьшаем информацию в масштабе 2.31815886 × 10 27 .Мы потеряли много информации!

Вероятно, мы можем потерять больше с некоторыми, чем с другими.Конечно, легко доказать, что некоторые решения могут потерять больше, чем другие, написав правильный, но очень плохой ответ.

(Худшее возможное правильное решение - return 0;, которое действительно какон никогда не ошибается или не совпадает на равных объектах, но настолько низок, насколько это возможно, поскольку он сталкивается для всех значений. Производительность коллекции на основе хеш-функции становится O (n), и медленной, поскольку O (n) идет, так как константы вышечем такие операции O (n), как поиск в неупорядоченном списке).

Трудно измерить, сколько было потеряно.Насколько больше смещение некоторых битов до XOR теряет, чем обмен битов, учитывая, что XOR уменьшает вдвое количество оставшейся информации.Даже наивный x ^ y не теряет больше, чем swap-and-xor, он просто сталкивается с общими ценностями;swap-and-xor будет конфликтовать со значениями, в которых не используется plain-xor.

Как только мы получим выбор между решениями, которые не теряют гораздо больше информации, чем возможно, но возвращают 4294967296 или близки к 4294967296 возможным значениямс хорошим распределением между этими значениями, вопрос больше не в , сколько информации потеряно (ответ, что остается только 4,31376821 × 10 -28 исходной информации), но какая информация потеряна.

Вот почему мое первое предложение выше игнорирует временные компоненты.В день происходит 864000000000 «тиков» (100-наносекундных единиц измерения DateTime), и я специально выбрасываю два куска этих тиков (7,46496 × 10 23 возможных значений между этими двумя), потому что яЯ думаю о сценарии, где эта информация не используется в любом случае.В этом случае я намеренно структурировал механизм таким образом, чтобы выбрать , что информации теряется, что улучшает хеш для данной ситуации, но делает его абсолютно бесполезным, если у нас были разные значения с самого началаи даты окончания происходят не в одни и те же дни, но в разное время.

Точно так же x ^ y не теряет больше информации, чем любая другая, но информация, которую он теряет, с большей вероятностью будет значимой, чемс другими вариантами.

В отсутствие какого-либо способа предсказать, какая информация может иметь важное значение (особенно, если ваш класс будет общедоступным, а его хэш-код используется внешним кодом), тогда мы более ограниченыв предположениях, которые мы можем сделать безопасно.

В целом, методы Prime-Mult или Prime-Mod лучше, в которых они теряют информацию, чем методы на основе сдвига, за исключением случаев, когда то же самое простое число используется для дальнейшего хеширования, котороеможет происходить внутри метода, основанного на хэше, по иронии судьбы с той же целью (нетномер относительно прост для себя!даже простые числа), в этом случае они намного хуже.С другой стороны, методы, основанные на смене, действительно теряют силу, если их вводить в дополнительный хеш на основе смены.Не существует идеального хэша для произвольных данных и произвольного использования (за исключением случаев, когда класс имеет несколько допустимых значений, и мы сопоставляем их все, и в этом случае это более строго кодировка, чем хеш, который мы создаем).

Короче, вы потеряете информацию, что бы вы ни делали, это , что вы потеряете, это важно.

4 голосов
/ 20 августа 2010

Хорошо, рассмотрим, какими характеристиками должна обладать хорошая хеш-функция. Это должен :

  • быть в согласии с Equals - то есть, если Equals имеет значение true для двух объектов, то оба хеш-кода также должны быть одинаковыми.
  • никогда не падает

И это должно :

  • Быть очень быстрым
  • дает разные результаты для аналогичных входных данных

Что бы я сделал, это придумал очень простой алгоритм; скажем, беря 16 бит из хеш-кода первого и 16 бит из хэш-кода второго, и объединяя их вместе. Сделайте себе тестовый набор репрезентативных образцов; диапазоны дат, которые, вероятно, будут фактически использоваться, и посмотрите, дает ли этот алгоритм хорошее распределение.

Обычный выбор - переписать два хэша вместе. Это не обязательно хорошая идея для этого типа, потому что кажется вероятным, что кто-то захочет представить диапазон нулевой длины, который идет от X до X. Если вы xor хэши двух равных DateTimes, вы всегда получаете ноль, который выглядит как рецепт для множества коллизий хешей.

1 голос
/ 20 августа 2010

Вы должны сместить один конец диапазона, иначе две равные даты будут хэшироваться до нуля, довольно распространенный сценарий, который я себе представляю:

return startDate.GetHashCode() ^ (endDate.GetHashCode() << 4);
0 голосов
/ 20 августа 2010
return startDate.GetHashCode() ^ endDate.GetHashCode();

может быть хорошим началом.Вы должны убедиться, что вы получаете хорошее распределение, когда между startDate и endDate одинаковое расстояние, но разные даты.

...