Рекомендации по переопределению isEqual: и hash

Как правильно переопределить isEqual: в Objective-C? "Улов", кажется, что если два объекта равны (как определено isEqual: method), они должны иметь одинаковое хэш-значение.

на интроспекция на Руководство По Основам Какао есть пример того, как переопределить 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?

(мозг пердеть: первоначально написал "побитовый или" их вместе с |=. Означало добавить.)

17 ответов


Начнем с

 NSUInteger prime = 31;
 NSUInteger result = 1;

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

 result = prime * result + var

для 64bit вы также можете захотеть shift и xor.

 result = prime * result + (int) (var ^ (var >>> 32));

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

 result = prime * result + [var hash];

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

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

объяснение и атрибуция

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

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

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


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

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

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

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


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

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

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

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

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

[nil isEqual: nil]

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

это легко исправить-изменить оператор if для:

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

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

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


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

например:

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

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


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

вот полная хэш-функция,которая может быть адаптирована к переменным экземпляра классов. Он использует 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;
}

Это мне очень помогло! Может быть ответ вы ищете. реализация равенства и хеширования


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

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

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


помните, что вам нужно только предоставить хэш, который равен, когда 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.

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

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

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

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

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

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

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


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

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

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

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

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


контракты 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. Если a MyWidget экземпляр сравнивает равным двум YourWidget экземпляров, то эти YourWidget экземпляры должны сравниваться равными друг другу, даже если их serialNo отличается.

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


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

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


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


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


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

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

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

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

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

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

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

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


объединение ответа @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);
}

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

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

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


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

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

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