78408

Categories for NSMutableString and NSString causing binding confusion?

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:'

Answer1:

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:…).

Answer2:

An example will make the problem with this easier to understand:

@interface Foo : NSObject { } - (NSString *)method; @end @interface Bar : Foo { } - (void)method; @end void MyFunction(void) { Foo *foo = [[[Bar alloc] init] autorelease]; NSString *string = [foo method]; }

In the above code, an instance of "Bar" will be allocated, but the callee (the code in MyFunction) has a reference to that Bar object through type Foo, as far as the callee knows, foo implements "name" to return a string. However, since foo is actually an instance of bar, it won't return a string.

Most of the time, you can't safely change the return type or the argument types of a method that's inherited. There are some special ways in which you can do it. They're called covariance and contravariance. Basically, you can change the return type of an inherited method to a stronger type, and you can change the argument types of an inherited method to a weaker type. The rational behind this is that every subclass must satisfy the interface of its base class.

So while it's not legal to change the return type of "method" from NSString * to void, it would be legal to change it from NSString * to NSMutableString *.

Answer3:

The key to the problem you've run into rests on a subtle point about polymorphism in Objective-C. Because the language doesn't support method overloading, a method name is assumed to uniquely identify a method within a given class. There's an implicit (but important) assumption that an overridden method has the same semantics as the method it overrides.

In the case you've given, arguably the semantics of two methods are <strong>not</strong> the same; i.e., the first method returns a new string initialized with a 'stripped' version of the receiver's contents whereas the second method modifies the content of the receiver directly. Those two operations really aren't equivalent.

I think if you take a closer look at the way Apple names its APIs, especially in Foundation, it can really help shed light on some semantic nuances. For example, in NSString there are several methods for creating a new string containing a modified version of the receiver, such as

- (NSString *)stringByAppendingFormat:(NSString *)format ...;

Note that the name is a noun, where the first word describes the return value, and the rest of the name describes the argument. Now compare this to the corresponding method in NSMutableString for appending directly to the receiver:

- (void)appendFormat:(NSString *)format ...;

By contrast, this method is a verb because there's no return value to describe. So its clear from the method name alone that -appendFormat: acts upon the receiver, whereas -stringByAppendingFormat: does not, and instead returns a new string.

(By the way, there's already a method in NSString that does at least part of what you want: -stringByTrimmingCharactersInSet:. You can pass whitespaceCharacterSet as the argument to trim leading and trailing whitespace.)

So while it may seem annoying initially, I think you'll find it really worthwhile in the long run to try to emulate Apple's naming conventions. If nothing else it'll help make your code more self-documenting, especially for other Obj-C developers. But I think it'll also help clarify some semantic subtleties of Objective-C and Apple's frameworks.

Also, I agree that the internal details of class clusters can be disconcerting, especially since they're mostly opaque to us. However, the fact remains that NSString is a class cluster that uses NSCFString for both mutable and immutable instances. So when your second category adds another -strip method, it replaces the -strip method added by the first category. Changing the name of one or both methods will eliminate this problem.

And since a method already exists in NSString that provides the same functionality, arguably you could just add the mutable method. Ideally its name would correspond with the existing method, so it would be:

- (void)trimCharactersInSet:(NSCharacterSet *)set

Recommend

  • convert const char* to NSString * and convert back - _NSAutoreleaseNoPool()
  • Performing selectors on main thread with NSInvocation
  • Application bundle Mac OS X Leopard
  • Size of store buffers on Intel hardware? What exactly is a store buffer?
  • Enable/Disable Wifi on non-jailbroken iOS device
  • OpenGL ES as a 2D Platform
  • ASM at&t syntax
  • Energy efficiency in unity3d - halting app waiting for user interaction
  • What is wrong with this _popen / select example?
  • “No provisioned iPhone OS device is connected” after update to 4.0
  • FastFormat on OS X
  • Passing multidimensional array in c90 (pre VLA-style)
  • Finder Plugin in Snow Leopard
  • How to have a QTextBrowser to display contents of a QTextEdit?
  • SPARQL queries doesn't works without LIMIT clause
  • Get the negative of an Optional Chain
  • mail server requires authentication when attempting to send to a non-local e-mail address when using
  • Is there a equivalent to JSON.Net in Java? [duplicate]
  • MarkLogic Node.js Sort on “last-modified”
  • Is it safe to drop the -webkit vendor prefix from the css3 border-radius yet?
  • A class implementing two different IObservables?
  • can variables be set randomly when declaring them again?
  • Suqueries in select clause with JPA
  • How do I get the list of bad records that didn't load in Bigquery?
  • Cloud Code function running twice
  • WPF version of .ScaleControl?
  • NUnit 3.0 TestCase const custom object arguments
  • Default parameter as generic type
  • bad substitution shell- trying to use variable as name of array
  • Assign variable to the value in HTML
  • Groovy: Unexpected token “:”
  • Bad request using file_get_contents for PUT request in PHP
  • Read text file and split every line in MSBuild
  • Deserializing XML into class C#
  • Function pointer “assignment from incompatible pointer type” only when using vararg ellipsis
  • Android Studio and gradle
  • SQL merge duplicate rows and join values that are different
  • Qt: Run a script BEFORE make
  • python draw pie shapes with colour filled
  • How to Embed XSL into XML