В одном случае использование хеш-кодов в качестве ярлыка для сравнений на равенство имеет смысл.
Рассмотрим случай, когда вы создаете хэш-таблицу или хэш-набор. Фактически, давайте просто рассмотрим хэш-наборы (хеш-таблицы расширяют это, также сохраняя значение, но это не имеет значения).
Существуют различные подходы, которые можно использовать, но во всех них у вас есть небольшое количество слотов, в которые могут быть помещены хэшированные значения, и мы используем либо открытый, либо закрытый подход (который просто для удовольствия, некоторые люди используют противоположный жаргон для других); если мы сталкиваемся в одном и том же слоте для двух разных объектов, мы можем либо сохранить их в одном и том же слоте (но иметь связанный список или тому подобное, для которого объекты хранятся на самом деле), либо повторно проверить, чтобы выбрать другой слот (существуют различные стратегии для этого).
Теперь, с любым подходом, мы отходим от сложности O (1), которую мы хотим, с хеш-таблицей, и к сложности O (n). Риск этого обратно пропорционален количеству доступных слотов, поэтому после определенного размера мы изменяем размер хеш-таблицы (даже если бы все было идеально, нам в конечном итоге пришлось бы сделать это, если бы количество хранимых элементов было больше, чем число слоты).
Повторная вставка элементов для изменения размера будет зависеть от хэш-кодов. Из-за этого, хотя редко имеет смысл запоминать GetHashCode()
в объекте (он просто не вызывается достаточно часто для большинства объектов), безусловно, имеет смысл запоминать его в самой хеш-таблице (или, возможно, запоминать производимый результат, например, если вы повторно хэшировали с помощью хэша Ванга / Дженкинса, чтобы уменьшить ущерб, вызванный неправильными реализациями GetHashCode()
).
Теперь, когда мы придем к вставке, наша логика будет выглядеть примерно так:
- Получить хеш-код для объекта.
- Получить слот для объекта.
- Если слот пуст, поместите в него объект и верните.
- Если слот содержит равный объект, мы закончили для хэш-набора и можем заменить значение для хеш-таблицы. Сделайте это и вернитесь.
- Попробуйте следующий слот в соответствии со стратегией столкновения и вернитесь к пункту 3 (возможно, измените размер, если мы сделаем это слишком часто).
Итак, в этом случае мы должны получить хеш-код, прежде чем сравнивать на равенство. У нас также есть хеш-код для существующих объектов, уже предварительно вычисленных для учета изменения размера. Сочетание этих двух фактов означает, что имеет смысл реализовать наше сравнение для элемента 4 следующим образом:
private bool IsMatch(KeyType newItem, KeyType storedItem, int newHash, int oldHash)
{
return ReferenceEquals(newItem, storedItem) // fast, false negatives, no false positives (only applicable to reference types)
||
(
newHash == oldHash // fast, false positives, no fast negatives
&&
_cmp.Equals(newItem, storedItem) // slow for some types, but always correct result.
);
}
Очевидно, что преимущество этого зависит от сложности _cmp.Equals
. Если бы наш тип ключа был int
, то это было бы полной тратой. Если наш тип ключа где строка, и мы использовали нечувствительные к регистру Unicode-нормализованные сравнения на равенство (поэтому он не может даже сокращать длину), тогда экономия вполне может стоить.
Как правило, запоминание хеш-кодов не имеет смысла, потому что они не используются достаточно часто, чтобы быть выигрышем в производительности, но их сохранение в хэш-наборе или самой хеш-таблице может иметь смысл.