Overriding isEquals in Objective-C

I am currently working on a small personal project, an iPhone app written in Objective-C. I needed a simple data structure to hold a 2-element tuple of integers. I originally implemented this as a C-struct:

1
2
3
4
5
struct Index {
    NSInteger row;
    NSInteger column;
};
typedef struct Index Index;

This worked fine, but I often found myself doing a multiline declare-then-set-members pattern every time I wanted to use the data structure. To simplify, I added a small C function to generate structs, similar to CGRectMake:

1
2
3
4
5
6
7
Index IndexMake(NSInteger r, NSInteger c)
{
    Index index;
    index.row = r;
    index.column = c;
    return index;
}

This worked great until I needed to put the data structure into a collection such as an NSMutableSet. Because the contained objects in such a collection need to be Objective-C pointers, you need to encapsulate the struct. According to this SO post, the correct way to do so is this:

1
2
Index index = IndexMake(i, j);
[selectedIndices addObject:[NSValue valueWithBytes:&index objCType:@encode(Index)]]

So this is now starting to get a bit ugly. Once I realized that I would be using this data structure all throughout my app, I decided it would be way easier to just make it a proper Objective-C class.

1
2
3
4
@interface GridIndex : NSObject
@property (nonatomic) NSInteger row;
@property (nonatomic) NSInteger column;
@end

With a static initializer, this really cleaned up the dependent code. Of course, this also promptly broke the entire program. Why? Because I hadn’t overridden the default isEqual: method in my custom class. My first attempt at doing so did not work as expected:

1
2
3
4
5
6
7
-(BOOL)isEqual:(id)object
{
    if(![(NSObject*)object isKindOfClass:[GridIndex class]])
        return NO;
    GridIndex *other = (GridIndex*)object;
    return self.row == other.row && self.column == other.column;
}

After adding a bunch of debugging I realized that the problem was when checking if an NSSet contained a GridIndex with a specific row and column. More debugging showed that my isEqual: method was not called deterministically by the set containment method. Sometimes my overridden function was called and worked as expected, sometimes it was totally bypassed and the result was always false.

After some more research, this post gave me the answer. As it turns out, to override isEquals: in Objective-C you are actually expected to do three things.

  • Implement a new isEqualTo__ClassName__: method, which performs the meaningful value comparison.
  • Override isEqual: to make class and object identity checks, falling back on the aforementioned value comparison method.
  • Override hash.

Doing so fixed the problem. Good thing someone had already written on this issue because I did not realize all of this was strictly necessary when overriding a simple isEquals: method.

Here’s my final implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-(BOOL)isEqualToGridIndex:(GridIndex*)other
{
    return self.row == other.row && self.column == other.column;
}

-(BOOL)isEqual:(id)object
{
    if(![(NSObject*)object isKindOfClass:[GridIndex class]])
        return NO;
    return [self isEqualToGridIndex:(GridIndex*)object];
}

-(NSUInteger)hash
{
    return self.row + self.column;
}

Comments

Copyright © 2013 Chris Greenwood
Powered by Octopress. Design credit: Shashank Mehta