Лучшие практики для переопределения isEqual: и hash

Как правильно переопределить 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?

(Мозговое пердеж: изначально было написано «поразрядное ИЛИ» вместе с |=. Имеется в виду добавление.)


person Dave Dribin    schedule 31.10.2008    source источник
comment
if (![other isKindOfClass:[self class]]) - Технически это означает, что равенство не будет коммутативным. Т.е. A = B не означает B = A (например, если один является подклассом другого)   -  person Robert    schedule 20.11.2013
comment
Ссылка на документацию мертва, теперь она заархивирована в Introspection < / а>   -  person jedwidz    schedule 02.01.2019


Ответы (16)


Начать с

 NSUInteger prime = 31;
 NSUInteger result = 1;

Затем для каждого примитива, который вы делаете

 result = prime * result + var

Для объектов вы используете 0 вместо nil, а в противном случае - их хэш-код.

 result = prime * result + [var hash];

Для логических значений вы используете два разных значения

 result = prime * result + ((var)?1231:1237);

Объяснение и авторство

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

Этот алгоритм был популяризирован в книге "Эффективная Java", и в настоящее время может можно найти в Интернете здесь. Эта книга популяризировала алгоритм, который теперь используется по умолчанию в ряде приложений Java (включая Eclipse). Однако он произошел от еще более старой реализации, которую по-разному приписывают Дэну Бернштейну или Крису Тореку. Этот старый алгоритм изначально распространялся в Usenet, и его определенная атрибуция затруднительна. Например, в этом коде Apache есть несколько интересных комментариев. (поиск по их именам), который ссылается на первоисточник.

Суть в том, что это очень старый простой алгоритм хеширования. Это не самый производительный алгоритм, и его «хороший» алгоритм даже не доказан математически. Но это просто, и многие люди использовали его в течение долгого времени с хорошими результатами, поэтому он имеет много исторических подтверждений.

person tcurdt    schedule 31.10.2008
comment
Мне это очень нравится. FWIW, результатом является NSUInteger, который имеет ширину 64 бита на x86-64, поэтому вы можете не захотеть сдвигать 64-битные ivars. - person Dave Dribin; 31.10.2008
comment
Откуда взялось 1231: 1237? Я тоже вижу это в Java Boolean.hashCode (). Это волшебно? - person David Leonard; 09.03.2009
comment
Спасибо за алгоритм хеширования, очень пригодился! - person Leibowitzn; 01.10.2010
comment
Не используйте этот метод, он может привести к переполнению вашего хеш-значения. Результирующие хэши могут отображаться на одно и то же значение после переполнения, что вызовет много проблем, когда вам понадобится хеш-функция для классов коллекции. - person Paul Solt; 12.12.2010
comment
Природа алгоритмов хеширования заключается в том, что могут возникать конфликты. Так что я не понимаю твоего мнения, Пол. - person tcurdt; 05.08.2011
comment
На мой взгляд, этот ответ не отвечает на фактический вопрос (лучшие практики для переопределения хэша NSObject). Он просто предоставляет один конкретный алгоритм хеширования. Вдобавок ко всему, скудность объяснения затрудняет понимание без глубоких знаний по этому вопросу и может привести к тому, что люди будут использовать его, не зная, что они делают. Я не понимаю, почему у этого вопроса так много голосов. - person Ricardo Sanchez-Saez; 13.10.2011
comment
Используйте алгоритм хеширования, который имеет хороший баланс между скоростью и небольшим количеством столкновений. Что еще можно сказать, кроме описания различных алгоритмов? - person tcurdt; 27.10.2011
comment
1-я проблема - (int) маленький и легко переполняется, используйте NSUInteger. Вторая проблема - если вы продолжите умножать результат на каждый хэш переменной, ваш результат будет переполнен. например. [NSString hash] создает большие значения. Если у вас 5+ переменных, этот алгоритм легко переполнит. Это приведет к тому, что все будет отображаться в один и тот же хэш, что плохо. См. Мой ответ: stackoverflow.com/a/4393493/276626 - person Paul Solt; 06.01.2012
comment
Хотя на самом деле использование NSUInteger было бы лучше, размер бит не обязательно должен быть больше. Максимальный размер NSUInteger зависит от системы. cocoadev.com/index.pl?NSUInteger. Почему переполнение должно приводить к тому же количество? МАКСИНТ + 1! = МАКСИНТ + 2 - person tcurdt; 20.01.2012
comment
@PaulSolt - Переполнение не проблема при генерации хеша, коллизия. Но переполнение не обязательно делает столкновение более вероятным, и ваше утверждение о переполнении, вызывающем сопоставление всего одного и того же хэша, просто неверно. - person DougW; 15.06.2012
comment
@DougW Оглядываясь назад на эту тему, вы правы. Переполнение не имеет значения. Похоже, у меня была ошибка, из-за которой объекты nil давали результат 0. Логические / целые числа вычислялись последними, и это приводило к конфликту хэшей. Было непонятно, почему использовалась каждая строка (простое число * результат), но я вижу, что это помогает предотвратить обнуление результата при умножении. - person Paul Solt; 25.06.2012
comment
@DougW На момент написания ваших комментариев код был основан на int: целочисленный тип со знаком. Подписанное переполнение - это неопределенное поведение, поэтому вполне возможно получить то же самое число. Рекомендация @ PaulScott о предпочтении NSUInteger верна и более правильна. - person Nikolai Ruhe; 18.09.2015
comment
Есть ли предпочтительный способ включения перечислений в хеш? Это вообще полезно? - person ariopolis; 18.09.2015
comment
Здесь есть огромная проблема с кодом для случая bool, тернарный оператор имеет более низкий приоритет, чем умножение и сложение, поэтому они будут выполняться первыми, а условное выражение всегда будет оценивать true, давая вам 1231. Если вы все еще хотите использовать тернарный оператор , используйте круглые скобки вокруг него, чтобы он вычислялся первым перед умножением и сложением. - person James Van Boxtel; 14.09.2016
comment
Для логических значений должно быть result = prime * result + (var?1231:1237); - person kolbasek; 13.03.2017
comment
Я считаю, что XOR'ing объектов намного быстрее, чем умножение чего-либо. Я заметил, что вы написали, что это не самый эффективный вариант, но все же это не большое изменение и, как мне кажется, намного лучше. - person Nat; 08.05.2017
comment
© Vive XOR'ing хеш-значений приводит к большему количеству конфликтов хешей, что в дальнейшем пагубно скажется на производительности. - person fishinear; 28.03.2020

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

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

