Two objects are added to an NSSet
, but when I check membership, I can't find one of them.
The test code below worked fine in iOS7 but fails 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?
SKNode
's implementations ofisEqual
andhash
have changed in iOS8 to include data members of the object (and not just the memory address of the object).The Apple documentation for collections warns about this exact situation:
And, more directly, here:
The general situation is described in other questions in detail. However, I'll repeat the explanation for the
SKNode
example, hoping it helps those who discovered this problem with the upgrade to iOS8.In the example, the
SKNode
objectchangingNode
is inserted into theNSSet
(implemented using a hash table). The hash value of the object is computed, and it is assigned a bucket in the hash table: let's say bucket 1.Output:
Then
changingNode
is modified. The modification results in a change to the object's hash value. (In iOS7, changing the object like this did not change its hash value.)Output:
Now when
containsObject
is called, the computed hash value is (likely) assigned to a different bucket: say bucket 2. All objects in bucket 2 are compared to the test object usingisEqual
, but of course all return NO.In a real-life example, the modification to
changedObject
probably happens elsewhere. If you try to debug at the location of thecontainsObject
call, you might be confused to find that the collection contains an object with the exact same address and hash value as the lookup object, and yet the lookup fails.Alternate Implementations (each with their own set of problems)
Only use unchanging objects in collections.
Only put objects in collections when you have complete control, now and forever, over their implementations of
isEqual
andhash
.Track a set of (non-retained) pointers rather than a set of objects:
[NSSet setWithObject:[NSValue valueWithPointer:(void *)changingNode]]
Use a different collection. For instance,
NSArray
will be affected by changes toisEqual
but won't be affected by changes tohash
. (Of course, if you try to keep the array sorted for quicker lookup, you'll have similar problems.)Often this is the best alternative for my real-world situations: Use an
NSDictionary
where the key is the[NSValue valueWithPointer]
and the object is the retained pointer. This gives me: quick lookup of an object that will be valid even if the object changes; quick deletion; and retention of objects put in the collection.Similar to the last, with different semantics and some other useful options: Use an
NSMapTable
with optionNSMapTableObjectPointerPersonality
so that key objects are treated as pointers for hashing and equality.