it-swarm.com.ru

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

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

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

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

263
Dave Dribin

Начать с

 NSUInteger prime = 31;
 NSUInteger result = 1;

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

 result = prime * result + var

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

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

110
tcurdt

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

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

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

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

81
Brian B.

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

Например:

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

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

31
Yariv

Я нашел этот поток чрезвычайно полезным, предоставляя все, что мне нужно, чтобы мои методы isEqual: и hash были реализованы с помощью одной команды catch. При тестировании переменных экземпляра объекта в 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;

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

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

27
LavaSlider

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

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

Это мне очень помогло! Может быть, ответ, который вы ищете. реализует равенство и хэширование

18
Steve M

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

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

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

13
Jens Ayton

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

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

Например, для 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)

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

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

10
user4951

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

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

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

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

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

7
Jonathan Ellis

Равные и хеш-контракты хорошо определены и тщательно исследованы в мире 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 не имеет эквивалента.

5
jedwidz

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

5
mipadi

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

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

5
schwa

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

4
ceperry

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

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

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

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

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

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

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

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

3
Ces

Комбинируя ответ @ 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);
}
3
johnboiles

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

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

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

2
user10345

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

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

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

1
Thibaud de Souza