An NSDictionary
will retain
it’s objects, and copy
it’s keys.
Here are some effects this has had on code I’ve worked on.
-
Sometimes you get the same object you put in, sometimes not.
Immutable objects are optimized to return themselves as acopy
. (But with some exceptions!). So the following code:NSDictionary *d = [NSDictionary dictionaryWithObject:@"object" forKey:originalKey]; for(id aKey in d) if(aKey == originalKey) NSLog(@"Found the original key!");
Might print “Found the original key!”, and might not, depending on how
[originalKey copy]
is implemented. For this reason, never use pointer-equality when comparing keys. - Mutable objects make bad keys. If
x
is a mutableNSObject
,[x copy] is an
immutable copy ofx
, at that point in time. Any changes tox
are not reflected in the copy. For example,[dict setObject:x forKey:key]; //...code that changes key, but not dict assert([[dict objectForKey:key] isEqual:x]); //fails!
Because the
copy
is an immutable object, it will blow up if you try to mutate it.NSMutableString *key = //something... [dict setObject:x forKey:key]; for(NSMutableString *aKey in dict) [aKey appendString:@"2"]; //Error, aKey isn't mutable, even though key is!
-
View objects make bad keys. Views have state related to the screen: their
frame
, position in the view hierarchy, animation layers, etc. When youcopy
a view object, the copy won’t (always) beisEqual:
to the original, because it’s not on the screen in exactly the same way. -
Your classes must support
NSCopying
to be used as a key in anNSDictionary
, you can’t just implement-hash
and-isEqual:
in your custom classes.
Of course, this isn’t a complete list of every way key-copying can trip you up. But if you understand what copy
means in Cocoa, and remember how NSDictionary
works, you’ll be able to avoid or quickly solve any issues.
How to Document Such Behavior Better Than Apple Did
Given what we know about NSDictionary
, what’s wrong with the following snippit from NSDictionary.h
?
@interface NSMutableDictionary : NSDictionary - (void)setObject:(id)anObject forKey:(id)aKey; @end
Answer: aKey
needs to implement NSCopying
, so it should be of type (id<NSCopying>)
instead of type (id)
. That way, the header is self-documenting, and, if like most smart programmers, you’re using autocomplete to type out Cocoa’s long method names, the auto-completed template will be self-documenting too.
One handy trick: instead of using an object instance as a key, explicitly place the pointer to the object in an NSValue and use *that* as the key.
I use this to associate extra data with UITouch instances. The UITouch object itself makes a bad key (it’s mutable, and doesn’t implement NSCopying), but the pointer value works very well.
Comment by Joel Bernstein — July 8, 2010 @ 1:10 pm
Good tip Joel!
It occurs to me just now that a
[NSArray arrayWithObject:uncopyableObject]
might work even better, because it (and its “copies”) willretain
the object, so you’re sure that as long as the key-array is around, the object it represents is around. But I haven’t tried it, so maybe there are pitfalls in practice.Comment by Vincent Gable — July 8, 2010 @ 1:23 pm
Depends; I’m not sure if two different NSArrays that contain the same object instance will evaluate as equal, or even have the same hash. I actually have no idea how NSDictionary does its internal hashing; the docs have been kept unhelpfully vague.
I’ll run a test tonight.
Comment by Joel Bernstein — July 8, 2010 @ 5:32 pm
They will. Arrays are equal if their contents are equal, as defined by
isEqual:
, so even the following works:I haven’t read the CF-Lite source, but I’ve seen
NSArray
behave like it implementshash
by just returningcount
. If that’s true, I could see using an array having bad performance in some situations (eg every key is an array with one item, so they all have the same hash). I don’t worry about it too much though because I generally assumeNSDictionary
does the right thing quickly enough until I’m proven otherwise. *knock on wood*.Comment by Vincent Gable — July 8, 2010 @ 5:55 pm
Turns out
[NSArray arrayWithObject:uncopyableObject]
isn’t a “safe” key ifuncopyableObject
is mutable. I was using afor(…in…)
loop to iterate over a dictionary that used arrays containing oneUISegmentedControl
object as a key. In the loop, theUISegmentedControl
objects were changed. This threw an exception because the dictionary was “mutated while being enumerated.”Also, given how
isEqual:
is defined for arrays, changing the objects in the array, can change it’s “equality”, and cause problems.In my case, doing
for(NSArray* arrayKey in [theDictionary allKeys])
instead offor(NSArray* arrayKey in theDictionary)
“fixed” the problem, because the arrayallKeys
wasn’t thrown off by what I was doing … I’m not sure that it’s safe though.Comment by Vincent Gable — July 16, 2010 @ 1:26 pm
The title should be “NSDictionary Copies Its Keys”.
“its” not “it’s”
Comment by bob — January 18, 2011 @ 5:22 pm