С точки зрения лучших практик, ваш хэш должен возвращать случайное распределение значений для входных данных. Это означает, что, например, если у вас есть double, но большинство ваших значений имеют тенденцию к кластеризации между 0 и 100, вам необходимо убедиться, что хэши, возвращаемые этими значениями, равномерно распределены по всему диапазону возможных значений хеш-функции. . Это значительно улучшит вашу производительность.

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

person Brian B.    schedule 31.10.2008
comment
+1 Отличный ответ, заслуживает большего количества голосов, тем более что он на самом деле говорит о лучших практиках и теории, по которой важен хороший (уникальный) хеш. - person Quinn Taylor; 11.07.2009

В 99% случаев достаточно простого XOR над хеш-значениями критических свойств.

Например:

- (NSUInteger)hash
{
    return [self.name hash] ^ [self.data hash];
}

Решение найдено на http://nshipster.com/equality/ Мэттом Томпсоном (который также сослался на этот вопрос в своем Почта!)

person Yariv Nissim    schedule 16.11.2013
comment
Проблема с этим ответом в том, что он вообще не принимает во внимание примитивные значения. И примитивные значения также могут быть важны для хеширования. - person Nat; 08.05.2017
comment
@Vive Большинство этих проблем решены в Swift, но эти типы обычно представляют собой собственный хэш, поскольку они примитивны. - person Yariv Nissim; 11.05.2017
comment
Хотя вы правы в пользу Swift, все же есть много проектов, написанных с помощью objc. Поскольку ваш ответ посвящен objc, его стоит хотя бы упомянуть. - person Nat; 11.05.2017
comment
Объединение хеш-значений XOR вместе - плохой совет, это приводит ко многим конфликтам хешей. Вместо этого умножьте на простое число, а затем сложите, как указано в других ответах. - person fishinear; 28.03.2020

Я нашел эту цепочку чрезвычайно полезной, предоставляя все необходимое для реализации моих методов isEqual: и hash с помощью одной уловки. При тестировании переменных экземпляра объекта в isEqual: пример кода использует:

