NSMutableString和NSString的类别导致绑定混淆?

时间:2009-08-07 00:51:38

标签: iphone cocoa cocoa-touch nsmutablestring

我使用类别的一些便利方法扩展了NSString和NSMutableString。这些添加的方法具有相同的名称,但具有不同的实现。例如,我已经实现了ruby“strip”函数,它删除了端点上的空格字符,但对于NSString,它返回一个新字符串,对于NSMutableString,它使用“deleteCharactersInRange”来剥离现有字符串并返回它(如红宝石条!)。

这是典型的标题:

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

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

问题在于,当我声明NSString *并运行[s strip]时,它会尝试运行NSMutableString版本并引发扩展。

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

失败了:

  

因未捕获而终止应用   例外   'NSInvalidArgumentException',原因:   '尝试改变不可变对象   使用deleteCharactersInRange:'

3 个答案:

答案 0 :(得分:4)

你被一个实现细节所困扰:一些NSString对象是NSMutableString的子类的实例,只有一个私有标志来控制对象是否可变。

这是一个测试应用程序:

#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;
}

如果您在Leopard上编译并运行它(至少),您将获得此输出:

NSCFString is a kind of NSMutableString? YES

正如我所说,该对象有一个私有标志,控制它是否可变。由于我经历了NSString而不是NSMutableString,因此该对象不可变。如果你试图改变它,就像这样:

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

你会得到(1)一个当之无愧的警告(一个人可以用演员沉默,但这不是一个坏主意)和(2)你在自己的应用中得到的同样的例外。

我建议的解决方案是将变异条包装在@try块中,并调用return [super strip]块中的NSString实现(@catch)。

另外,我不建议给方法提供不同的返回类型。我会让变异的一个返回self,就像retainautorelease一样。然后,您可以随时执行此操作:

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

不用担心unstripped是否是可变字符串。事实上,这个示例提供了一个很好的例子,您应该完全删除变异strip,或者将复制strip重命名为stringByStripping或其他内容(类似于replaceOccurrencesOfString:…和{ {1}})。

答案 1 :(得分:1)

一个例子可以使问题更容易理解:

@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];
}

在上面的代码中,将分配一个“Bar”实例,但被调用者(MyFunction中的代码)通过类型Foo引用该Bar对象,只要被调用者知道,foo实现“name”返回一个字符串。但是,由于foo实际上是bar的一个实例,因此它不会返回字符串。

大多数情况下,您无法安全地更改继承方法的返回类型或参数类型。有一些特殊的方法可以做到这一点。他们被称为covariance and contravariance。基本上,您可以将继承方法的返回类型更改为更强类型,并且可以将继承方法的参数类型更改为较弱类型。这背后的理性是每个子类必须满足其基类的接口。

因此,将“方法”的返回类型从NSString *更改为v​​oid是不合法的,将它从​​NSString *更改为NSMutableString *是合法的。

答案 2 :(得分:1)

您遇到的问题的关键在于关于Objective-C中多态性的微妙观点。由于该语言不支持方法重载,因此假定方法名称唯一地标识给定类中的方法。有一个隐含的(但很重要的)假设,即重写的方法与它覆盖的方法具有相同的语义。

在你给出的情况下,可以说两种方法的语义是相同;即,第一种方法返回用接收者内容的“剥离”版本初始化的新字符串,而第二种方法直接修改接收者的内容。这两项行动实际上并不相同。

我认为如果你仔细看看Apple如何命名其API,特别是在Foundation中,它可以真正帮助揭示一些语义上的细微差别。例如,在NSString中,有几种方法可用于创建包含接收器修改版本的新字符串,例如

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

请注意,名称是名词,第一个单词描述返回值,名称的其余部分描述参数。现在将它与NSMutableString中的相应方法进行比较,以便直接附加到接收器:

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

相比之下,这种方法是动词,因为没有要描述的返回值。所以从单独的方法名称可以清楚地看出-appendFormat:作用于接收器,而-stringByAppendingFormat:则不然,而是返回一个新的字符串。

(顺便说一下,NSString中已经有一个方法至少可以完成你想要的部分:-stringByTrimmingCharactersInSet:。你可以传递whitespaceCharacterSet作为参数来修剪前导和尾随空格。)< / p>

因此,虽然最初看起来很烦人,但我认为从长远来看,你会发现尝试模仿Apple的命名惯例是非常值得的。如果没有别的东西,它将有助于使您的代码更加自我记录,特别是对于其他Obj-C开发人员。但我认为这也有助于澄清Objective-C和Apple框架的一些语义细微之处。

此外,我同意类集群的内部细节可能令人不安,特别是因为它们对我们来说几乎是不透明的。但是,事实仍然是NSString是一个类集群,它将NSCFString用于可变实例和不可变实例。因此,当您的第二个类别添加另一个-strip方法时,它会替换第一个类别添加的-strip方法。更改一个或两个方法的名称将消除此问题。

由于NSString中已经存在一个提供相同功能的方法,可以说你可以添加可变方法。理想情况下,它的名称将与现有方法相对应,因此它将是:

- (void)trimCharactersInSet:(NSCharacterSet *)set