4

I've always used NSDictionaries with strings as keys, and pretty much all the examples on the web/books/etc. are the same. I figured that I'd try it with a custom object for a key. I've read up on implementing the "copyWithZone" method and created the following basic class:

@interface CustomClass : NSObject
{
    NSString *constString;
}

@property (nonatomic, strong, readonly) NSString *constString;

- (id)copyWithZone:(NSZone *)zone; 

@end

@implementation CustomClass

@synthesize constString;

- (id)init
{
    self = [super init];
    if (self) {
        constString = @"THIS IS A STRING";
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone
{
    CustomClass *copy = [[[self class] allocWithZone: zone] init];
    return copy;
}

@end

Now I'm trying to just add one of these objects with a simple string value, and then getting the string value back out to log to the console:

CustomClass *newObject = [[CustomClass alloc] init];
NSString *valueString = @"Test string";
NSMutableDictionary *dict =
[[NSMutableDictionary alloc] initWithObjectsAndKeys:valueString, newObject, nil];

    NSLog(@"Value in Dictionary: %@", [dict objectForKey: newObject]);
    // Should output "Value in Dictionary: Test string"

Unfortunately the log displays a (null). I'm pretty sure I'm missing something really obvious, and feel like I need another set of eyes.

5StringRyan
  • 3,604
  • 5
  • 46
  • 69

2 Answers2

7

NSDictionary key objects work off three methods:

  • -(NSUInteger)hash
  • -(BOOL)isEqual:(id)other
  • -(id)copyWithZone:(NSZone*)zone

The default NSObject implementation of hash and isEqual: only use the object's pointer, so when your object is copied via copyWithZone: the copy and the original object are no longer equal.

What you need is something like this:

@implementation CustomClass

-(NSUInteger) hash;
{
    return [constString hash];
}

-(BOOL) isEqual:(id)other;
{
    if([other isKindOfClass:[CustomClass class]])
        return [constString isEqualToString:((CustomClass*)other)->constString];
    else
        return NO;
}
- (id)copyWithZone:(NSZone *)zone
{
    CustomClass *copy = [[[self class] allocWithZone: zone] init];
    copy->constString = constString; //might want to copy or retain here, just incase the string isn't a constant
    return copy;
}

@end

It's a little bit difficult to find this out from the documentation. The overview for NSDictionary tells you about isEqual: and NSCopying:

Within a dictionary, the keys are unique. That is, no two keys in a single dictionary are equal (as determined by isEqual:). In general, a key can be any object (provided that it conforms to the NSCopying protocol—see below), but note that when using key-value coding the key must be a string (see “Key-Value Coding Fundamentals”).

And if you have a look at the documentation for -[NSObject isEqual:] it tells you about hash:

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 hash in your subclass.

Tom Dalling
  • 23,305
  • 6
  • 62
  • 80
  • Yeah, I would have put a retain in there if I had ivars that had anything to copy. For the sake of simplicity I just put a constant readonly string in the class just to get the concept of an object for a dictionary key. Just curious though, it seems like the obvious problem was that I wasn't implementing a method that was needed for the dictionary. Since I'm not conforming to any protocols, how would I have known what methods that I needed to implement to get this working (besides doing a stackoverflow search). This way I can figure it out myself for something else like this in the future. – 5StringRyan Sep 20 '11 at 16:25
  • I've updated the answer with some links/quotes from Apple's documentation. – Tom Dalling Sep 20 '11 at 16:47
  • Thanks. I have only two questions. 1) Do I need to add these methods that are in your implementation to the interface file? Why or Why not? 2) It looks like the hash and equal methods are testing just based on that constant string. So if I were to use this for a full blown class, how would I go about updating the hash method? I'm assuming for the isEqual, I would have to exhaustively test each ivar to make sure the two objects are actually equal. – 5StringRyan Sep 21 '11 at 19:31
  • You don't technically need to declare the methods in the interface because `isEqual:`, `hash` and `copy` all come from `NSObject`. It's good practice to declare that you conform to `NSCopying`, though. First decide what makes two objects equal (could be every ivar if you want), then implement `isEqual:`. Next, implement `hash` so that equal objects also have the same hash, but different objects have different hashes. One easy approach is to XOR the hashes of the ivars used in `isEqual:`. The hash shouldn't change while the object is in an `NSDictionary`, so key objects are normally immutable. – Tom Dalling Sep 22 '11 at 02:15
0

I think your class needs to define:

- (BOOL)isEqual:(id)anObject

That is what is probably used by the dictionary to determine if your key is equal to one already used in the dictionary.

mahboudz
  • 39,196
  • 16
  • 97
  • 124