Подтвердить что ты не робот

Реализация -hash/-isEqual:/-isEqualTo...: для Objective-C коллекций

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


Фон

NSObject предоставляет стандартные реализации -hash (который возвращает адрес экземпляра, например (NSUInteger)self) и -isEqual: (который возвращает NO, если адреса приемника и параметр не совпадают). Эти методы предназначены для переопределения по мере необходимости, но в документации четко указано, что вы должны предоставить оба или ни одно из них. Кроме того, если -isEqual: возвращает YES для двух объектов, то результат -hash для этих объектов должен быть таким же. Если нет, могут возникнуть проблемы, когда объекты, которые должны быть одинаковыми, например два экземпляра строк, для которых -compare: возвращает NSOrderedSame -, добавляются в коллекцию Cocoa или сравниваются напрямую.

Контекст

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

Вместо сравнения только адресов памяти, эти сравнения должны учитывать объекты, присутствующие в двух коллекциях (включая порядок, если применимо). Этот подход имеет прецедент в Cocoa и обычно использует отдельный метод, включая следующее:

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

Проблемы

Метод -isEqualTo...: отлично работает сам по себе, но классы, которые определяют эти методы, обычно также переопределяют -isEqual: для вызова [self isEqualTo...:], если параметр имеет тот же класс (или, возможно, подкласс) в качестве получателя, или [super isEqual:] в противном случае. Это означает, что класс должен также определить -hash, чтобы он возвращал одно и то же значение для разрозненных экземпляров, имеющих одинаковое содержимое.

Кроме того, документация Apple для -hash предусматривает следующее: (акцент мой)

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

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

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

Для меня имеет смысл реализовать -isEqual: и -hash, поскольку (например) косвенный пользователь одного из моих классов может не знать конкретного метода -isEqualTo...: для вызова или даже заботиться о том, являются ли два объекта экземпляры того же класса. Они должны иметь возможность вызывать -isEqual: или -hash для любой переменной типа id и получать ожидаемый результат.

В отличие от -isEqual: (который имеет доступ к двум экземплярам, ​​которые сравниваются), -hash должен возвращать результат "вслепую", имея доступ только к данным в конкретном экземпляре. Поскольку он не может знать, для чего используется хеш, результат должен быть последовательным для всех возможных экземпляров, которые должны считаться равными/идентичными и всегда должны совпадать с -isEqual:удаp > . (Edit: Это было развенчано ответами ниже, и это, безусловно, облегчает жизнь.) Кроме того, писать хорошие хэш-функции нетривиально - гарантировать уникальность является проблемой, особенно когда у вас есть только NSUInteger (32/64 бит) в котором его представлять.

Вопросы

  • Существуют ли лучшие практики при реализации сравнений равенства -hash для коллекций?
  • Есть ли какие-либо особенности для планирования в Objective-C и Cocoa -сеских коллекциях?
  • Есть ли хорошие подходы к модульному тестированию -hash с разумной степенью уверенности?
  • Любые предложения по реализации -hash для согласования с -isEqual: для коллекций, содержащих элементы произвольных типов? О каких ошибках я должен знать? ( Изменить: Не так проблематично, как я думал сначала, - указывает @kperryua, - значения -hash не подразумевают -isEqual: ".)

Изменить: Я должен был уточнить, что я не смущен тем, как реализовать -isEqual: или -isEqualTo...: для коллекций, это просто. Я думаю, что моя путаница возникла главным образом из (ошибочно) мысли, что -hash ДОЛЖЕН вернуть другое значение, если -isEqual: возвращает NO. Сделав криптографию в прошлом, я думал, что хеши для разных значений ДОЛЖНЫ быть разными. Однако приведенные ниже ответы помогли мне понять, что "хорошая" хеш-функция действительно о минимизации коллизий и цепочки цепочек для коллекций, которые используют -hash. Хотя уникальные хеши предпочтительнее, они не являются строгим требованием.

4b9b3361

Ответ 1

Я думаю, что попытка придумать какую-то полезную хэш-функцию, которая будет генерировать уникальные значения хэша для коллекций, - это бесполезное упражнение. Предложение U62 о объединении хэшей всего содержимого не будет хорошо масштабироваться, так как оно делает хэш-функцию O (n). Хеш-функции должны действительно быть O (1), чтобы обеспечить хорошую производительность, иначе цель хэша будет побеждена. (Рассмотрим общую конструкцию Cocoa plists, которые являются словарями, содержащими массивы и другие словари, потенциально аномальным. Попытка взять хэш словаря верхнего уровня большого plist будет мучительно медленным, если хэш-функции коллекций были О (п).)

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

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

Я надеюсь, что ответит хотя бы на один из ваших вопросов.

Что касается № 1: Реализация -isEqual: довольно разрезана и суха. Вы перечисляете содержимое и проверяете isEqual: по каждому из элементов.

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

Нет. 2 является своего рода расплывчатым. Не уверен, что вы имеете в виду.

Ответ 2

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

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

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

Ответ 3

Я провел некоторое расследование по реализации хэш файла NSArray и NSMutableArray и (если только я не понял что-то), он выглядит как Apple, не следуя собственным правилам:

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

Вот мой тестовый код

NSMutableArray* myMutableArray = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil];
NSMutableArray* containerForMutableArray = [NSMutableArray arrayWithObject:myMutableArray];

NSUInteger hashBeforeMutation = [[containerForMutableArray objectAtIndex:0] hash];
[[containerForMutableArray objectAtIndex:0] removeObjectAtIndex:1];
NSUInteger hashAfterMutation = [[containerForMutableArray objectAtIndex:0] hash];

NSLog(@"Hash Before: %d", hashBeforeMutation);
NSLog(@"Hash After : %d", hashAfterMutation);

Вывод:

Hash Before: 3
Hash After : 2

Таким образом, это похоже на реализацию по умолчанию для метода Hash как для NSArray, так и для NSMutableArray - это подсчет массива, и он не заботится о том, находится ли он внутри коллекции или нет.