Categories for NSMutableString and NSString causing binding confusion?

萝らか妹 提交于 2019-12-10 22:43:03

问题


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


回答1:


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




回答2:


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




回答3:


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


来源:https://stackoverflow.com/questions/1242335/categories-for-nsmutablestring-and-nsstring-causing-binding-confusion

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!