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

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

Раздел Introspection в Cocoa Руководство по основам имеет пример того, как переопределить 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?

( Brain fart: Изначально он писал "побитовое ИЛИ" вместе с |=. Meant add.)

Ответ 1

Начните с

 NSUInteger prime = 31;
 NSUInteger result = 1;

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

 result = prime * result + var

Для 64-битного вам также может потребоваться сдвиг и xor.

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

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

 result = prime * result + [var hash];

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

[nil isEqual: nil]

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

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

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

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

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

Ответ 4

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

Например:

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

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

Ответ 5

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

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

Ответ 7

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

Не используйте | =, однако, он насытится. Используйте ^ = вместо.

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

Ответ 8

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

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

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

Ответ 9

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

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

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

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

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

Ответ 10

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

Ответ 11

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

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

Ответ 12

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

Ответ 13

Контракты 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 ограничение "no override" может быть принудительно введено, помещая методы equals и hashcode как final, но Objective-C не имеет эквивалента.

Ответ 14

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

Ответ 15

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

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

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

Ответ 16

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

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

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

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

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

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

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

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

Ответ 17

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

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

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