【问题标题】:Categories for NSMutableString and NSString causing binding confusion?NSMutableString 和 NSString 的类别导致绑定混淆?
【发布时间】:2026-01-09 07:30:01
【问题描述】:

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

这是典型的标题:

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

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

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

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

失败:

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

【问题讨论】:

  • 除非 S 的值是固定的,否则你永远不应该做类似 NSLog([s strip]) 的事情。如果 s 的值为 @"%@",它会崩溃。通常不值得考虑是否固定了 s 的域,而是应该养成这样做的习惯: NSLog(@"%@", [s strip]);

标签: iphone cocoa cocoa-touch nsmutablestring


【解决方案1】:

你被一个实现细节所困扰:一些 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 块中,并在 @catch 块中调用您的 NSString 实现 (return [super strip])。

另外,我不建议为方法提供不同的返回类型。我会让变异一个返回self,就像retainautorelease 一样。然后,您总是可以这样做:

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

不用担心unstripped 是否是可变字符串。事实上,这个例子很好地说明了你应该完全删除变异的strip,或者将复制的strip重命名为stringByStripping或其他东西(类似于replaceOccurrencesOfString:…stringByReplacingOccurrencesOfString:…)。

【讨论】:

  • 这个问题很久以前(豹时代)在cocoabuilder.com/archive/message/cocoa/2004/7/8/111294 上讨论过,当你说 Leopard(至少)时,你是否暗示这个问题在 Mac OS X 的未来版本中已修复?
  • 这与 NSString 的真正实现方式无关。通过像上面那样声明两种不同的方法,不管类是如何在内部实现的,你都是在玩火。
  • 0xced:可能。我没有办法知道。另外,我没有尝试过 Tiger 或任何其他版本。我所知道的是它在 Leopard 上也是如此。
  • Mike Abdullah:同意,就像我回答的最后几段一样。
【解决方案2】:

一个例子会让这个问题更容易理解:

@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 * 是合法的。

【讨论】:

  • 哇,我什至没有注意到他给了他们不同的返回类型。当然,这是不这样做的一个很好的理由。
【解决方案3】:

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

在您给出的情况下,可以说两种方法的语义相同;即,第一种方法返回一个新字符串,该字符串用接收者内容的“剥离”版本初始化,而第二种方法直接修改接收者的内容。这两个操作真的不等价。

我认为,如果您仔细研究一下 Apple 命名其 API 的方式,尤其是在 Foundation 中,它确实有助于阐明一些语义上的细微差别。例如,在 NSString 中有几种方法可以创建一个包含修改版本的接收器的新字符串,例如

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

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

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

相比之下,这个方法是一个动词,因为没有返回值来描述。因此,仅从方法名称就可以清楚地看出 -appendFormat: 作用于接收者,而 -stringByAppendingFormat: 不作用,而是返回一个新字符串。

(顺便说一句,NSString 中已经有一个方法至少可以完成您想要的部分操作:-stringByTrimmingCharactersInSet:。您可以将whitespaceCharacterSet 作为参数传递给修剪前导和尾随空格。)

因此,虽然最初看起来很烦人,但我认为从长远来看,尝试效仿 Apple 的命名约定真的很值得。如果不出意外,它将有助于使您的代码更具自文档性,尤其是对于其他 Obj-C 开发人员。但我认为它也有助于澄清 Objective-C 和 Apple 框架的一些语义上的细微差别。

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

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

- (void)trimCharactersInSet:(NSCharacterSet *)set

【讨论】:

    最近更新 更多