if (![(id)[self name] isEqual:[aWidget name]])
    return NO;

Это неоднократно не удавалось (т.е., возвращалось NO) без ошибки, когда я знал, что объекты были идентичны в моем модульное тестирование. Причина заключалась в том, что одна из переменных экземпляра NSString была nil, поэтому приведенный выше оператор был:

if (![nil isEqual: nil])
    return NO;

и поскольку nil будет реагировать на любой метод, это совершенно законно, но

[nil isEqual: nil]

возвращает nil, что равно NO, поэтому, когда и объект, и тестируемый имеют объект nil, они будут считаться не равными (< i> т.е., isEqual: вернет NO).

Это простое исправление заключалось в изменении оператора if на:

if ([self name] != [aWidget name] && ![(id)[self name] isEqual:[aWidget name]])
    return NO;

Таким образом, если их адреса совпадают, он пропускает вызов метода независимо от того, являются ли они оба nil или оба указывают на один и тот же объект, но если либо не nil, либо они указывают на разные объекты, тогда соответствующим образом вызывается компаратор.

Надеюсь, это избавит кого-то от нескольких минут почесывания головы.

person LavaSlider    schedule 26.11.2010

Хеш-функция должна создавать полууникальное значение, которое вряд ли будет конфликтовать или совпадать с хеш-значением другого объекта.

Вот полная хеш-функция, которую можно адаптировать к переменным экземпляра ваших классов. Он использует NSUInteger, а не int для совместимости с 64/32-битными приложениями.

Если результат становится равным 0 для разных объектов, вы рискуете столкнуться с хешами. Конфликтующие хэши могут привести к неожиданному поведению программы при работе с некоторыми классами коллекции, которые зависят от хеш-функции. Обязательно проверьте свою хеш-функцию перед использованием.

-(NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;
    NSUInteger yesPrime = 1231;
    NSUInteger noPrime = 1237;
    
    // Add any object that already has a hash function (NSString)
    result = prime * result + [self.myObject hash];
    
    // Add primitive variables (int)
    result = prime * result + self.primitiveVariable; 

    // Boolean values (BOOL)
    result = prime * result + (self.isSelected ? yesPrime : noPrime);
    
    return result;
}
person Paul Solt    schedule 08.12.2010
comment
Здесь есть одна проблема: я предпочитаю избегать синтаксиса с точкой, поэтому я преобразовал ваш оператор BOOL в (например) result = prime * result + [self isSelected] ? yesPrime : noPrime;. Затем я обнаружил, что это устанавливает result на (например) 1231, я полагаю, из-за того, что оператор ? имеет приоритет. Я исправил проблему, добавив скобки: result = prime * result + ([self isSelected] ? yesPrime : noPrime); - person Ashley; 12.06.2013

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

Не используйте | =, это приведет к насыщению. Вместо этого используйте ^ =.

Случайный забавный факт: [[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]], но [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]. (rdar: // 4538282, открыт с 5 мая 2006 г.)

person Jens Ayton    schedule 31.10.2008
comment
Вы совершенно правы в | =. На самом деле не это имел в виду. :) + = и ^ = довольно эквивалентны. Как вы обрабатываете нецелочисленные примитивы, такие как double и float? - person Dave Dribin; 31.10.2008
comment
Случайный забавный факт: проверьте это на Snow Leopard ... ;-) - person Quinn Taylor; 11.07.2009
comment
Он прав, говоря об использовании XOR вместо OR для объединения полей в хеш. Однако не используйте совет возвращать одно и то же значение -hash для каждого объекта - хотя это легко, это может серьезно ухудшить производительность всего, использующего хэш объекта. Хэш не должен отличаться от объектов, которые не равны, но если вы можете этого добиться, ничего подобного нет. - person Quinn Taylor; 11.07.2009
comment
Открытый отчет об ошибке радара закрыт. openradar.me/4538282 Что это означает? - person JJD; 19.11.2010
comment
JJD, ошибка была исправлена ​​в Mac OS X 10.6, как намекнул Куинн. (Обратите внимание, что этому комментарию два года.) - person Jens Ayton; 19.11.2010
comment
Возвращать один и тот же хэш для каждого экземпляра - ЧРЕЗВЫЧАЙНО плохой совет. И использование XOR для объединения хеш-значений - тоже плохой совет. Вместо этого, как утверждают другие ответы, умножьте на простое число, а затем сложите. - person fishinear; 28.03.2020

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

