5

So i'm overriding isEquals and hash to compare custom objects to be able to remove duplicates from a NSArray. The problem is that i'm missing some values in the list which contains no duplicated items, and it seems that my hash or isEquals implementation is wrong. The custom object is a Course object which has some variables like: id and name I'll put the code here:

- (BOOL)isEqual:(id)object {
    if ([object isKindOfClass:[Course self]]) {
        return YES;
    } 
    if(self == object){
        return YES;
    }
    else {
        return NO;
    }
}

- (unsigned)hash {

    NSString *idHash = [NSString stringWithFormat: @"%d", self._id];
    return [idHash hash];
}

Then, after querying the database i put the values in an array and then in a set which should remove the duplicated items like this:

NSMutableSet *noDuplicates = [[NSMutableSet alloc] initWithArray:tempResults];

Can you see what i'm doing wrong in the isEquals or hash implementation?

Thanks a lot.

madcoderz
  • 4,423
  • 10
  • 47
  • 74
  • See this question which is similar to yours: http://stackoverflow.com/questions/254281/best-practices-for-overriding-isequal-and-hash – vakio Jul 01 '11 at 09:25

4 Answers4

11

Step 1. Decide which instance variables / state are used to determine equality. It's a good idea to make sure properties exist for them (they can be private properties declared in a class extension if you like).

Step 2. Write a hash function based on those instance variables. If all the properties that count are objects, you can just xor their hashes together. You can also use C ints etc directly.

Step 3. Write isEqual: The normal pattern is probably to first test that both objects are in the class or a subclass of the method in which isEqual: is defined and then to test equality for all the properties.

So if a class Person has a name property (type NSString) and a number property (type int) which together define a unique person, hash might be:

-(NSUInteger) hash
{
    return [[self name] hash] ^ [self number];
}

isEqual: might be

-(BOOL) isEqual: (id) rhs
{
    BOOL ret = NO;
    if ([rhs isKindOfClass: [Person class]]) // do not use [self class]
    {
        ret = [[self name] isEqualToString: [rhs name]] && [self number] == [rhs number];
    } 
    return ret;
}

I don't think it is stated as an explicit requirement in the doc but it is probably assumed that equality is symmetric and transitive i.e.

  • [a isEqual: b] == [b isEqual: a] for all a and b
  • [a isEqual: b] && [b isEqual: c]implies [a isEqual: c] for all a, b, c

So you have to be careful if you override isEqual: for subclasses to make sure it works both ways round. This is also why the comment, do not use [self class] above.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
3

Well, your isEqual: implementation really just tests if the two objects are the same class. That's not at all correct. Without knowing the details of your object, I don't know what a good implementation would look like, but it would probably follow the structure

- (BOOL)isEqual:(id)object {
    if ([object isMemberOfClass:[self class]]) {
        // test equality on all your important properties
        // return YES if they all match
    }
    return NO;
}

Similarly, your hash is based on converting an int into a string and taking its hash. You could also just return the int itself as your hash.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • well i tried to test equality but i couldn't or didn't know the syntax like `if (self._id == object->_id) { }` that wouldn't work and the hash wouldn't let me return an int – madcoderz Jul 01 '11 at 09:09
2

Your code violates the principal of "objects that are equal should have equal hashes." Your hash method generates a hash from self._id and doesn't take that value into consideration when evaluating the equality of the objects.

Concepts in Objective-C Programming has a section on introspection where this topic has examples and coverage. isEqual is meant to answer the question of two objects are equivalent even if they are two distinct instances. So you want to return a BOOL indicating if the object should be considered equivalent. If you don't implement isEqual it will simply compare the pointer for equality which is not what you probably want.

- (BOOL)isEqual:(id)object {
    BOOL result = NO;

    if ([object isKindOfClass:[self class]]) {
        result = [[self firstName] isEqualToString:[object firstName]] && 
        [[self lastName] isEqualToString:[object lastName]] &&
        [self age] == [object age];
    }

    return result;
}

From the NSObject Protocol Reference:

Returns an integer that can be used as a table address in a hash table structure.

If two objects are equal (as determined by the isEqual: 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.

- (NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;

    result = prime * result + [_firstName hash];
    result = prime * result + [_lastName hash];
    result = prime * result + _age;

    return result;
}

So what defines two objects as equal is defined by the programmer and their needs. However, whatever methodology of equality is developed, equal objects should have equal hashes.

Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
-14

this is how you implement hash and isEqual (at-least the one which is working for me for purpose of identifying duplicates)

Hash Function

The Apple Doc says that the hash of two objects should be same for those which are considered equal( logically). hence I would implement the hash as below

-(unsigned int)hash  
{  
    return 234;//some random constant  
} 

isEqual: method implemenation would be something like

-(BOOL)isEqual:(id)otherObject  
{  
  MyClass *thisClassObj = (MyClass*)otherObject;

  *// this could be replaced by any condition statement which proves logically that the two object are same even if they are two different instances* 

return ([thisClassObj primaryKey] == [self primaryKey]);

}

More reference here : Techniques for implementing -hash on mutable Cocoa objects

Implementing -hash / -isEqual: / -isEqualTo...: for Objective-C collections

Community
  • 1
  • 1
Bharath Booshan
  • 211
  • 2
  • 10
  • 1
    Just remember that returning the same hash for all objects might lead to performance problems in collection classes. – JeremyP Jul 01 '11 at 10:57
  • Jeremey You are right.. If I do not override hash function, then isEqual method is not invoked in case the there are two instances of the object which are logically same.. Does Anyone has a solution for this? – Bharath Booshan Jul 01 '11 at 12:27
  • 1
    A constant for a hash result is very poor; if you had 1000 items in a dictionary, they will all be stored in the same dictionary bucket internally, and when you get/set objects you will have poor performance - the hash should resolve to something in your isEqual method (eg, a primaryKey.hash) and NOT be a constant. – Andrew Mar 21 '14 at 01:51
  • How is this the accepted answer? A constant hash value is terrible, it totally defeats the purpose of hashing – André Fratelli Jul 21 '15 at 21:56