For comparison, this is what you're trying to avoid writing.
-(NSUInteger)hash {
return [firstName hash] ^ [lastName hash] ^ [gender hash] ^ age;
}
-(BOOL)isEqual:(id)other {
return [other isKindOfClass:[self class]]
&& age == other.age
&& [gender isEqualToString:other.gender]
&& [firstName isEqualToString:other.firstName]
&& [lastName isEqualToString:other.lastName];
}
Using XOR is an extremely simple way of combining hashes, and I mostly include it as a stand-in. It may hurt the quality of the hash value, depending on distribution of the underlying hash functions. If the hashes have a uniform distribution, it should be all right. Note also that combining hashes only works because NSStrings that are equal in content have the same hashes. This approach won't work with all types; in particular, it won't work with types that use the default implementation of hash
.
To get around writing the above, first change the type of the age
property to NSNumber
, so it doesn't have to be handled as a special case. You don't have to change the ivar, though you can if you want.
@interface MyUser : NSObject {
...
unsigned int age; // Or just make this an NSNumber*
}
...
@property (assign,nonatomic) NSNumber *age;
@implementation MyUser
@synthesize firstName, lastName, gender;
/* if the age ivar is an NSNumber*, the age property can be synthesized
instead of explicitly defining accessors.
*/
@dynamic age;
-(NSNumber*)age {
return [NSNumber numberWithUnsignedInt:age];
}
-(void)setAge:(NSNumber*)newAge {
age = [newAge unsignedIntValue];
}
Second, make sure your class supports the fast enumeration protocol. If it doesn't, you can implement -countByEnumeratingWithState:objects:count:
by making use of reflection (with the Objective-C runtime functions) to get the list of properties for instances of your class. For example (taken in part from "Implementing countByEnumeratingWithState:objects:count:" on Cocoa With Love):
#import <objc/runtime.h>
...
@interface MyUser (NSFastEnumeration) <NSFastEnumeration>
-(NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len;
@end
@implementation MyUser
@synthesize firstName, lastName, gender;
/* defined in the main implementation rather than a category, since there
can be only one +[MyUser initialize].
*/
static NSString **propertyNames=0;
static unsigned int cProperties=0;
+(void)initialize {
unsigned int i;
const char *propertyName;
objc_property_t *properties = class_copyPropertyList([self class], &cProperties);
if ((propertyNames = malloc(cProperties * sizeof(*propertyNames)))) {
for (i=0; i < cProperties; ++i) {
propertyName = property_getName(properties[i]);
propertyNames[i] = [[NSString alloc]
initWithCString:propertyName
encoding:NSASCIIStringEncoding];
}
} else {
cProperties = 0;
// Can't initialize property names. Fast enumeration won't work. What do?
}
}
...
@end
@implementation MyUser (NSFastEnumeration)
-(NSUInteger)
countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id *)stackbuf
count:(NSUInteger)len
{
if (state->state >= cProperties) {
return 0;
}
state->itemsPtr = propertyNames;
state->state = cProperties;
state->mutationsPtr = (unsigned long *)self;
return cProperties;
}
@end
Last, implement hash
(using fast enumeration) and isEqual:
. Hash should calculate the hashes of all properties, then combine them to create the hash for the MyUser
instance. isEqual:
can simply check the other object is an instance of MyUser
(or a subclass thereof) and compare hashes. For example:
-(NSUInteger)hash {
NSUInteger myHash=0;
for (NSString *property in self) {
// Note: extremely simple way of combining hashes. Will likely lead
// to bugs
myHash ^= [[self valueForKey:property] hash];
}
return myHash;
}
-(BOOL)isEqual:(id)other {
return [other isKindOfClass:[self class]]
&& [self hash] == [other hash];
}
Now, ask yourself which is less work overall. If you want a single approach what will work for all your classes, it might be the second (with some changes, such as turning +initialize
into a class method on NSObject
that returns the property name array and length), but in all likelihood the former is the winner.
There's a danger in both of the above hash
implementations with calculating the hash based on property values. From Apple's documentation on hash
:
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.
Since you want isEqual:
to be true whenever two objects have the same property values, the hashing scheme must depend directly or indirectly on the object's state, so there's no getting around this danger.