Сохраняйте простоту хеширования. Выберите наиболее характерную переменную члена (или нескольких членов).

Например, для CLPlacemark достаточно только имени. Да, есть 2 или 3 отличительных знака CLPlacemark с одним и тем же именем, но они встречаются редко. Используйте этот хеш.

@interface CLPlacemark (equal)
- (BOOL)isEqual:(CLPlacemark*)other;
@end

@implementation CLPlacemark (equal)

...

-(NSUInteger) hash
{
    return self.name.hash;
}


@end

Заметьте, я не утруждаюсь указанием города, страны и т. Д. Названия достаточно. Возможно название и CLLocation.

Хеш должен распределяться равномерно. Таким образом, вы можете объединить несколько переменных-членов с помощью символа вставки ^ (знак xor)

Так что это что-то вроде

hash = self.member1.hash ^ self.member2.hash ^ self.member3.hash

Таким образом, хеш будет равномерно распределен.

Hash must be O(1), and not O(n)

Итак, что делать в массиве?

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

person user4951    schedule 24.09.2012
comment
XORing хеш-значения не дает равномерного распределения. - person fishinear; 28.03.2020

Подождите, конечно, гораздо более простой способ сделать это - сначала переопределить - (NSString )description и предоставить строковое представление состояния вашего объекта (вы должны представить все состояние вашего объекта в этой строке).

Затем просто предоставьте следующую реализацию hash:

- (NSUInteger)hash {
    return [[self description] hash];
}

Это основано на принципе, что «если два строковых объекта равны (как определено методом isEqualToString:), они должны иметь одинаковое хеш-значение».

Источник: Справочник по классу NSString

person Jonathan Ellis    schedule 17.01.2012
comment
Это предполагает, что метод описания будет уникальным. Использование хэша описания создает зависимость, которая может быть неочевидной, и повышает риск коллизий. - person Paul Solt; 25.06.2012
comment
+1 проголосовали за. Это отличная идея. Если вы опасаетесь, что описания вызывают коллизии, вы можете отменить это. - person user4951; 23.09.2012
comment
Спасибо, Джим, я не буду отрицать, что это что-то вроде взлома, но он сработает в любом случае, о котором я могу думать - и, как я уже сказал, при условии, что вы переопределите description, я не понимаю, почему это хуже к любому из решений, получивших более высокую оценку. Возможно, это не самое элегантное с математической точки зрения решение, но оно должно помочь. Как заявляет Брайан Б. (наиболее одобренный ответ на данный момент): Я стараюсь избегать создания новых алгоритмов хеширования - согласен! - Я просто hash тот NSString! - person Jonathan Ellis; 25.09.2012
comment
Проголосовали за то, что это отличная идея. Я не собираюсь его использовать, потому что боюсь дополнительных выделений NSString. - person karwag; 07.05.2013
comment
Это не универсальное решение, поскольку для большинства классов description включает адрес указателя. Таким образом, получается два разных экземпляра одного и того же класса, которые равны с разным хешем, что нарушает базовое предположение о том, что два одинаковых объекта имеют одинаковый хеш! - person Diogo T; 06.04.2016
comment
Для -[hash] важнее быть быстрым, чем избегать столкновений, и я не думаю, что description очень быстро ... - person DeFrenZ; 13.02.2018

Контракты равенства и хеширования хорошо определены и тщательно исследованы в мире Java (см. Ответ @mipardi), но все те же соображения должны применяться к Objective-C.

Eclipse надежно генерирует эти методы на Java, поэтому вот пример Eclipse, вручную перенесенный на Objective-C:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if ([self class] != [object class])
        return false;
    MyWidget *other = (MyWidget *)object;
    if (_name == nil) {
        if (other->_name != nil)
            return false;
    }
    else if (![_name isEqual:other->_name])
        return false;
    if (_data == nil) {
        if (other->_data != nil)
            return false;
    }
    else if (![_data isEqual:other->_data])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = 1;
    result = prime * result + [_name hash];
    result = prime * result + [_data hash];
    return result;
}

