Почему ValueType.GetHashCode() реализован так, как есть?
От ValueType.cs
**Action: Our algorithm for returning the hashcode is a little bit complex. We look
** for the first non-static field and get it hashcode. If the type has no
** non-static fields, we return the hashcode of the type. We can't take the
** hashcode of a static member because if that member is of the same type as
** the original type, we'll end up in an infinite loop.
Я был укушен этим сегодня, когда я использовал KeyValuePair в качестве ключа в словаре (он хранил имя атрибута xml (enum) и его значение (строка)) и ожидал, что он будет иметь hashcode, вычисленный на основе всех его поля, но в соответствии с реализацией он рассматривал только ключевую часть.
Пример (c/p из Linqpad):
void Main()
{
var kvp1 = new KeyValuePair<string, string>("foo", "bar");
var kvp2 = new KeyValuePair<string, string>("foo", "baz");
// true
(kvp1.GetHashCode() == kvp2.GetHashCode()).Dump();
}
Первое нестатическое поле, которое, я думаю, означает первое поле в порядке декларатора, что также может вызвать проблемы при изменении переменной порядка в источнике по любой причине и полагая, что семантически не изменяет код.
Ответы
Ответ 1
ОБНОВЛЕНИЕ: Этот ответ был (частично) основой статьи в блоге, которую я написал, в которой более подробно рассматриваются характеристики дизайна GetHashcode
. Спасибо за интересный вопрос!
Я не реализовал это, и я не говорил с людьми, которые сделали. Но я могу указать на несколько вещей.
(Прежде чем продолжить, обратите внимание, что здесь я конкретно говорю о хеш-кодах для целей балансировки хеш-таблиц, где содержимое таблицы выбирается не враждебными пользователями. Проблемы хеш-кодов для цифровой подписи, проверки избыточности или Обеспечение хорошей производительности хеш-таблицы, когда некоторые пользователи проводят атаки типа "отказ в обслуживании" на провайдера таблиц, выходит за рамки этого обсуждения.)
Во-первых, как правильно отмечает Джон, данный алгоритм реализует требуемый контракт GetHashCode. Это может быть неоптимальным для ваших целей, но это законно. Все, что требуется, - это чтобы вещи, сравниваемые равными, имели одинаковые хеш-коды.
Так что же "приятно иметь" в дополнение к этому контракту? Хорошая реализация хеш-кода должна быть:
1) Быстро. Очень быстро! Помните, весь смысл хеш-кода в первую очередь заключается в том, чтобы быстро найти относительно пустой слот в хеш-таблице. Если вычисление O (1) хеш-кода на практике медленнее, чем время O (n), затрачиваемое на наивный поиск, то решение с использованием хеш-кода является чистым убытком.
2) Хорошо распределено по пространству 32-битных целых для заданного распределения входов. Чем хуже распределение по целым, тем больше будет наивного линейного поиска хеш-таблицы.
Итак, как бы вы создали алгоритм хеширования для произвольных типов значений с учетом этих двух противоречивых целей? Каждый раз, когда вы тратите на сложный алгоритм хеширования, который гарантирует хорошее распределение, время тратится плохо.
Распространенным предложением является "хэширование всех полей и затем XOR вместе полученных хеш-кодов". Но это напрашивается на вопрос; XORing двух 32-битных целых дает хорошее распределение, только если сами входы очень хорошо распределены и не связаны друг с другом, и это маловероятный сценарий:
// (Updated example based on good comment!)
struct Control
{
string name;
int x;
int y;
}
Какова вероятность того, что x и y хорошо распределены по всему диапазону 32-битных целых чисел? Очень низкий. Шансы намного лучше, потому что они малы и близки друг к другу, и в этом случае кеширование их хэш-кодов вместе делает вещи хуже, а не лучше. xoring вместе целые числа, которые близки друг к другу, обнуляют большинство битов.
Кроме того, это O (n) в количестве полей! Тип значения с большим количеством маленьких полей может занять сравнительно много времени для вычисления хеш-кода.
По сути, мы имеем дело с тем, что пользователь сам не предоставил реализацию хеш-кода; либо им все равно, либо они не ожидают, что этот тип когда-либо будет использоваться в качестве ключа в хэш-таблице. Учитывая, что у вас нет семантической информации о типе, что лучше всего делать? Лучшее, что можно сделать, это то, что быстро и дает хорошие результаты в большинстве случаев.
Большую часть времени два структурных экземпляра, которые отличаются, будут отличаться в большинстве своих полей, а не только в одном из них, поэтому просто выбрать одно из них и надеяться, что оно отличается, и это кажется разумным.
В большинстве случаев два экземпляра структуры, которые отличаются, будут иметь некоторую избыточность в своих полях, поэтому объединение хеш-значений многих полей может уменьшить, а не увеличить энтропию в хеш-значении, даже если оно потребляет время, которое алгоритм хеширования предназначен для сохранения.
Сравните это с дизайном анонимных типов в С#. С анонимными типами мы знаем, что весьма вероятно, что тип используется в качестве ключа к таблице. Мы знаем, что весьма вероятно, что будет иметь место избыточность между экземплярами анонимных типов (потому что они являются результатом декартового произведения или другого объединения). И поэтому мы объединяем хеш-коды всех полей в один хеш-код. Если это приводит к плохой производительности из-за избыточного числа вычисляемых хеш-кодов, вы можете использовать собственный номинальный тип, а не анонимный.
Ответ 2
Фактическая реализация ValueType.GetHashCode() не совсем соответствует комментарию. Он имеет две версии алгоритма, быстрые и медленные. Сначала он проверяет, содержит ли структура какие-либо элементы ссылочного типа, и есть ли какие-либо дополнения между полями. Заполнение пустого пространства в структурном значении, создаваемом, когда компилятор JIT выравнивает поля. Там заполнение в структуре, которая содержит bool и int (3 байта), но не имеет отступов, когда она содержит int и int, они плотно прилегают друг к другу.
Без ссылки и без заполнения, он может выполнять быструю версию, так как каждый бит в структурном значении является битом, который принадлежит значению поля. Он просто xors по 4 байта за раз. Вы получите "хороший" хэш-код, который учитывает всех членов. Таким образом, многие простые типы структуры в платформе .NET ведут себя так же, как Point и Size.
В противном случае это медленная версия, моральный эквивалент отражения. Что вы получаете, ваш KeyValuePair < > содержит ссылки. И этот только проверяет первое поле кандидата, как говорится в комментарии. Это, безусловно, первоочередная оптимизация, избегая горения слишком много времени.
Да, неприятная деталь и не такая широко известная. Обычно это обнаруживается, когда кто-то замечает, что их код коллекции всасывает грязь.
Еще одна мучительная деталь: у быстрой версии есть ошибка, которая байт, когда структура содержит поле типа decimal. Значения 12m и 12.0m логически равны, но у них нет одинакового битового шаблона. GetHashCode() скажет, что они не равны. Уч.
Ответ 3
Он должен по-прежнему подчиняться контракту GetHashCode
, даже если изменяется порядок поля: равные значения будут иметь одинаковые хэш-коды в течение времени жизни этого процесса.
В частности:
- Не равные значения не обязательно должны иметь неравные хэш-коды
- Коды хэша не обязательно должны быть согласованными между процессами (вы можете изменить реализацию, перестроить, и все должно работать - вы не должны сохранять хэш-коды в основном)
Теперь я не говорю, что реализация ValueType - отличная идея - это приведет к зависанию производительности различными способами... но я не думаю, что это действительно сломалось.
Ответ 4
Ну, есть плюсы и минусы для любой реализации GetHashCode()
. Это, конечно, то, что мы взвешиваем при реализации наших собственных, но в случае ValueType.GetHashCode()
существует особая трудность в том, что у них нет большой информации о том, каковы будут фактические данные конкретного типа. Конечно, это часто случается с нами, когда мы создаем абстрактный класс или планируем быть базой классов, которые добавят намного больше с точки зрения состояния, но в этих случаях у нас есть очевидное решение, просто использующее реализацию по умолчанию object.GetHashCode()
, если производный класс не хочет его переопределять.
С ValueType.GetHashCode()
у них нет такой роскоши, поскольку основное различие между типом значения и ссылочным типом, несмотря на популярность разговоров о деталях реализации стека против кучи, тот факт, что для эквивалентности типа значения относится к значению, тогда как для эквивалентности типа объекта относится к идентичности (даже если объект определяет другую форму эквивалентности, переопределяя Equals()
и GetHashCode()
, понятие ссылочного равенства все еще существует и по-прежнему полезно.
Итак, для метода Equals()
реализация очевидна; проверьте, что два объекта одного типа, и если он затем проверяет также, что все поля равны (на самом деле есть оптимизация, которая в некоторых случаях выполняет побитовое сравнение, но оптимизацию по одной и той же базовой идее).
Что делать для GetHashCode()
? Просто нет идеального решения. Одна вещь, которую они могут сделать, - это что-то вроде mult-then-add или shift-then-xor для каждого поля. Вероятно, это даст довольно хороший хэш-код, но может быть дорогостоящим, если бы было много полей (неважно, что не рекомендуется иметь типы значений, у которых много полей, разработчик должен учитывать, что они все еще могут и действительно могут быть даже времена, когда это имеет смысл, хотя я честно не могу представить себе время, когда это имеет смысл, и имеет смысл также хешировать его). Если бы они знали, что некоторые поля редко отличались между экземплярами, они могли игнорировать эти поля и все еще иметь довольно хороший хэш-код, а также довольно быстро. Наконец, они могут игнорировать большинство полей и надеются, что те, которые они не игнорируют, часто меняются по значению. Они пошли на самую экстремальную версию последнего.
(Вопрос о том, что делается, когда нет полей экземпляра, является другим вопросом и довольно хорошим выбором, такие типы значений равны всем другим экземплярам того же типа, и они имеют хэш-код, который соответствует этому).
Итак, это реализация, которая засасывает, если вы хешируете множество значений, где первое поле является одинаковым (или иным образом возвращает один и тот же хэш-код), но другие реализации будут сосать в других случаях (Mono отправляется на xoring все поля " хэш-коды вместе, лучше в вашем случае, хуже в других).
Вопрос об изменении порядка поля не имеет значения, поскольку hashcode довольно четко заявлен как остающийся действительным для времени жизни процесса и не подходящий для большинства случаев, где они могут быть сохранены за пределами этого (могут быть полезны в некоторых кеширование ситуаций, когда это не повредит, если что-то не удается найти после изменения кода).
Итак, не здорово, но ничего не было бы идеально. Это показывает, что всегда нужно учитывать обе стороны того, что означает "равенство" при использовании объекта в качестве ключа. Он легко устанавливается в вашем случае с помощью:
public class KVPCmp<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>, IEqualityComparer
{
bool IEqualityComparer.Equals(object x, object y)
{
if(x == null)
return y == null;
if(y == null)
return false;
if(!(x is KeyValuePair<TKey, TValue>) || !(y is KeyValuePair<TKey, TValue>))
throw new ArgumentException("Comparison of KeyValuePairs only.");
return Equals((KeyValuePair<TKey, TValue>) x, (KeyValuePair<TKey, TValue>) y);
}
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
{
return x.Key.Equals(y.Key) && x.Value.Equals(y.Value);
}
public int GetHashCode(KeyValuePair<TKey, TValue> obj)
{
int keyHash = obj.GetHashCode();
return ((keyHash << 16) | (keyHash >> 16)) ^ obj.Value.GetHashCode();
}
public int GetHashCode(object obj)
{
if(obj == null)
return 0;
if(!(obj is KeyValuePair<TKey, TValue>))
throw new ArgumentException();
return GetHashCode((KeyValuePair<TKey, TValue>)obj);
}
}
Используйте это как свой компаратор при создании словаря, и все должно быть хорошо (вам действительно нужны только общие методы компаратора, но остальное остальное не вредит и может быть полезно иногда иметь).
Ответ 5
Спасибо всем, очень, очень информативные ответы. Я знал, что в этом решении должно быть какое-то обоснование, но я бы хотел, чтобы это было документировано лучше. Я не могу использовать v4 структуры, поэтому нет Tuple<>
, и это была основная причина, по которой я решил контрейлеризовать структуру KeyValuePair
. Но я думаю, что нет режущих углов, и мне придется сворачивать самостоятельно. Еще раз спасибо всем.