Как стать автором
Обновить

Комментарии 17

Само по себе такое поведение, конечно, хэш-таблицу сломать неспособно, поэтому для того, чтобы красиво выстрелить в ногу, внутри класса Document надо написать так

Вроде, Microsoft явно говорит, что нельзя менять хеш код после использования:


Otherwise, you might think that the mutable object is lost in the hash table. If you do choose to override GetHashCode() for a mutable reference type, your documentation should make it clear that users of your type should not modify object values while the object is stored in a hash table.



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

Это ужас, из-за которого UI может тормозить на ровном месте на большом числе компьютеров. Создал issue — https://github.com/dotnet/wpf/issues/3499, проследим за реакцией.

Это ужас, из-за которого UI может тормозить на ровном месте

А почему? Кто-то держит размеры всех элементов в хеш-таблицах, или словарях и оных размеров могут быть сотни тысяч?
едит: убрал глупость
Ну и в предложенном вами варианте сдвигать 32-х битовое значение на 397 битов как-то не очень, всегдаж будет 0 :)

Мой косяк, вот так подобные баги и рождаются. Спасибо, исправил на умножение.


А почему? Кто-то держит размеры всех элементов в хеш-таблицах, или словарях и оных размеров могут быть сотни тысяч?

Не удивлюсь, что кто-то и держит довольно много этих структур в каких-нибудь хеш-таблицах. Или классов, где этот Size — еще одно поле, которое участвует в GetHashCode.


Это библиотечный код, который работает на множестве устройств. Так что если какое-нибудь корпоративное приложение на 100.000 пользователей зачем-то использует схему выше, то на ровном месте уходит производительность, а это плохо.
Ну и плюс там явно генерированный код, так что, как я думаю, хорошо бы этот генератор просто исправить, что улучшит ситуацию и с другими классами.

Мой косяк, вот так подобные баги и рождаются. Спасибо, исправил на умножение.

Я написал, а потом проверил, оказалось, что когда сдвигаешь на большее количество бит, чем занимает само число, то все кратные сдвиги отбрасываются.
То есть, например, для Int32
x << 1;
y >> 1;
и
x << 1 + 32;
y >> 1 + 32;
будет одинаковый результат. а раньше не знал :)
Microsoft явно говорит

Но кто же читает документацию? :)
Тем более, что при всей своей капитанской очевидности, занимательное поведение наблюдается только при совпадении ряда факторов.

github.com/dotnet/wpf/issues/3499

Там не только в WPF стреляет, System.Drawings много где используется.
Rect и Point не лучше: везде значения просто xor'ятся.

edit: там всё плохо: все примитивны содержат эту ошибку в хеш-функции

А почему хранить хэшкод плохо?
Буквально на этой неделе добавил кэширование хэшкода для иммутабельного класса, который используется в качестве ключа в местах с интенсивным обращением.


Да, чудо не произошло. Но на капельку снизить нагрузку на CPU удалось.

Вопрос в том, как вы этот код считаете:

1. Если вы где-то на пути вычисления используете стандартный GetHashCode(), то MS не гарантирует, что даже между запусками приложения этот код будет одинаков.
Самый банальный пример — Object.GetHashCode(), который тупо зависит от ссылки объекта.
Или если, например, починят генерацию кода для структуры Size (по реквесту выше) — то у вас тоже сохраненное с вычисленным не совпадет.

2. Вам надо везде учитывать изменения вашего объекта, чтобы консистентно сериализовывать хэш-код.

3. А если вы, положим, добавите в класс новое свойство, то придется в дополнение к стандартной миграции базы писать обработчик, которые все коды в базе пересчитает.

4. Если у вас все обложено тестами, то, наверное, можно спать спокойно… Но если покрытие не 100%, то изредка (и, скорее всего, только на продакшене) вы будете периодически удивляться чудесам.

Ну а стоит ли вот это вот все капельки снижения нагрузки на CPU — я не знаю.

Ситуация немного другая. Я на scala пишу.
Речь об хэшкоде иммутабельного case класса. Сериализовать в базу его не требуется.


Реализация конкретная — MurmurHash3 по всем параметрам конструктора (для знатоков — хэш от Product).


Ну т.е. это объекты живущие исключительно в рантайме и никогда не изменяющиеся.

Помнится, Седжвик писал, что в java в некоторых местах хеш таки хранится внутри класса, если считать его долго, а используется часто, кажется речь шла о string.

А почему хранить хэшкод плохо?

В теории, он может поменяться между запусками, а значит на него можно расчитывать только внутри запущенного процесса (а точнее даже — внутри домена). Ну и плюс он может быть разным между разными версиями .Net'а и разными архитектурами процессора (например, String.GetHashCode различался раньше для x64 и x86).


Пруф по ссылке:


 Warning

A hash code is intended for efficient insertion and lookup in collections that are based on a hash table. A hash code is not a permanent value. For this reason:

Do not serialize hash code values or store them in databases.
Do not use the hash code as the key to retrieve an object from a keyed collection.
Do not send hash codes across application domains or processes. In some cases, hash codes may be computed on a per-process or per-application domain basis.
Do not use the hash code instead of a value returned by a cryptographic hashing function if you need a cryptographically strong hash. For cryptographic hashes, use a class derived from the System.Security.Cryptography.HashAlgorithm or System.Security.Cryptography.KeyedHashAlgorithm class.
Do not test for equality of hash codes to determine whether two objects are equal. (Unequal objects can have identical hash codes.) To test for equality, call the ReferenceEquals or Equals method.

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

Ежели так, то, конечно, ничего плохого в этом не вижу; особенно, если объект иммутабельный.

Мне в последнее время всё чаще приходит в голову мысль, что сам интерфейс GetHashCode должен был быть GetHashCode(IHasher). И уже в инстанс этого IHasher скармливать значащие данные типа. А он уж сам решает, как из них собрать хеш. Избавляет от множества головняков, в т.ч. описанного для Size и SizeF. Плюс возможность менять по необходимости хешер в хеш-контейнере, не правя тип ключа.

Кажется, вы только что придумали интерфейс IEquatable(T) — это как раз и есть внешний хешер и сравнитель)
Хотя идея ваша понятна — не объект передавать во внешний хешер (IEquatable), а хешер в объект — как стратегию, инвертировать эту ситуацию.


Отчасти при реализации GetHashCode может помочь https://docs.microsoft.com/en-gb/dotnet/api/system.hashcode?view=dotnet-plat-ext-3.1
Да, это не внешний хешер, но структура, принимающая значимые данные типа, и вычисляющая хеш.

Мне показалось, что так не стали делать by default сугубо по эстетическим причинам. Видимо, кому-то из дизайнеров языка очень хотелось, чтобы можно было писать как-то так:
if (a == b)…

Т.е. сам объект должен уметь в Equals без каких-то сторонних параметров. Ну а раз есть Equals, то, логично, нужен такой же GetHashCode. А то как же так — объекты равны, а хэш-коды у них разные? В результате и получили то, что получили.

И теперь вот затыкаем с разных сторон такую красоту: начиная от перегруженного Equals для строк, и заканчивая IEqualityComparer(T).
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории