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

267

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

( Мозг пердеть : Первоначально написал "побитовое ИЛИ" их вместе с |=. Значит добавить.)

Дейв Дрибин
источник
2
if (![other isKindOfClass:[self class]])- Технически это означает, что равенство не будет коммутативным. Т.е. A = B не означает B = A (например, если один является подклассом другого)
Robert
Документация ссылка мертва, теперь архивируются Самоанализ
jedwidz

Ответы:

111

Начать с

 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 есть несколько интересных комментариев (поиск по их именам), которые ссылаются на первоисточник.

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

tcurdt
источник
9
Откуда взялись 1231: 1237? Я также вижу это в Java Boolean.hashCode (). Это волшебно?
Дэвид Леонард
17
Природа алгоритмов хеширования заключается в том, что будут конфликты. Так что я не понимаю твою точку зрения, Пол.
tcurdt
85
На мой взгляд, этот ответ не отвечает на реальный вопрос (лучшие практики для переопределения хэша NSObject). Он просто предоставляет один конкретный алгоритм хеширования. Кроме того, ограниченность объяснения затрудняет понимание без глубоких знаний по этому вопросу и может привести к тому, что люди будут использовать его, не зная, что они делают. Я не понимаю, почему у этого вопроса столько голосов.
Рикардо Санчес-Саез
6
1-я проблема - (int) мала и легко переполняется, используйте NSUInteger. 2-я проблема - если вы продолжите умножать результат на хэш каждой переменной, ваш результат будет переполнен. например. [NSString hash] создает большие значения. Если у вас есть 5+ переменных, этот алгоритм легко переполнить. Это приведет к тому, что все будет отображаться в один и тот же хеш, что плохо. Смотрите мой ответ: stackoverflow.com/a/4393493/276626
Пол Солт
10
@PaulSolt - Переполнение не является проблемой при генерации хеша, столкновение есть. Но переполнение не обязательно делает конфликт более вероятным, и ваше утверждение о переполнении, заставляющем все отображаться в один и тот же хеш, просто неверно.
DougW
81

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

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

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

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

Брайан Б.
источник
4
+1 Отличный ответ, заслуживает большего количества голосов, особенно потому, что он на самом деле говорит о «лучших практиках» и теории, объясняющей, почему хороший (уникальный) хэш важен.
Куинн Тейлор
30

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

Например:

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

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

Ярив Ниссим
источник
1
Проблема с этим ответом состоит в том, что он вообще не принимает во внимание примитивные значения. И примитивные значения также могут быть важны для хеширования.
Vive
@Vive Большинство этих проблем решаются в Swift, но эти типы обычно представляют свои собственные хеши, поскольку они примитивны.
Ярив Ниссим
1
Хотя вы подходите для Swift, все же есть много проектов, написанных на objc. Поскольку ваш ответ посвящен objc, стоит хотя бы упомянуть.
Vive
Совместное использование значений хеша - плохой совет, оно приводит к множеству коллизий хешей. Вместо этого умножьте на простое число, а затем добавьте, как указано в других ответах.
рыбий день
27

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

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

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

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

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

[nil isEqual: nil]

возвращает ноль , то есть НЕТ , поэтому, когда и у объекта, и у тестируемого был нулевой объект, они считались бы не равными ( т.е. , isEqual:будет возвращать NO ).

Это простое исправление состояло в том, чтобы изменить оператор if на:

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

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

Надеюсь, это сэкономит кому-то несколько минут от царапин на голове.

LavaSlider
источник
20

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

Вот полная хеш-функция, которую можно адаптировать к переменным экземпляра ваших классов. Он использует 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;
}
Пол Солт
источник
3
Здесь есть один момент: я предпочитаю избегать точечного синтаксиса, поэтому я преобразовал вашу инструкцию BOOL в (например) result = prime * result + [self isSelected] ? yesPrime : noPrime;. Затем я обнаружил, что это настройка result(например) 1231, я полагаю, из-за того, что ?оператор имеет приоритет. Я исправил проблему, добавив скобки:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Эшли
12

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

Не используйте | =, хотя, это насытит. Вместо этого используйте ^ =.

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

Дженс Айтон
источник
1
Ты точно прав на | =. На самом деле это не значит. :) + = и ^ = довольно эквивалентны. Как вы обрабатываете нецелочисленные примитивы, такие как double и float?
Дэйв Дрибин
Случайный забавный факт: протестируйте его на снежном барсе ... ;-)
Куинн Тейлор
Он правильно использует XOR вместо OR для объединения полей в хеш. Однако не используйте совет по возвращению одинакового значения -hash для каждого объекта - хотя это легко, это может серьезно ухудшить производительность всего, что использует хэш объекта. Хеш не обязательно должен быть разным для объектов, которые не равны, но если вы можете достичь этого, ничего подобного нет.
Куинн Тейлор
Отчет об ошибке открытого радара закрыт. openradar.me/4538282 Что это значит?
JJD
JJD, ошибка была исправлена ​​в Mac OS X 10.6, как намекнул Куинн. (Обратите внимание, что комментарию два года.)
Дженс Айтон
9

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

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

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

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

