How to properly override isEqual: in Objective-C? The "catch" seems to be that if two objects are equal (as determined by the isEqual: method), they must have the same hash value.
The Introspection section of the Cocoa Fundamentals Guide does have an example on how to override isEqual:, copied as follows, for a class named ShowWidget:
- (BOOL)isEqual:(id)showOther {
if (showOther == self)
return YES;
if (!showOther || ![showOther isKindOfClass:[self class]])
return NO;
return [self isEqualToWidget:showOther];
}
- (BOOL)isEqualToWidget:(ShowWidget *)showWidget {
if (self == showWidget)
return YES;
if (![(id)[self name] isEqual:[showWidget name]])
return NO;
if (![[self data] isEqualToData:[showWidget data]])
return NO;
return YES;
}
It checks pointer equality, then class equality, and finally compares the objects using isEqualToWidget:, which only checks the name and data properties. What the example doesn't show is how to override hash.
Let's assume there are other properties that do not affect equality, say age. Shouldn't the hash method be overridden such that only name and data affect the hash? And if so, how would you do that? Just add the hashes of name and data? For example:
- (NSUInteger)myHash {
NSUInteger myHash = 0;
myHash += [[self name] myHash];
myHash += [[self data] myHash];
return myHash;
}
Is that sufficient? Is there a better technique? What if you have primitives, like int? Convert them to NSNumber to get their hash? Or structs like NSRect?
Tarun Kumar
16-Sep-2015I found this thread extremely helpful supplying everything I needed to get my isEqual: and hash methods implemented with one catch. When testing object instance variables in isEqual: the example code uses:
if (![(id)[self name] isEqual:[showWidget name]])
return NO;
This repeatedly failed (i.e., returned NO) without and error, when I knew the objects were identical in my unit testing. The reason was, one of the NSString instance variables was nil so the above statement was:
and since nil will respond to any method, this is perfectly legal but
returns nil, which is NO, so when both the object and the one being tested had a nil object they would be considered not equal (i.e., isEqual: would return NO).
This simple fix was to change the if statement to:
This way, if their addresses are the same it skips the method call no matter if they are both nil or both pointing to the same object but if either is not nil or they point to different objects then the comparator is appropriately called.
I hope this saves someone a few minutes of head scratching.