我使用类别的一些便利方法扩展了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:'
答案 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
,就像retain
和autorelease
一样。然后,您可以随时执行此操作:
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 *更改为void是不合法的,将它从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