Why does [NSSet containsObject] fail for SKNode members in iOS8?

The NSSet

added two objects, but when I check the membership, I can not find them.

The test code below worked fine in iOS7 but crashes in 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");
}

      

Output:

could not find node

What happened between iOS7 and iOS8 and how can I fix it?

+3


source to share


1 answer


SKNode

implementation isEqual

and hash

changed in iOS8 to include object data members (not just the object's memory address).

Apple's documentation for Collections warns of this exact situation:

If mutable objects are stored in a collection, either the hash method of the objects should not depend on the internal state of the mutable objects or the mutable objects should not be modified while theyre in the collection. For example, a mutable dictionary can be put into a set, but you should not change it while it is there.

And, more directly, here :

Saving modified objects to collection objects can cause problems. Some collections can become invalid or even damaged if objects contain a mutation, because by mutating these objects can affect the way they are placed into the collection.

The general situation is described in other questions in more detail . However, I will repeat the explanation of the example SKNode

, hoping it will help those who discovered this issue when upgrading to iOS8.

In this example, an object is SKNode

changingNode

inserted into NSSet

(implemented using a hash table). The hash value of the object is calculated and a bucket is assigned to it in the hash table: let say bucket 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];

      

Output:



pointer 790756a0 hash 838599421

Then it changes changingNode

. The modification changes the value of the hash object. (In iOS7, changing an object like this didn't change its hash value.)

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

      

Output:



pointer 790756a0 hash 3025143289

Now when called containsObject

, the computed hash value is (probably) assigned to another bucket: say bucket 2. All objects in bucket 2 are compared to the test object using isEqual

, but of course all returns NO.

In a real-life example, the modification changedObject

is likely happening elsewhere. If you try to debug at the site of the call containsObject

, you might be confused to find that the collection contains an object with the same address and hash value as the search object, and yet the search fails.

Alternative implementations (each with its own set of problems)

  • Use only immutable objects in collections.

  • Place objects in collections only when you have complete control, now and forever, over their implementations isEqual

    and hash

    .

  • Track a set of (not saved) pointers, not a set of objects: [NSSet setWithObject:[NSValue valueWithPointer:(void *)changingNode]]

  • Use a different collection. For example, NSArray

    changes to will affect isEqual

    but will not be affected by changes to hash

    . (Of course, if you're trying to keep the array sorted for faster searches, you'll have similar problems.)

  • This is often the best alternative for my real-world situations: use NSDictionary

    where key is [NSValue valueWithPointer]

    and object is a stored pointer. This gives me a quick search for an object that will be valid even if the object changes; quick removal; and saving the objects placed in the collection.

  • Similar to the latter, with different semantics and some other useful options: use NSMapTable

    with option NSMapTableObjectPointerPersonality

    to treat key objects as pointers for hashing and equality.

+3


source







All Articles