@implementation CLPlacemark (equal)

...

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


@end

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

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

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

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

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

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

Так что же делать в массиве?

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

user4951
источник
Значения хеш-функции XOR не дают равномерного распределения.
рыбий день
7

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

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

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

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

Источник: Ссылка класса NSString

Джонатан Эллис
источник
1
Это предполагает, что метод описания будет уникальным. Использование хэша описания создает зависимость, которая может быть неочевидной, и повышает риск коллизий.
Пол Солт
1
+1 Проголосовал. Это потрясающая идея. Если вы боитесь, что описания вызывают коллизии, вы можете переопределить это.
user4951 23.09.12
Спасибо, Джим, я не буду отрицать, что это что-то вроде хака, но это сработает в любом случае, о котором я могу подумать - и, как я уже сказал, при условии, что вы переопределите description, я не понимаю, почему это хуже любое из решений с более высоким рейтингом. Возможно, это не самое математически элегантное решение, но оно должно сработать. Как заявляет Брайан Б. (наиболее одобренный ответ на данный момент): «Я стараюсь избегать создания новых алгоритмов хеширования» - согласился! - Я просто ! hashNSString
Джонатан Эллис
Проголосовал, потому что это хорошая идея. Я не собираюсь использовать это, хотя, потому что я боюсь дополнительных распределений NSString.
Карваг
1
Это не общее решение, так как для большинства классов descriptionуказатель адреса. Таким образом, это создает два разных экземпляра одного и того же класса, которые равны разному хешу, что нарушает базовое предположение, что два равных объекта имеют одинаковый хеш!
Диого Т
5

Контракты equals и hash хорошо определены и тщательно исследованы в мире 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с той же базовой последовательностью символов, независимо от различия NSString/ NSMutableString, а также независимо от того, какие частные классы в NSStringкластере классов задействованы.

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

jedwidz
источник
@adubr Это описано в моих последних двух параграфах. Это не фокус, как MyWidgetпонимается, не кластер классов.
Джедвидз
5

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

Думаю, я должен объяснить, почему: ропот быстро

нейтральный гласный звук
источник
2
Библиотека C ++, которая ориентирована на уникальные хеши для ключа void *, использующего случайное число (а также не относящегося к объектам Objective-C), на самом деле не является полезным предложением здесь. Метод -hash должен каждый раз возвращать согласованное значение, иначе оно будет совершенно бесполезным. Если объект добавляется в коллекцию, которая вызывает -hash и каждый раз возвращает новое значение, дубликаты никогда не будут обнаружены, и вы также никогда не сможете извлечь объект из коллекции. В этом случае термин «хеш» отличается от значения в безопасности / криптографии.
Куинн Тейлор
3
murmurhash - это не криптографическая хеш-функция. Пожалуйста, проверьте ваши факты, прежде чем размещать неверную информацию. Murmurhash может быть полезен для хэширования пользовательских классов target -c (особенно если у вас много NSDatas), потому что это очень быстро. Тем не менее, я допускаю, что, возможно, предлагая это не лучший совет, чтобы дать кому-то «просто взять цель-c», но, пожалуйста, запишите мой префикс в моем первоначальном ответе на вопрос.
Шва
5

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

mipadi
источник
1
кэшированная ссылка через archive.org: web.archive.org/web/20071013053633/http://www.geocities.com/…
cobbal
4

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

ceperry
источник
Я новичок в Cocoa / Objective C, и этот ответ и ссылка действительно помогли мне перебрать все более продвинутые вещи, приведенные выше, к нижней строке - мне не нужно беспокоиться о хешах - просто реализую метод isEqual: Спасибо!
Джон Галлахер
Не пропустите ссылку @ ceperry. Статья Equality vs IdentityКарла Крафта действительно хороша.
JJD
6
@ Джон: Я думаю, что вы должны перечитать статью. В нем очень четко сказано, что «одинаковые экземпляры должны иметь одинаковые значения хеш-функции». Если вы переопределите isEqual:, вы также должны переопределить hash.
Стив Мэдсен
3

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

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

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

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

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

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

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

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


источник
3

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

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);
}
johnboiles
источник
2

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

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

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

user10345
источник
1
Вы неправильно читаете хеш-документы - это, по сути, ситуация "или-или". Если объект изменяется, хэш обычно также изменяется. Это действительно предупреждение для программиста, что если хеш изменяется в результате изменения объекта, то изменение объекта, находящегося в коллекции, использующей хеш, вызовет неожиданное поведение. Если объект должен быть «безопасно изменяемым» в такой ситуации, у вас нет другого выбора, кроме как сделать хэш не связанным с изменяемым состоянием. Эта конкретная ситуация звучит странно для меня, но, безусловно, бывают ситуации, когда это применимо.
Куинн Тейлор
1

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

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

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

Тибо де Соуза
источник