NSMutableString 和 NSString 的类别导致绑定混乱?
我使用类别通过一些方便的方法扩展了 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:'
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:'
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
您被一个实现细节所困扰:一些 NSString 对象是 NSMutableString 子类的实例,只有一个私有标志控制该对象是否可变。
这是一个测试应用程序:
如果您(至少)在 Leopard 上编译并运行它,您将得到以下输出:
正如我所说,该对象有一个私有标志控制它是否可变。 由于我使用的是 NSString 而不是 NSMutableString,因此该对象是不可变的。 如果你尝试改变它,就像这样:
你会得到(1)一个当之无愧的警告(可以用强制转换来沉默,但这将是一个坏主意)和(2)你在你的自己的应用程序。
我建议的解决方案是将您的变异条包装在
@try
块中,并在@ 中调用您的 NSString 实现(
块。return [super strip]
) catch另外,我不建议为该方法提供不同的返回类型。 我会让变异返回
self
,就像retain
和autorelease
所做的那样。 然后,您始终可以这样做:而不必担心
unstripped
是否是可变字符串。 事实上,这个例子很好地说明了您应该完全删除变异的strip
,或者将复制的strip
重命名为stringByStripping
或其他名称(通过与replaceOccurrencesOfString:...
和stringByReplacingOccurrencesOfString:...
类比)。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:
If you compile and run this on Leopard (at least), you'll get this output:
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:
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
, likeretain
andautorelease
do. Then, you can always do this:without worrying about whether
unstripped
is a mutable string or not. In fact, this example makes a good case that you should remove the mutatingstrip
entirely, or rename the copyingstrip
tostringByStripping
or something (by analogy withreplaceOccurrencesOfString:…
andstringByReplacingOccurrencesOfString:…
).一个例子会让这个问题更容易理解:
在上面的代码中,将分配一个“Bar”的实例,但是被调用者(MyFunction中的代码)通过类型Foo拥有对该Bar对象的引用,只要被调用者知道,foo 实现了“name”来返回一个字符串。 但是,由于 foo 实际上是 bar 的实例,因此它不会返回字符串。
大多数时候,您无法安全地更改继承方法的返回类型或参数类型。 您可以通过一些特殊的方法来做到这一点。 它们被称为协变和逆变。 基本上,您可以将继承方法的返回类型更改为更强的类型,并且可以将继承方法的参数类型更改为较弱的类型。 其背后的原理是每个子类都必须满足其基类的接口。
因此,虽然将“方法”的返回类型从 NSString * 更改为 void 是不合法的,但将其从 NSString * 更改为 NSMutableString * 是合法的。
An example will make the problem with this easier to understand:
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 *.
您遇到的问题的关键在于 Objective-C 中多态性的一个微妙点。 由于该语言不支持方法重载,因此假定方法名称唯一标识给定类中的方法。 有一个隐含的(但重要的)假设,即重写的方法与其重写的方法具有相同的语义。
在您给出的情况下,可以说两种方法的语义不相同; 即,第一个方法返回一个用接收器内容的“剥离”版本初始化的新字符串,而第二个方法直接修改接收器的内容。 这两个操作实际上并不等同。
我认为,如果你仔细看看 Apple 命名其 API 的方式,尤其是在 Foundation 中,它确实可以帮助阐明一些语义上的细微差别。 例如,在 NSString 中,有几种方法用于创建包含接收者的修改版本的新字符串,例如
注意名称是名词,其中第一个单词描述返回值,名称的其余部分描述参数。 现在将其与 NSMutableString 中用于直接附加到接收者的相应方法进行比较:
相比之下,此方法是一个动词,因为没有返回值可描述。 因此,仅从方法名称就可以清楚地看出 -appendFormat: 对接收者起作用,而 -stringByAppendingFormat: 则不会,而是返回一个新字符串。
(顺便说一句,NSString 中已经有一个方法至少可以完成您想要的部分功能:
-stringByTrimmingCharactersInSet:
。您可以将whitespaceCharacterSet
作为参数传递给修剪前导和尾随空白。)因此,虽然一开始可能看起来很烦人,但我认为从长远来看,您会发现尝试模仿 Apple 的命名约定确实值得。 如果不出意外的话,它将有助于使您的代码更加自文档化,特别是对于其他 Obj-C 开发人员而言。 但我认为它也将有助于澄清 Objective-C 和 Apple 框架的一些语义微妙之处。
另外,我同意类簇的内部细节可能会令人不安,特别是因为它们对我们来说大多是不透明的。 然而,事实仍然是 NSString 是一个类簇,它使用 NSCFString 作为可变和不可变实例。 因此,当您的第二个类别添加另一个
-strip
方法时,它将替换第一个类别添加的-strip
方法。 更改一个或两个方法的名称将消除此问题。由于 NSString 中已经存在提供相同功能的方法,因此您可以只添加可变方法。 理想情况下,它的名称应与现有方法相对应,因此它是:
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
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:
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 passwhitespaceCharacterSet
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: