Защо [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