Как правильно переопределить isEqual:
в Objective-C? «Подвох», по-видимому, заключается в том, что если два объекта равны (как определено isEqual:
методом), они должны иметь одинаковое значение хеш-функции.
В разделе « Самоанализ » Руководства по основам какао есть пример того isEqual:
, как переписать, скопированный следующим образом, класс с именем MyWidget
:
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return [self isEqualToWidget:other];
}
- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
if (self == aWidget)
return YES;
if (![(id)[self name] isEqual:[aWidget name]])
return NO;
if (![[self data] isEqualToData:[aWidget data]])
return NO;
return YES;
}
Он проверяет указатель равенства, то класс равенства, и , наконец , сравнивает объекты , используя isEqualToWidget:
, которые только проверяет name
и data
свойства. В примере не показано, как переопределить hash
.
Допустим, есть и другие свойства, которые, например, не влияют на равенство age
. Не следует ли hash
метод быть переопределен таким образом, что только name
и data
влияют на хэш? И если так, как бы вы это сделали? Просто добавьте хэши name
и data
? Например:
- (NSUInteger)hash {
NSUInteger hash = 0;
hash += [[self name] hash];
hash += [[self data] hash];
return hash;
}
Этого достаточно? Есть ли лучшая техника? Что делать, если у вас есть примитивы, как int
? Преобразовать их, чтобы NSNumber
получить их хэш? Или как структура NSRect
?
( Мозг пердеть : Первоначально написал "побитовое ИЛИ" их вместе с |=
. Значит добавить.)
источник
if (![other isKindOfClass:[self class]])
- Технически это означает, что равенство не будет коммутативным. Т.е. A = B не означает B = A (например, если один является подклассом другого)Ответы:
Начать с
Тогда для каждого примитива вы делаете
Для объектов вы используете 0 для nil и в противном случае их хэш-код.
Для логических значений вы используете два разных значения
Объяснение и атрибуция
Это не работа tcurdt, и в комментариях требовалось больше объяснений, поэтому я считаю, что редактирование для атрибуции справедливо.
Этот алгоритм был популяризирован в книге «Эффективная Java», и соответствующую главу в настоящее время можно найти здесь . Эта книга популяризировала алгоритм, который теперь используется по умолчанию во многих Java-приложениях (включая Eclipse). Однако он произошел от еще более старой реализации, которая по-разному приписывается Дэну Бернштейну или Крису Тореку. Этот старый алгоритм изначально использовался в Usenet, и определенная атрибуция затруднена. Например, в этом коде Apache есть несколько интересных комментариев (поиск по их именам), которые ссылаются на первоисточник.
Суть в том, что это очень старый, простой алгоритм хеширования. Это не самый производительный и даже математически не доказанный, чтобы быть "хорошим" алгоритмом. Но это просто, и многие люди использовали его в течение долгого времени с хорошими результатами, поэтому он имеет большую историческую поддержку.
источник
Я просто забираю Objective-C сам, поэтому я не могу говорить конкретно за этот язык, но на других языках, которые я использую, если два экземпляра равны, они должны возвращать один и тот же хэш - иначе вы получите все виды проблем при попытке использовать их в качестве ключей в хеш-таблице (или любых коллекциях словарного типа).
С другой стороны, если 2 экземпляра не равны, они могут иметь или не иметь один и тот же хэш - лучше, если они этого не делают. В этом разница между поиском O (1) по хеш-таблице и поиском O (N) - если все ваши хеши сталкиваются, вы можете обнаружить, что поиск по вашей таблице не лучше, чем поиск по списку.
С точки зрения лучших практик ваш хеш должен возвращать случайное распределение значений для своего ввода. Это означает, что, например, если у вас есть double, но большинство ваших значений имеют тенденцию кластеризоваться между 0 и 100, вам нужно убедиться, что хэши, возвращаемые этими значениями, равномерно распределены по всему диапазону возможных значений хеш-функции. , Это значительно улучшит вашу производительность.
Существует множество алгоритмов хеширования, в том числе несколько перечисленных здесь. Я стараюсь избегать создания новых алгоритмов хеширования, поскольку это может иметь большое влияние на производительность, поэтому использование существующих методов хеширования и выполнение побитовой комбинации некоторого вида, как вы делаете в своем примере, является хорошим способом избежать этого.
источник
Например:
Решение, найденное на http://nshipster.com/equality/ Мэттом Томпсоном (который также упомянул этот вопрос в своем посте!)
источник
Я нашел эту ветку чрезвычайно полезной, предоставляя все, что мне нужно, чтобы мои
isEqual:
иhash
методы были реализованы с помощью одного улова. При тестировании переменных экземпляра объекта вisEqual:
примере кода используется:Это многократно проваливалось ( то есть возвращалось НЕТ ) без ошибок, когда я знал, что объекты были идентичны в моем модульном тестировании. Причина была в том, что одна из
NSString
переменных экземпляра была равна нулю, поэтому приведенное выше утверждение было:и так как ноль будет отвечать на любой метод, это совершенно законно, но
возвращает ноль , то есть НЕТ , поэтому, когда и у объекта, и у тестируемого был нулевой объект, они считались бы не равными ( т.е. ,
isEqual:
будет возвращать NO ).Это простое исправление состояло в том, чтобы изменить оператор if на:
Таким образом, если их адреса совпадают, метод пропускает вызов независимо от того, оба они равны нулю. или оба указывают на один и тот же объект, но если либо не nil, либо они указывают на разные объекты, то соответствующим образом вызывается компаратор.
Надеюсь, это сэкономит кому-то несколько минут от царапин на голове.
источник
Хеш-функция должна создавать полууникальное значение, которое вряд ли столкнется или совпадет с хеш-значением другого объекта.
Вот полная хеш-функция, которую можно адаптировать к переменным экземпляра ваших классов. Он использует NSUInteger вместо int для совместимости на 64/32-битных приложениях.
Если результат становится 0 для разных объектов, вы рискуете столкнуться с хэшами. Столкновение хэшей может привести к неожиданному поведению программы при работе с некоторыми классами коллекций, которые зависят от хэш-функции. Обязательно проверьте свою хеш-функцию перед использованием.
источник
result = prime * result + [self isSelected] ? yesPrime : noPrime;
. Затем я обнаружил, что это настройкаresult
(например)1231
, я полагаю, из-за того, что?
оператор имеет приоритет. Я исправил проблему, добавив скобки:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Простой, но неэффективный способ - возвращать одно и то же
-hash
значение для каждого экземпляра. В противном случае, да, вы должны реализовать хеш, основанный только на объектах, которые влияют на равенство. Это сложно, если вы используете слабые сравнения в-isEqual:
(например, сравнение строк без учета регистра). Для целых можно вообще использовать сам int, если вы не будете сравнивать с NSNumbers.Не используйте | =, хотя, это насытит. Вместо этого используйте ^ =.
Случайные забавный факт:
[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]]
, но[[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]
. (rdar: // 4538282, открыт с 05 мая 2006 г.)источник
Помните, что вам нужно предоставить хеш, равный равному
isEqual
true. КогдаisEqual
ложно, хеш не должен быть неравным, хотя, вероятно, так оно и есть. Следовательно:Хэш просто Выберите переменную члена (или нескольких членов), которая является наиболее характерной.
Например, для CLPlacemark достаточно только имени. Да, есть 2 или 3 отличия CLPlacemark с одинаковыми именами, но они встречаются редко. Используйте этот хэш.
...
Заметьте, я не затрудняюсь указывать город, страну и т. Д. Название достаточно. Возможно название и название.
Хеш должен быть равномерно распределен. Таким образом, вы можете объединить несколько переменных членов, используя символ ^ (знак xor)
Так что это что-то вроде
Таким образом, хэш будет равномерно распределен.
Так что же делать в массиве?
Опять все просто. Вам не нужно хэшировать все элементы массива. Достаточно хешировать первый элемент, последний элемент, количество, может быть, некоторые средние элементы, и все.
источник
Постойте, конечно, гораздо более простой способ сделать это - сначала переопределить
- (NSString )description
и предоставить строковое представление состояния вашего объекта (вы должны представить все состояние вашего объекта в этой строке).Затем просто предоставьте следующую реализацию
hash
:Это основано на принципе, что «если два строковых объекта равны (как определено isEqualToString: метод), они должны иметь одинаковое значение хеш-функции».
Источник: Ссылка класса NSString
источник
description
, я не понимаю, почему это хуже любое из решений с более высоким рейтингом. Возможно, это не самое математически элегантное решение, но оно должно сработать. Как заявляет Брайан Б. (наиболее одобренный ответ на данный момент): «Я стараюсь избегать создания новых алгоритмов хеширования» - согласился! - Я просто !hash
NSString
description
указатель адреса. Таким образом, это создает два разных экземпляра одного и того же класса, которые равны разному хешу, что нарушает базовое предположение, что два равных объекта имеют одинаковый хеш!Контракты equals и hash хорошо определены и тщательно исследованы в мире Java (см. Ответ @ mipardi), но все те же соображения должны применяться к Objective-C.
Eclipse выполняет надежную работу по генерации этих методов в Java, поэтому вот пример Eclipse, перенесенный вручную в Objective-C:
И для подкласса,
YourWidget
который добавляет свойствоserialNo
:Эта реализация позволяет избежать некоторых подводных камней в примере
isEqual:
Apple:other isKindOfClass:[self class]
является асимметричным для двух разных подклассовMyWidget
. Равенство должно быть симметричным: a = b тогда и только тогда, когда b = a. Это можно легко исправить, изменив тест наother isKindOfClass:[MyWidget class]
, тогда всеMyWidget
подклассы будут взаимно сопоставимы.isKindOfClass:
теста подкласса предотвращает переопределение подклассовisEqual:
с помощью усовершенствованного теста на равенство. Это потому, что равенство должно быть транзитивным: если a = b и a = c, то b = c. ЕслиMyWidget
экземпляр сравнивается равным двумYourWidget
экземплярам, тогда этиYourWidget
экземпляры должны сравниваться равными друг другу, даже если ониserialNo
различаются.Вторая проблема может быть решена, если рассматривать объекты как равные, только если они принадлежат к одному и тому же классу, поэтому
[self class] != [object class]
здесь и тест. Для типичных классов приложений это, кажется, лучший подход.Однако, безусловно, есть случаи, когда
isKindOfClass:
тест предпочтительнее. Это более типично для каркасных классов, чем для классов приложений. Например, любойNSString
должен сравниваться равным любому другомуNSString
с той же базовой последовательностью символов, независимо от различияNSString
/NSMutableString
, а также независимо от того, какие частные классы вNSString
кластере классов задействованы.В таких случаях
isEqual:
должно иметь четко определенное, хорошо документированное поведение, и должно быть ясно, что подклассы не могут переопределить это. В Java ограничение «без переопределения» можно применить, помечая методы equals и hashcode какfinal
, но Objective-C не имеет эквивалента.источник
MyWidget
понимается, не кластер классов.Это не дает прямого ответа на ваш вопрос (вообще), но я использовал MurmurHash прежде, чтобы генерировать хэши: murmurhash
Думаю, я должен объяснить, почему: ропот быстро
источник
Я обнаружил, что эта страница является полезным руководством по переопределению методов типа equals и hash. Он включает в себя достойный алгоритм для вычисления хэш-кодов. Страница ориентирована на Java, но ее довольно легко адаптировать к Objective-C / Cocoa.
источник
Я тоже новичок в Objective C, но я нашел отличную статью об идентичности и равенстве в Objective C здесь . Из моего прочтения видно, что вы можете сохранить хеш-функцию по умолчанию (которая должна обеспечивать уникальную идентичность) и реализовать метод isEqual, чтобы он сравнивал значения данных.
источник
Equality vs Identity
Карла Крафта действительно хороша.isEqual:
, вы также должны переопределитьhash
.Квинн просто ошибается, что ссылка на хэш ропота здесь бесполезна. Куинн прав, что вы хотите понять теорию хеширования. Ропот превращает большую часть этой теории в реализацию. Выяснение того, как применить эту реализацию к этому конкретному приложению, заслуживает изучения.
Некоторые из ключевых моментов здесь:
Пример функции из tcurdt предполагает, что «31» является хорошим множителем, потому что он является простым. Нужно показать, что простота является необходимым и достаточным условием. На самом деле 31 (и 7), вероятно, не особенно хорошие простые числа, потому что 31 == -1% 32. Нечетный множитель с примерно половиной установленных битов и половиной очищенных битов, вероятно, будет лучше. (Константа умножения хэша рота имеет это свойство.)
Этот тип хэш-функции, вероятно, был бы сильнее, если бы после умножения значение результата было скорректировано с помощью сдвига и xor. Умножение имеет тенденцию давать результаты большого количества битовых взаимодействий в верхнем конце регистра и низкие результаты взаимодействия в нижнем конце регистра. Shift и xor увеличивают взаимодействия в нижней части регистра.
Установка начального результата в значение, при котором около половины битов равно нулю, а около половины битов равно единице, также может оказаться полезным.
Может быть полезно быть осторожным относительно порядка, в котором элементы объединяются. Вероятно, следует сначала обработать логические значения и другие элементы, где значения не сильно распределены.
Может быть полезно добавить пару дополнительных этапов скремблирования битов в конце вычисления.
Является ли хэш рота на самом деле быстрым для этого приложения - вопрос открытый. Шум бормотушек предварительно смешивает биты каждого входного слова. Несколько входных слов могут обрабатываться параллельно, что помогает многопоточному конвейерному процессору.
источник
Комбинируя ответ @ tcurdt с ответом @ oscar-gomez для получения имен свойств , мы можем создать простое решение для вставки для isEqual и hash:
Теперь в вашем пользовательском классе вы можете легко реализовать
isEqual:
иhash
:источник
Обратите внимание, что если вы создаете объект, который может быть изменен после создания, значение хеш-функции не должно изменяться, если объект вставлен в коллекцию. На практике это означает, что значение хеш-функции должно быть зафиксировано с момента создания исходного объекта. См . Документацию Apple по методу -hash протокола NSObject для получения дополнительной информации:
Для меня это звучит как полный удар, так как он потенциально делает рендеринг хеша гораздо менее эффективным, но я полагаю, что лучше ошибиться с осторожностью и следовать указаниям документации.
источник
Извините, если я рискну озвучить полный гроб здесь, но ... ... никто не потрудился упомянуть, что для того, чтобы следовать «лучшим практикам», вам определенно не следует указывать метод equals, который НЕ учитывал бы все данные, принадлежащие вашему целевому объекту, например, что угодно данные агрегируются в ваш объект, и его сопоставление следует учитывать при реализации equals. Если вы не хотите принимать во внимание, скажем, «возраст», то вам следует написать компаратор и использовать его для сравнения, а не isEqual :.
Если вы определяете isEqual: метод, который произвольно выполняет сравнение на равенство, вы рискуете использовать этот метод другим разработчиком или даже самим собой, если вы забыли «поворот» в своей интерпретации равенства.
Ergo, хотя это отличные вопросы о хешировании, вам обычно не нужно переопределять метод хеширования, вам, вероятно, следует вместо этого определить специальный компаратор.
источник