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