Search

isEqual: vs isEqualToString:

Mark Dalrymple

9 min read

Mar 25, 2012

isEqual: vs isEqualToString:

_TL;DR: When to use isEqual: or isEqualToString:? There’s no meaningful performance difference between the two. For convenience use isEqual:. For a modicum of type safety use isEqualToString:, but it’s not as safe as you might believe. If you have unicode strings with different normalizations, use compare:. Be careful if nils are involved.
_

Down the Rabbit Hole

A couple of weeks ago I was reviewing a pre-release draft of More Cocoa Programming for Mac OS X: The Big Nerd Ranch Guide, The Book With a Really Long Title, and came across this snippet:

// Change node label only if truly different
if (![newString isEqual:oldString]) {
    [node setLabel:newString];
}

This is perfectly reasonable code: see if the node’s value changed, and if so, change the label. Otherwise optimize out the extra work.

I had my code review hat on, so I started wondering to myself “should it be using isEqualToString: there?” And while pondering this, I realized I didn’t know when you should prefer to use isEqual: or isEqualToString:. That’s not even considering the rest of the pantheon type-specific isEqual: methods like isEqualToDate:, isEqualToAttributedString:, isEqualToValue:, etc, so there must be a use for them. You can also compare strings for equality with compare:. More on that later.

I figured this would be a really easy post to write. “These calls have been around forever and are well-known.” In fact, this is the first posting I started working on when I took over management of the Big Nerd Ranch Weblog. You can see that I’ve been chewing on this for a while given the number of postings we’ve made since I started.

Performance

The only real guidance that Apple gives us in the class references is a reference to performance, saying “When you know both objects are strings, isEqualToString: is a faster way to check equality than isEqual:

So, is isEqualToString: actually faster than isEqual:? And if so, how much faster? (related post about checking assumptions when measuring runtimes) I wrote a simple test program using BNRTimeBlock that compared a string using the three different methods. Here’s the basic code (equaltime.m – you can find it in this gist) that takes two different literal strings, differing at the last character so the method should need to iterate all the characters:

NSString *thing1 = @"Hello there, how are you doing today?";
NSString *thing2 = @"Hello there, how are you doing today!";
CompareStrings (thing1, thing2);

The comparison function is pretty dull:

#define LOOPAGE 10000000
void CompareStrings (NSString *thing1, NSString *thing2) {
    CGFloat equalTime = BNRTimeBlock (^{
        for (NSInteger i = 0; i < LOOPAGE; i++) {
            (void) [thing1 isEqual: thing2];
        }
    });
    CGFloat equalToStringTime = BNRTimeBlock (^{
        for (NSInteger i = 0; i < LOOPAGE; i++) {
            (void) [thing1 isEqualToString: thing2];
        }
    });
    CGFloat compareTime = BNRTimeBlock (^{
        for (NSInteger i = 0; i < LOOPAGE; i++) {
           (void) [thing1 compare: thing2];
        }
    });
    NSLog (@"Time for         isEqual: %f", equalTime);
    NSLog (@"Time for isEqualToString: %f", equalToStringTime);
    NSLog (@"Time for         compare: %f", compareTime);
} // CompareStrings

Running it presents no surprises on a 2.66 GHz i7 MacBook Pro:

Time for         isEqual: 0.761214
Time for isEqualToString: 0.756883
Time for         compare: 0.850303

isEqualToString: has a slight performance edge over isEqual:, which is a bit faster than compare: Seeing as how these all take less than a second to perform ten million operations, the time difference is immaterial. Actually, running the test a couple of times shows that sometimes isEqual: is a hair faster than isEqualToString:, so it’s a wash. (You’d actually want more iterations to account for general machine fluctuations, but I wanted to keep the number of iterations consistent for other timing tests that you’ll see.)

The first performance surprise came running on the iOS simulator (here’s a project). Same timing code was used, and I got these results:

isEqual: 1.381478
isEqualToString: 1.398475
compare: 1.289256

