Blog

Maecenas sed diam eget risus varius blandit sit amet non magna. Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Curabitur blandit tempus porttitor. Sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.

On The Perils of isEmpty (or isBlank) in Objective-C

I recently fixed a rather pernicious and pervasive crashing bug in an app I’ve been contributing to. The bug appears to have resulted from a misunderstanding of some relatively subtle behavior in Objective-C. This post explores the problem as well as a solution.

Here is the problematic code:


@implementation NSString (Additions)
- (BOOL)isEmpty {
    if (self == nil)
        return YES;
    if (self == (NSString*)[NSNull null])
        return YES;
    if ([self isEqualToString:@""])
        return YES;
    if (self.length == 0)
        return YES;
    return NO;
}
@end

Can you spot the problems? There are several. I will address them in order of increasing severity.

Whitespace

The least worrisome potential problem is the third if statement. This may even be correct in many circumstances, although it is redundant with the fourth if statement in these circumstances. In such circumstances the redundancy can be overlooked without any detrimental impact on behavior of the app.

It is incorrect, however, any time a string that only contains whitespace should be treated as empty. In those circumstances stringByTrimmingCharactersInSet: should be used with the domain appropriate character set before checking for a length of 0. It should not be necessary to call isEqualToString: under any circumstance.

NSNull

The second if statement is somewhat more troublesome. It is superfluous. A category method on NSString will never be called on an instance of NSNull. Code that cannot be called has no reason to exist. Yet the fact that it does exist and can never be correct should raise a red flag. It is a sign that the author may not understand how the Objective-C language works.

My best guess is that the author expected this code to be invoked when NSNull is returned from a collection containing instances of NSString. In reality, if a collection returns an instance of NSNull and an attempt is made to call isEmpty on that instance the app will crash due to an unrecognized selector (in the absence of an isEmpty category method on NSObject or NSNull). The app needs to be audited to ensure it is correctly checking for NSNull where necessary and is not relying on the NSString category method to do that.

One way to address this problem would be to simply add a category to NSNull with the appropriate isEmpty method like this:


@implementation NSNull (Additions)
- (BOOL)isEmpty {
    return YES;
}
@end

This method will allow us to safely call isEmpty on objects returned from a collection that contains strings and uses NSNull used as a nil placeholder.

nil

Unfortunately, in the course of fixing the problem of NSNull we have propagated the more subtle and most severe issue demonstrated in the first if statement of the original category method. This line of code is also superfluous and also indicates that the author may not understand some of the more subtle aspects of Objective-C very well.

The issue with this line of code is that it performs a nil-check on self. Since it is perfectly legal to send a message to nil in Objective-C it is not totally unreasonable to think a method might be called when self is nil. If that happened this if statement would be correct.

In reality, when you send a message to nil the message send is short-circuited and nil, NO, 0, or a struct where members are initialized with these values is returned. No message send actually happens. In fact, no message send can happen as nil pointers do not actually point to an object with an isa Class pointer. Without an isa pointer the Objective-C runtime does not know which implementation of the selector to use.

When the isEmpty message is sent to a nil NSString pointer NO will automatically be returned. This is bad. The original code was expecting YES to be returned when isEmpty is sent to nil.

Because NO is returned when isEmpty is sent to nil, nil values were slipping past it. The failed nil-check resulted in various issues including several crashes. In a real-world sized code base, this issue was made even more subtle by the fact that in most places, isEmpty was preceded by a manual nil-check, therefore avoiding the problematic isEmpty method.

What we have just discovered is that the intuitive semantics of isEmpty do not play well with the nil messaging behavior of Objective-C.1 The purpose of the method is to indicate whether an object has meaningful content or not. We are getting tripped up because isEmpty does this by checking for the absence of content.

The Solution

The solution is rather simple. We check for presence of content rather than absence of content. If we happen to send a message to nil, NO will be returned just as one would expect. The check for presence of content does not have a semantic conflict with the behavior of nil-messaging.

This method can be named either hasContent or isNotEmpty. It is also possible to implement both (each name reads more clearly than the other in some calling contexts). I recommend implementing hasContent as the primary method. It avoids the mental gymnastics of double-negatives that can result with isNotEmpty (i.e. ![aString isNotEmpty]). If you find you have call sites that read substantially more clearly when isNotEmpty is used you can add that alias.

The simple version (assuming any characters are meaningful) looks like this:


@implementation NSString (Additions)
- (BOOL)hasContent {
    return self.length > 0;
}
@end

If we want to ignore whitespace, the method would change to look like this:


@implementation NSString (Additions)
- (BOOL)hasContent {
    NSString *trimmed =
        [self stringByTrimmingCharactersInSet: 
            [NSCharacterSet whitespaceAndNewlineCharacterSet]
        ];
    return trimmed.length > 0;
}
@end

Now we can add the negative variant:


@implementation NSString (Additions)
- (BOOL)isNotEmpty {
    return [self hasContent];
}
@end

I will leave it as an exercise for the reader to create similar categories for NSNull, NSArray, NSDictionary, and any other classes you may wish.


  1. If the semantics of nil-messaging are not clear to you, please refer to the section Working with Objects in Apple’s guide Programming with Objective-C.