views:

683

answers:

4

Hi,

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

+3  A: 

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

Peter Hosey
This problem was discussed ages ago (Panther days) on http://www.cocoabuilder.com/archive/message/cocoa/2004/7/8/111294When you say Leopard (at least), do you imply that this is fixed in a future version of Mac OS X?
0xced
This has nothing to do with how NSString is implemented really. By declaring two different methods as above, you are playing with fire regardless of how the class is implemented internally.
Mike Abdullah
0xced: Could be. I have no way of knowing. Also, I didn't try Tiger or any other versions. All I know is that it works that way on Leopard.
Peter Hosey
Mike Abdullah: Agreed, as in the last few paragraphs of my answer.
Peter Hosey
+1  A: 

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 *.

Jon Hess
Whoa, I didn't even notice that he gave them different return types. That, of course, is a good reason not to do that.
Peter Hosey
A: 

Thanks for the various responses. Point well taken that it's a bad idea to change the return type of an over-ridden inherited method. So I change the second declaration to:

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

I still have the core problem that I described in the initial post: I read that NSMutableString inherits from NSString. Yet, a message sent to a class (NSString) is actually implemented by a subclass method (NSMutableString).

This would occur if NSString and NSMutableString were "proper" objects. For example, in ruby:

class Foo
   def meth
     puts "Called meth on Foo"
   end
end

class Bar < Foo
   def meth
     puts "Called meth on Bar"
   end
end

foo = Foo.new
foo.meth

bar = Bar.new
bar.meth

returns

Called meth on Foo
Called meth on Bar

As one would expect.

I'm very confused that I need to care about the implementation of these so-called "class clustered" classes if I want to treat them as objects. To me, it obviates one of the core advantages of OO.

Or am I missing something?

Thanks.

LeftHem
+1  A: 

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 not 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
jlehr