Custom objects as NSDictionary keys

This post describes what is needed for any class to work as a key of NSDictionary or NSMutableDictionary.

The only thing that is really required is that your class adopt the NSCopying protocol.  That’s because every key is copied before being added to the dictionary.  I mention why this design decision was probably made below.

The Tricky Part

However, in many cases you’ll also want to override the

isEqual:

and

hash:

methods as well, with these signatures:

- (BOOL)isEqual:(id)anObject;
- (NSUInteger)hash;

Why? Without these methods, your key objects are only considered equal when they are literally the same object at the same point in memory. For example, suppose you have an NSObject subclass with only one instance variable called

string

, and you do this:

MyObject *obj1 = [MyObject myObjectWithString:@"hello!"];
MyObject *obj2 = [MyObject myObjectWithString:@"hello!"];
[myDict setObject:@"one" forKey:obj1];
[myDict setObject:@"two" forKey:obj2];

You might expect that you now have one key in the dictionary with string value @”hello!” and value @”two”, but in reality you’ll have two keys and two values. The only difference in the keys is where they are in memory, but by default this is what defines the “isEqual” relationship among custom NSObject subclasses.

Defining isEquals and hashing

You can theoretically get away with not defining these methods since you inherit their default implementation from NSObject. However, their default implementation doesn’t know about any of your custom instance variables, so it’s really up to you to handle things if you want more sophisticated dictionary behavior.

The isEqual: method should return YES if and only if the two objects (the receiver and anObject) are of the same type, and represent the same object. It’s up to you to determine what counts as “the same object”.

The hash method needs to return an unsigned integer that is very likely to be different for different objects (in this case “different” means the opposite of “isEqual:” in the sense of your custom isEqual: method). It is absolutely required that whenever

isEqual:

returns YES for two objects, their

hash

integer is identical as well (otherwise dictionary behavior is undefined, and will very likely break).  On the other hand, it is ok to occasionally have different objects with the same hash value, although it’s better if you minimize how often this happens.

Suppose we have a sample NSObject subclass called TwoDice with only two integers, diceOne and diceTwo, as instance variables. (I know the singular of “dice” is “die” but I’m sticking with “dice” because my friends never say die for a single dice. You know what I mean.) Then we might define the methods as follows:

- (NSUInteger)hash {
  return diceOne + 7 * diceTwo;
}

- (BOOL)isEqual:(id)anObject {
  if (![anObject isKindOfClass:[TwoDice class]]) return NO;
  TwoDice *otherDice = (TwoDice *)anObject;
  // This code cares about the order of dice (maybe one is red and one is blue, to distinguish).
  return diceOne == otherDice.diceOne && diceTwo == otherDice.diceTwo;
}

You’ll typically want something like the factor of 7 here to help avoid hash collisions. For example, if I didn’t multiply diceTwo by seven and instead just added, then we would have the same hash value for the pairs (2, 5) and (4, 3). This will not break the dictionary – it can handle false hash collisions like this – but it will slow it down. By carefully choosing your hash method, you’ll keep your dictionary nice and fast, the way it was meant to be.

Why keys are copied (possible gotcha)

You might be wondering why NSDictionary doesn’t just retain the keys, and does the extra work of copying.

Here’s why: Suppose we do something like this –

NSMutableString *string = [NSMutableString stringWithString:@"hi"];
[aDict setObject:@"one" forKey:string];
[string setString:@"there"];
[aDict setObject:@"two" forKey:string];

If the keys were only retained then the dictionary would be in a very weird state right now. It would have two internal bins – one hashed from “hi” and the other from “there”, but both stored value objects (“one” and “two”) would have back-pointers to the single mutable string object with current value “there”. If we did a lookup on @”hi” we would not get a hit because no key would currently have that value. In fact, no lookup at this point could possibly return “one”, so that we have basically wasted memory. Copying keys avoids this problem by keeping the keys the same from that point on.

Keep this tricky spot in mind if you’re using a mutable object as a key!  It can still get you by allowing you to add an object as a key, then later have a failed lookup for the “same” key with a changed internal value.

Adopting NSCopying

You only need to implement one method for this protocol, and it’s implementation is basically always the same:

- (id)copyWithZone:(NSZone *)zone {
  MyObject *objectCopy = [[MyObject allocWithZone:zone] init];
  // Copy over all instance variables from self to objectCopy.
  // Use deep copies for all strong pointers, shallow copies for weak.
  return objectCopy;
}

References

Apple’s topics-for-cocoa notes on using NSDictionary and NSMutableDictionary (quote):

Important: Because the dictionary copies each key, keys must conform to the 

NSCopying

protocol). You should also bear this in mind when choosing what objects to use as keys. Although you can use any object that adopts the 

NSCopying

protocol, it is typically bad design to use large objects such as instances of 

NSImage

since this may incur performance penalties.

NSObject class reference, including descriptions of the hash and isEqual: methods:

Discussion from that page on hash:

If two objects are equal (as determined by the 

<a href="#//apple_ref/occ/intfm/NSObject/isEqual:">isEqual:</a>

method), they must have the same hash value. This last point is particularly important if you define 

hash

in a subclass and intend to put instances of that subclass into a collection.

If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the 

hash

method of the object must not change while the object is in the collection. Therefore, either the 

hash

method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection. Thus, for example, a mutable dictionary can be put in a hash table but you must not change it while it is in there. (Note that it can be difficult to know whether or not a given object is in a collection.)

Discussion from that page on isEqual:

This method defines what it means for instances to be equal. For example, a container object might define two containers as equal if their corresponding objects all respond 

YES

to an 

isEqual:

request. See the 

<a href="../../../Classes/NSData_Class/Reference/Reference.html#//apple_ref/occ/cl/NSData" target="_top">NSData</a>

, 

<a href="../../../Classes/NSDictionary_Class/Reference/Reference.html#//apple_ref/occ/cl/NSDictionary" target="_top">NSDictionary</a>

, 

<a href="../../../Classes/NSArray_Class/NSArray.html#//apple_ref/occ/cl/NSArray" target="_top">NSArray</a>

, and 

<a href="../../../Classes/NSString_Class/Reference/NSString.html#//apple_ref/occ/cl/NSString" target="_top">NSString</a>

class specifications for examples of the use of this method.

If two objects are equal, they must have the same hash value. This last point is particularly important if you define 

isEqual:

in a subclass and intend to put instances of that subclass into a collection. Make sure you also define 

<a href="#//apple_ref/occ/intfm/NSObject/hash">hash</a>

in your subclass.

NSDictionary | NSMutableDictionary class references

One Trackback

  1. By Bynomial Code » Clash-happy method names on June 22, 2010 at 5:52 pm

    […] hash method is used by collections like dictionaries and sets to do fast lookups (more info  on that here).  If you override it for your own hash purposes, your dictionaries and sets are most likely to […]

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*