А для подкласса YourWidget, который добавляет свойство serialNo:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if (![super isEqual:object])
        return false;
    if ([self class] != [object class])
        return false;
    YourWidget *other = (YourWidget *)object;
    if (_serialNo == nil) {
        if (other->_serialNo != nil)
            return false;
    }
    else if (![_serialNo isEqual:other->_serialNo])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = [super hash];
    result = prime * result + [_serialNo hash];
    return result;
}

Эта реализация позволяет избежать некоторых подводных камней в примере isEqual: от Apple:

  • Тест класса 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 с той же базовой последовательностью символов, независимо от различия _20 _ / _ 21_, а также независимо от того, какие частные классы в кластере классов NSString задействованы.

В таких случаях isEqual: должно иметь четко определенное, хорошо задокументированное поведение, и должно быть ясно, что подклассы не могут это переопределить. В Java ограничение «без переопределения» может быть реализовано путем пометки методов equals и hashcode как final, но Objective-C не имеет эквивалента.

person jedwidz    schedule 19.05.2013

Это не дает прямого ответа на ваш вопрос (вообще), но я использовал MurmurHash раньше для генерации хэшей: murmurhash

Думаю, мне стоит объяснить почему: ропот чертовски быстр ...

person schwa    schedule 31.10.2008
comment
Библиотека C ++, ориентированная на уникальные хэши для ключа void * с использованием случайного числа (а также не имеющая отношения к объектам Objective-C), здесь не является полезным предложением. Метод -hash должен каждый раз возвращать согласованное значение, иначе он будет совершенно бесполезен. Если объект добавлен в коллекцию, которая вызывает -hash и каждый раз возвращает новое значение, дубликаты никогда не будут обнаружены, и вы также никогда не сможете получить объект из коллекции. В этом случае термин хэш отличается от значения в безопасности / криптографии. - person Quinn Taylor; 11.07.2009
comment
murmurhash - это не криптографическая хеш-функция. Пожалуйста, проверьте свои факты, прежде чем публиковать неверную информацию. Murmurhash может быть полезным для хеширования пользовательских классов objective-c (особенно, если у вас задействовано много NSDatas), потому что это очень быстро. Однако я действительно согласен с тем, что, возможно, предлагать это не лучший совет для кого-то, кто просто набирает objective-c, но, пожалуйста, обратите внимание на мой префикс в моем исходном ответе на вопрос. - person schwa; 22.01.2010

Я нашел эта страница, чтобы быть полезным руководством по переопределению методов равного и хеш-типа. В нем есть неплохой алгоритм вычисления хэш-кодов. Страница ориентирована на Java, но ее довольно легко адаптировать к Objective-C / Cocoa.

person mipadi    schedule 31.10.2008
comment
кешированная ссылка на archive.org: http://web.archive.org/web/20071013053633/http://www.geocities.com/technofundo/tech/java/equalhash.html - person cobbal; 20.05.2010

Я тоже новичок в Objective C, но я нашел отличную статью о идентичности и равенстве в Objective C здесь. Из моего чтения похоже, что вы могли бы просто сохранить хеш-функцию по умолчанию (которая должна обеспечивать уникальную идентификацию) и реализовать метод isEqual, чтобы он сравнивал значения данных.

person ceperry    schedule 24.02.2009
comment
Я новичок в Cocoa / Objective C, и этот ответ и ссылка действительно помогли мне прорезать все более сложные вещи, приведенные выше, в нижнюю строку - мне не нужно беспокоиться о хэшах - просто реализовал метод isEqual :. Спасибо! - person John Gallagher; 14.12.2009
comment
Не пропустите ссылку @ ceperry. Статья Equality vs Identity Карла Крафта действительно хороша. - person JJD; 19.11.2010
comment
@ Джон: Думаю, тебе стоит перечитать статью. В нем очень четко сказано, что равные экземпляры должны иметь равные хэш-значения. Если вы переопределите isEqual:, вы также должны переопределить hash. - person Steve Madsen; 16.03.2011

Куинн ошибается, считая, что здесь бесполезно упоминать хэш бормотания. Куинн прав в том, что вы хотите понять теорию хеширования. Ропот превращает эту теорию в реализацию. Стоит изучить, как применить эту реализацию к этому конкретному приложению.

Некоторые из ключевых моментов здесь:

