Почему [NSSet containsObject] не работает для членов SKNode в iOS8?

Два объекта добавлены в NSSet, но когда я проверяю членство, я не могу найти ни одного из них.

Приведенный ниже тестовый код отлично работал в iOS7, но не работал в iOS8.

SKNode *changingNode = [SKNode node];
SKNode *unchangingNode = [SKNode node];
NSSet *nodes = [NSSet setWithObjects:unchangingNode, changingNode, nil];

changingNode.position = CGPointMake(1.0f, 1.0f);

if ([nodes containsObject:changingNode]) {
  printf("found node\n");
} else {
  printf("could not find node\n");
}

Выход:

не смог найти узел

Что произошло между iOS7 и iOS8 и как это исправить?


person Karl Voskuil    schedule 01.10.2014    source источник


Ответы (1)


Реализации SKNode isEqual и hash изменились в iOS8 и теперь включают элементы данных объекта (а не только адрес памяти объекта).

документация Apple для коллекций предупреждает именно об этой ситуации:

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

И, более конкретно, здесь :

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

Общая ситуация подробно описана в других вопросах. Тем не менее, я повторю объяснение для примера SKNode, надеясь, что оно поможет тем, кто обнаружил эту проблему при обновлении до iOS8.

В примере SKNode объект changingNode вставляется в NSSet (реализовано с помощью хеш-таблицы). Вычисляется хеш-значение объекта, и ему присваивается сегмент в хеш-таблице: скажем, сегмент 1.

SKNode *changingNode = [SKNode node];
SKNode *unchangingNode = [SKNode node];
printf("pointer %lx hash %lu\n", (uintptr_t)changingNode, (unsigned long)changingNode.hash);
NSSet *nodes = [NSSet setWithObjects:unchangingNode, changingNode, nil];

Выход:

указатель 790756a0 хеш 838599421

Затем изменяется changingNode. Модификация приводит к изменению хеш-значения объекта. (В iOS7 подобное изменение объекта не приводило к изменению его хеш-значения.)

changingNode.position = CGPointMake(1.0f, 1.0f);
printf("pointer %lx hash %lu\n", (uintptr_t)changingNode, (unsigned long)changingNode.hash);

Выход:

указатель 790756a0 хэш 3025143289

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

В реальном примере изменение changedObject, вероятно, происходит в другом месте. Если вы попытаетесь выполнить отладку в месте вызова containsObject, вы можете быть сбиты с толку, обнаружив, что коллекция содержит объект с точно таким же адресом и хеш-значением, что и объект поиска, и все же поиск завершается ошибкой.

Альтернативные реализации (каждая со своим набором проблем)

  • Используйте только неизменяемые объекты в коллекциях.

  • Помещайте объекты в коллекции только тогда, когда у вас есть полный контроль, сейчас и навсегда, над их реализациями isEqual и hash.

  • Отслеживайте набор (несохраненных) указателей, а не набор объектов: [NSSet setWithObject:[NSValue valueWithPointer:(void *)changingNode]]

  • Используйте другую коллекцию. Например, на NSArray повлияют изменения в isEqual, но не на hash. (Конечно, если вы попытаетесь отсортировать массив для более быстрого поиска, у вас возникнут аналогичные проблемы.)

  • Часто это лучшая альтернатива для моих реальных ситуаций: используйте NSDictionary, где ключ — это [NSValue valueWithPointer], а объект — сохраненный указатель. Это дает мне: быстрый поиск объекта, который будет действительным, даже если объект изменится; быстрое удаление; и сохранение объектов, помещенных в коллекцию.

  • Подобно последнему, с другой семантикой и некоторыми другими полезными опциями: используйте NSMapTable с опцией NSMapTableObjectPointerPersonality, чтобы ключевые объекты обрабатывались как указатели для хеширования и равенства.

person Karl Voskuil    schedule 01.10.2014