That’s very interesting! It’s about 50% slower. Also compare: is a hair faster. But it’s still ten million comparisons in less than a second and a half.

Quick aside: why the speed difference in the simulator? What’s different? Turns out that the first mac program is a 64-bit executable while the simulator is 32-bit. Compiling equaltime.m for 32-bit using -arch i386 gives comparable times:

Time for         isEqual: 1.516448
Time for isEqualToString: 1.479202
Time for         compare: 0.851608

I have no explanation why compare: is so much faster on 32-bit Mac than its isEqual: kinfolk. I presume there are some awesome optimizations for i386 that don’t translate well, or weren’t ported to x86_64.

edit: It might be related to this constant-time string hashing function that Rob Napier pointed me to in CFString.c, which includes the comment “!!! We haven’t updated for LP64 yet”.

Finally, timings for a device. This is on my iPhone4:

isEqual: 7.226365
isEqualToString: 7.436645
compare: 10.008563

So the 64-bit Mac is about nine to ten times faster than the device. What’s also interesting is that compare: is definitely the slowpoke, and isEqualToString: is a bit slower than isEqual:.

The upshot? Even with the worst performance of compare: on a device, it’s still a million comparisons per second. Performance is not a reason to choose one over the other, documentation notwithstanding.

Type Safety

The second concern is type safety. When you use isEqualToString:, you’re telling the world that you’re expecting two NSStrings. You’re informing the human that’s reading the code, and you’re informing the compiler to do as much type checking as it can. Given Objective-C’s dynamic nature, though, it can’t catch everything.

Static checking

The compiler will catch errors such as messaging the wrong explicit type:

NSString *string = @"I seem to be a verb.";
NSNumber *number = [NSNumber numberWithInt: 42];
[number isEqualToString: string];
[string isEqualToString: number];

The first comparison will generate a warning: 'NSNumber' may not respond to 'isEqualToString:'. Numbers don’t know how to compare themselves to strings.

The second comparison generates a compilergripe because NSNumber is not miscible with NSString as a type: Incompatible pointer types sending 'NSNumber *' to parameter of type 'NSString *'

I polled a number of friends on IRC and Twitter about when they’d favor isEqualToString: over isEqual: A number of very experienced developers replied “isEqualToString: will blow up if you pass it something bad.”

One of the axioms of being an effective developer is “trust, but verify.” That had been my understanding too, that if you fed isEqualToString: something bad, you’d blow up at run time because it would try to send length to an object that doesn’t understand it. That gives you a bit of extra safety – if you’re expecting a string but a get an NSNumber (or something else), then you’d like to fail early before something corrupts the user’s data.

Unfortunately, isEqualToString: only blows up if the non-string is the receiver, not if it’s the argument. “But MarkD”, you say. “The compiler will catch it if I try to pass a non-string as the argument!”. The truth is, not so much. Much of our day-to-day work with Cocoa involves stuff going into and out of containers. The container API uses ids, and there’s no guarantee of the actual type of the object coming out of your array or dictionary. Usually that’s a good thing allowing us to happily DuckType all day, but it can mask errors you might be expecting the toolkit to catch for you.

Here are two arrays. One contains a string, and the other contains a boxed number:

NSString *string = @"i can haz cheezburger?";
NSArray *stringArray = [NSArray arrayWithObject: string];
NSNumber *number = [NSNumber numberWithInt: 42];
NSArray *numberArray = [NSArray arrayWithObject: number];

Like you’d expect, using a number as the receiver results in a runtime exception even though the compiler did not (and can not) complain about the code:


[[numberArray lastObject] isEqualToString: [stringArray lastObject]];

This results in an exception:

-[__NSCFNumber isEqualToString:]: unrecognized selector sent to instance 0x141130

“Yay! We’re safe from numbers being compared to strings!”

Unfortunately, going the other way doesn’t complain.

[[stringArray lastObject] isEqualToString: [numberArray lastObject]];

