Реализации 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