Vincent Gable’s Blog

July 8, 2010

NSDictionary Copies It’s Keys

Filed under: Bug Bite,Cocoa,iPhone,MacOSX,Objective-C,Programming | , , ,
― Vincent Gable on July 8, 2010

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 a copy. (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 mutable NSObject, [x copy] is an immutable copy of x, at that point in time. Any changes to x 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 you copy a view object, the copy won’t (always) be isEqual: 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 an NSDictionary, 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.

6 Comments »

  1. 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

  2. Good tip Joel!

    It occurs to me just now that a [NSArray arrayWithObject:uncopyableObject] might work even better, because it (and its “copies”) will retain 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

  3. 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

  4. I’m not sure if two different NSArrays that contain the same object instance will evaluate as equal, or even have the same hash.

    They will. Arrays are equal if their contents are equal, as defined by isEqual:, so even the following works:

    	NSString *s1 = @"hey!";
    	NSMutableString *s2 = [NSMutableString stringWithString:s1];
    	assert([s1 isEqual:s2]);
    	assert(s1 != s2);//s1 and s2 aren't the same object in memory
    	NSArray *a1 = [NSArray arrayWithObject:s1];
    	NSArray *a2 = [NSArray arrayWithObject:s2];
    	assert([a1 isEqual:a2]);
    	assert(a1 != a2);
    

    I haven’t read the CF-Lite source, but I’ve seen NSArray behave like it implements hash by just returning count. 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 assume NSDictionary does the right thing quickly enough until I’m proven otherwise. *knock on wood*.

    Comment by Vincent Gable — July 8, 2010 @ 5:55 pm

  5. Turns out [NSArray arrayWithObject:uncopyableObject] isn’t a “safe” key if uncopyableObject is mutable. I was using a for(…in…) loop to iterate over a dictionary that used arrays containing one UISegmentedControl object as a key. In the loop, the UISegmentedControl 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 of for(NSArray* arrayKey in theDictionary) “fixed” the problem, because the array allKeys 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

  6. The title should be “NSDictionary Copies Its Keys”.

    “its” not “it’s”

    Comment by bob — January 18, 2011 @ 5:22 pm

RSS feed for comments on this post.

Leave a comment

Powered by WordPress