That works. No compiler warning. No runtime complaint. It just returns NO. So the “safety” of catching string-to-non-string comparisons only works one direction. Granted, better than nothing, and it actually is the correct behavior (nope, these ain’t the same, pardner), but it’s not a security blanket you can depend on to catch this kind of type incompatibility.

The conclusion I draw at this point is that your choice of isEqual: and isEqualToString: is purely stylistic. If you have a lot of isEqual: calls in a method comparing other objects, go ahead and use isEqual:. If you want to tell the reader that you’re only expecting strings to be involved, use isEqualToString:. I believe that the demonstrated corner case makes any safety benefits moot in choosing which method to use.

compare:

OK, so what about compare:? It’s faster in some cases, and slower than others. When would you want to use compare: vs isEqual: or isEqualToString:? Of course, you’d use compare: when you want to see how objects (including strings) order against each other. That’s its main purpose in life. You can use compare: for string equality by looking for the NSOrderedSame return value.

isEqual: and isEqualToString: perform a “literal search”, meaning that two accented characters which are the same, but differ at the byte level (e.g. o-umlaut vs o + umlaut) are considered different. compare: will treat them as being equal.

Here are two ö’s:

NSString *oUmlaut = @"u00f6"; // o-umlaut
NSString *oPlusUmlaut = @"ou0308";  // o + combining diaeresis

And some comparisons:

NSLog (@"%@ and %@: equal: %d vs compare: %d",
       oUmlaut, oPlusUmlaut,
       [oUmlaut isEqualToString: oPlusUmlaut],
       [oUmlaut compare: oPlusUmlaut] == NSOrderedSame);

Running this shows that isEqualToString: treats them as different and compare: as the same:

ö and ö: equal: 0 vs compare: 1

So, if you’re expecting your Unicode strings to have different normalization rules you’d want to use compare: or one of its variants.

nils

The last stop on this part of the tour involves nils. What happens when nil pointers are involved? And, more importantly, what do you expect when nils are compared? Are two nil strings considered equal because the value doesn’t change from one nil to another? Or are two nil strings considered unequal, like NULL in a relational database where NULL just means “no value” and the expression NULL = NULL is always false? It’s your choice.

If you want “nil is never equal to anything else” you’ll want to use isEqual: or isEqualToString:. A nil receiver will return zero (because messages sent to nil are no-ops, and will return zero for integral types like BOOL), which is NO, which means that a nil receiver will never equal anything else. A nil as the argument will cause the equality method to return NO.

If you want “nil strings are equal” you will need to make that check yourself. At first blush, it seems like compare: would do the work for you. A nil receiver causes a zero return value, which is the same as NSOrderedSame. So this seems to work OK:

NSString *nil1 = nil;
NSString *nil2 = nil;
NSLog (@"nil and nil: equal %d vs compare: %d",
       [nil1 isEqualToString: nil2],
       [nil1 compare: nil2] == NSOrderedSame);

Which prints out when run:

nil and nil: equal 0 vs compare: 1

Yay! The test passes! Ship it! Unfortunately, because nil-sends are no-ops, the second argument is ignored. Therefore, compare: messages to nil always “equal” the argument:

NSString *notNil = @"greeble bork";
NSLog (@"nil and non-nil: equal: %d vs compare: %d",
       [nil1 isEqualToString: notNil],
       [nil1 compare: notNil] == NSOrderedSame);

Which run, prints out the same thing:

nil and non-nil: equal: 0 vs compare: 1

So, according to compare:, "greeble bork” is the same (or at least orders the same) as nil. That might not be the behavior you’re expecting.

That’s All Folks

So, you can see how an innocent little question can result in a lot of research and involve a number of subtle corner cases, and still have no definitive decision one way or the other, alas.

P.S.

There’s also an isEqualTo:, but it’s part of Cocoa scripting, so it doesn’t really belong in this discussion.

Mark Dalrymple

Author Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News