R – Categories for NSMutableString and NSString causing binding confusion

cocoacocoa-touchiphonensmutablestring

I have extended both NSString and NSMutableString with some convenience methods using categories. These added methods have the same name, but have different implementations. For e.g., I have implemented the ruby "strip" function that removes space characters at the endpoints for both but for NSString it returns a new string, and for NSMutableString it uses the "deleteCharactersInRange" to strip the existing string and return it (like the ruby strip!).

Here's the typical header:

@interface NSString (Extensions)
-(NSString *)strip;
@end

and

@interface NSMutableString (Extensions)
-(void)strip;
@end

The problem is that when I declare NSString *s and run [s strip], it tries to run the NSMutableString version and raises an extension.

NSString *s = @"   This is a simple string    ";
NSLog([s strip]);

fails with:

Terminating app due to uncaught
exception
'NSInvalidArgumentException', reason:
'Attempt to mutate immutable object
with deleteCharactersInRange:'

Best Answer

You've been bitten by an implementation detail: Some NSString objects are instances of a subclass of NSMutableString, with only a private flag controlling whether the object is mutable or not.

Here's a test app:

#import <Foundation/Foundation.h>

int main(int argc, char **argv) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSString *str = [NSString stringWithUTF8String:"Test string"];
    NSLog(@"%@ is a kind of NSMutableString? %@", [str class], [str isKindOfClass:[NSMutableString class]] ? @"YES" : @"NO");

    [pool drain];
    return EXIT_SUCCESS;
}

If you compile and run this on Leopard (at least), you'll get this output:

NSCFString is a kind of NSMutableString? YES

As I said, the object has a private flag controlling whether it's mutable or not. Since I went through NSString and not NSMutableString, this object is not mutable. If you try to mutate it, like this:

NSMutableString *mstr = str;
[mstr appendString:@" is mutable!"];

you'll get (1) a well-deserved warning (which one could silence with a cast, but that would be a bad idea) and (2) the same exception you got in your own application.

The solution I suggest is to wrap your mutating strip in a @try block, and call up to your NSString implementation (return [super strip]) in the @catch block.

Also, I wouldn't recommend giving the method different return types. I would make the mutating one return self, like retain and autorelease do. Then, you can always do this:

NSString *unstripped = …;
NSString *stripped = [unstripped strip];

without worrying about whether unstripped is a mutable string or not. In fact, this example makes a good case that you should remove the mutating strip entirely, or rename the copying strip to stringByStripping or something (by analogy with replaceOccurrencesOfString:… and stringByReplacingOccurrencesOfString:…).

Related Topic