0

For example the Object is something like this:

MyUser: NSObject{
   NSString *firstName;
   NSString *lastName;
   NSString *gender;
   int       age;
}

and I would like to compare to user, if their attributes are the same, I will treat it as equal... instead of write a static method to compare enough attribute one by one, can I have a lazy way to get all the attribute to compare themselves, Thanks.?

DNB5brims
  • 29,344
  • 50
  • 131
  • 195
  • See also: ["Best practices for overriding isEqual: and hash"](http://stackoverflow.com/q/254281/) – outis Feb 07 '12 at 01:53

1 Answers1

-1

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.

Community
  • 1
  • 1
outis
  • 75,655
  • 22
  • 151
  • 221
  • While this is interesting from a 'mechanics' standpoint, it's really not a good general recommendation. Making a data structure implement NSFastEnumeration just to support a lazy man's isEqual: implementation is just horrible API design. The question wants something for nothing, and sadly there's no such thing as a free lunch. Even languages/runtimes that provide standard POD-style equality tests by default don't do so by abusing mechanisms like enumerability. This is a horrible approach, in the abstract. – ipmcc Feb 07 '12 at 02:59
  • @ipmcc: for a class of just a few properties, I agree completely. One might be able to make the case for the above approach for a class with scores of properties (though, at that point, there are probably other design issues with the class). This looks to be one of those KISS situations, hence the last bit in my answer. – outis Feb 07 '12 at 03:05