Пример функции из tcurdt предполагает, что «31» - хороший множитель, потому что он простой. Нужно показать, что простота - необходимое и достаточное условие. На самом деле 31 (и 7), вероятно, не очень хорошие простые числа, потому что 31 == -1% 32. Нечетный умножитель с примерно половиной установленных битов и половиной битов очищенными, вероятно, будет лучше. (Этим свойством обладает константа умножения хэша murmur.)

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

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

Может быть полезно внимательно следить за порядком, в котором объединяются элементы. Вероятно, сначала следует обработать логические значения и другие элементы, значения которых не сильно распределены.

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

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

person Community    schedule 31.08.2009

Комбинируя ответ @ tcurdt с ответом @ oscar-gomez для получения имен свойств, мы можем создать простое решение для обоих isEqual и хэш:

NSArray *PropertyNamesFromObject(id object)
{
    unsigned int propertyCount = 0;
    objc_property_t * properties = class_copyPropertyList([object class], &propertyCount);
    NSMutableArray *propertyNames = [NSMutableArray arrayWithCapacity:propertyCount];

    for (unsigned int i = 0; i < propertyCount; ++i) {
        objc_property_t property = properties[i];
        const char * name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        [propertyNames addObject:propertyName];
    }
    free(properties);
    return propertyNames;
}

BOOL IsEqualObjects(id object1, id object2)
{
    if (object1 == object2)
        return YES;
    if (!object1 || ![object2 isKindOfClass:[object1 class]])
        return NO;

    NSArray *propertyNames = PropertyNamesFromObject(object1);
    for (NSString *propertyName in propertyNames) {
        if (([object1 valueForKey:propertyName] != [object2 valueForKey:propertyName])
            && (![[object1 valueForKey:propertyName] isEqual:[object2 valueForKey:propertyName]])) return NO;
    }

    return YES;
}

NSUInteger MagicHash(id object)
{
    NSUInteger prime = 31;
    NSUInteger result = 1;

    NSArray *propertyNames = PropertyNamesFromObject(object);

    for (NSString *propertyName in propertyNames) {
        id value = [object valueForKey:propertyName];
        result = prime * result + [value hash];
    }

    return result;
}

Теперь в вашем пользовательском классе вы можете легко реализовать isEqual: и hash:

- (NSUInteger)hash
{
    return MagicHash(self);
}

- (BOOL)isEqual:(id)other
{
    return IsEqualObjects(self, other);
}
person johnboiles    schedule 29.10.2013

Обратите внимание, что если вы создаете объект, который может быть изменен после создания, значение хеш-функции не должно не изменяться, если объект вставлен в коллекцию. Практически это означает, что значение хеш-функции должно быть зафиксировано с момента создания исходного объекта. См. документацию Apple по методу -hash протокола NSObject для получения дополнительной информации:

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

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

person user10345    schedule 03.11.2008
comment
Вы неправильно читаете хеш-документы - это, по сути, ситуация либо-либо. Если объект изменяется, обычно изменяется и хеш. Это действительно предупреждение для программиста, что если хеш изменяется в результате изменения объекта, то изменение объекта, когда он находится в коллекции, использующей хеш, вызовет неожиданное поведение. Если объект должен быть безопасно изменен в такой ситуации, у вас нет другого выбора, кроме как сделать хэш не связанным с изменяемым состоянием. Эта конкретная ситуация кажется мне странной, но, безусловно, бывают нечастые ситуации, когда она применима. - person Quinn Taylor; 11.07.2009

Извините, если я рискну показаться здесь полным болваном, но ... ... никто не позаботился упомянуть, что для следования `` лучшим практикам '' вам определенно не следует указывать метод equals, который НЕ будет учитывать все данные, принадлежащие вашему целевому объекту, например, что угодно данные агрегированы для вашего объекта, а не ассоциированного с ним, следует учитывать при реализации equals. Если вы не хотите принимать во внимание, скажем, «возраст» при сравнении, тогда вам следует написать компаратор и использовать его для сравнения вместо isEqual :.

Если вы определяете метод isEqual:, который произвольно выполняет сравнение на равенство, вы подвергаетесь риску того, что этот метод будет неправильно использован другим разработчиком или даже вами, если вы забудете «поворот» в своей интерпретации равенства.

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

person Thibaud de Souza    schedule 05.11.2009