Cuckoo for Cocoa Dev

A blog for all those Cuckoo for Cocoa Development

Handling Equality With Custom Classes

There comes a time in every Objective-C developers life when we question the equality of an object. Most of the time we are okay with the default -isEqual: method that is given to us with NSObject which tests for equality using the reference of the object itself. However, most of the time we would rather have the custom object’s properties and attributes define and determine equality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Fnacy Dog class
@interface Dog : NSObject

@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *breed;
@property (nonatomic) NSInteger age;

@end

@implementation Dog

@end

// in main
Dog *rinTinTin = [[Dog alloc] init];
rinTinTin.name = @"Rin Tin Tin";
rinTinTin.breed = @"German Shepard";
rinTinTin.age = 95;

Dog *dog = [[Dog alloc] init];
dog.name = @"Rin Tin Tin";
dog.breed = @"German Shepard";
dog.age = 95;

BOOL isEqual = [rinTinTin isEqualToDog:dog];

// isEqual is NO

In this example, if we test for equality using the -isEqual: method as is, it will return NO because the default implementation tests for equality using the object reference. As a “consumer” of the dog class above, if I have a dog object that’s name, breed and age match, then I would want the dogs to be “equal.”

In order to change the default behavior of -isEqual: for a custom class, we need to create our own -isEqual method. If we follow the naming standard (if you want to call it that, all I’m doing is looking at other Cocoa classes for inspiration, i.e. -isEqualToString: for NSString), we have -isEqualToDog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implemenation Dog

- (BOOL)isEqualToDog:(Dog *)dog
{
    if (!dog) {
        return NO;
    }

    BOOL hasEqualName = [self.name isEqualToString:dog.name];
    BOOL hasEqualBreed = [self.breed isEqualToString:dog.breed];
    BOOL hasEqualAge = (self.age == dog.age);

    return hasEqualName && hasEqualBreed && hasEqualAge;
}

@end

The code is pretty straight forward for this example. We first check if the passed in dog object is nil and return NO automatically if it is, and then take the properties for each dog and just compare the values of each. If they are all the same, then they are equal and the method returns YES.

In addition to creating our own -isEqualToDog method, we also need to override the -isEqual: method from NSObject to call our -isEqualToDog: method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation Dog

// other methods

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[Dog class]]) {
        return NO;
    }

    return [self isEqualToDog:(Dog *)object];
}

@end

The first quick check we made is to check for object reference equality. If the object is exactly the same object from the object reference itself, we just return YES and not bother calling our -isEqualToDog: method. The second check we make is checking the object type itself. If the object being passed in isn’t even a dog type, then we know they can’t be equal and we again don’t bother calling our -isEqualToDog: method as well.

You’d think at this point we would be done. We’ve created our own method to test for equality as well as overridden the default -isEqual: method, but unfortunately if we override -isEqual:, we also HAVE to override the NSObject -hash method.

1
2
3
4
5
6
7
8
9
10
@implementation Dog

// other methods

- (NSUInteger)hash
{
    return [self.name hash] ^ [self.breed hash] + self.age;
}

@end

If you are wondering why I chose such a hash method let me give a quick explanation. If you spend some time using the Googles, Stack Overflows and other random blogs, it seems like there various opinions on what constitutes a good hashing algorithm. While I was search for something to actually “use” consistently in my applications I found an article from NSHipster that says doing a bit shift using the string properties works 99% of the time. In order to give the age property some love, I just add it to the shifted string properties.

Now that we have a created a -isEqualToDog: method and overridden -isEqual: method, we now have a custom Dog class that is much more robust than the default equality given to us by NSObject.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Dog *pancho = [[Dog alloc] init];
pancho.name = @"Pancho";
pancho.breed = @"Chihuahua";
pancho.age = 7;

Dog *buddy = [[Dog alloc] init];
buddy.name = @"Buddy";
buddy.breed = @"Chihuahua";
buddy.age = 4;

Dog *chihuahua = [[Dog alloc] init];
chihuahua.name = @"Pancho";
chihuahua.breed = @"Chihuahua";
chihuahua.age = 7;

BOOL isPanchoEqualToBuddy = [pancho isEqualToDog:buddy];
BOOL isPanchoEqualToChihuahua = [pancho isEqualToDog:chihuahua];

// isPanchoEqualToBuddy is NO
// isPanchoEqualToChihuahua is YES

Comments