如何从类集群上的类别方法返回instancetype对象

时间:2015-11-18 18:01:39

标签: objective-c

我想在NSString上编写一个尊重当前正在使用的子类的类别(因此调用NSMutableString内部的函数应该返回NSMutableString的实例,等等:< / p>

@interface NSString (HashTagUtilities)
- (instancetype *)stringWithHashTag;
@end

此类别的实施是

- (instancetype)stringWithHashTag {
  return [self.class stringWithFormat:@"#%@", self];
}

但是,因为NSString是一个类集群,所以调用此方法实际上会崩溃:

  

***因未捕获的异常而终止应用   NSInvalidArgumentException&#39;,原因:&#39; ***初始化方法   -initWithFormat:locale:arguments:无法发送到类__NSCFString的抽象对象:创建一个具体的实例!&#39;

我的问题是,有没有办法编写这样的类别 - 有一个方法可以返回正在扩展的超类的所有当前和未来子类的正确子类的实例?

3 个答案:

答案 0 :(得分:2)

正如@Avi指出的那样,简短的回答似乎为否,Mike Ash在这里做了:https://mikeash.com/pyblog/friday-qa-2010-03-12-subclassing-class-clusters.html

以下是摘录:

  

该技术只是将类别添加到集群类而不是对其进行子类化。人们通常只是简单地添加新方法,而不是修改现有功能。在Objective-C中,您可以在类别中添加新方法:

@interface NSArray (FirstObjectAdditions)
- (id)my_firstObject;
@end

@implementation NSArray (FirstObjectAdditions)
- (id)my_firstObject {
    return [self count] ? [self objectAtIndex: 0] : nil;
}
@end

因此,根据Ash的说法,它应该可行,并且在对类集群进行子类化时没有问题。但它不起作用!

原因如下:

作为Apple的doc(在这里,在NSData上,但对任何类集群都是如此)注意到,

  

...对象不是NSData或NSMutableData类的实际实例,而是是其私有子类之一的实例

问题在于您返回实例类型。 instancetype是编译器的提示,返回类型是声明方法的类。编译器有点傻,只需要NSString作为stringWithFormat的接收者。它不知道,在运行时,该类不是NSString而是其私有子类之一

在您的情况下,__NSCFString无法回答initWithFormat方法(stringWithFormat似乎归结为initWithFormat方法,正如错误所指出的那样)。对于更明显的情况,请使用__NSCFConstantString,这是任何@"string"声明的类型。因为它在你使用字符串之前由ObjC运行时实例化并且完全不可变(你不能初始化,dealloc或修改它),所以它没有init类型的方法,所以调用initWithFormat失败,因此对stringWithFormat的调用失败,因此您无法调用[@"hashtag" stringWithHashTag],因此您的代码将在中失败,至少一个案例中没有任何先前的警告,所以它注定要失败。

因此,问题的核心在于self.class:您无法确定接收器类型,或者它是否能接听您的呼叫。我的建议是:

@interface NSString (HashTagUtilities)
- (instancetype)stringWithHashTag; //NOTE that there is no need for a * after instancetype - it's like id, it's already one
@end
@implementation NSString (HashTagUtilities)
- (instancetype)stringWithHashTag {
    return [NSString stringWithFormat:@"#%@", self];
}
@end

@interface NSMutableString (HashTagUtilities)
- (instancetype)stringWithHashTag;
@end
@implementation NSMutableString (HashTagUtilities)
- (instancetype)stringWithHashTag {
    return [NSMutableString stringWithFormat:@"#%@", self];
}
@end

我有两个类别:一个用于NSMutableString,另一个用于NSString。 NSString一个总是返回一个动态创建的NSString,无论它实际上变成什么私有子类,所以我相信它不会失败。 最后,NSMutableString使用其类的可变特性,因此我不必实例化另一个字符串这不起作用,因为 __NSCFConstantString实际上是一个子类NSMutableString 的。有多奇怪所以,我只是实例化一个新字符串。

编辑:正如@RobNapier非常正确地指出

  

我们应该避免围绕它们[类集群]的任何聪明。没有办法知道你是否测试了所有角落的情况。还有其他私有字符串类吗?也许。 iOS 10会增加更多吗?也许。 小心踩

真。 仔细踩,这些都不是容易的野兽,就像我们刚刚看到的一样。

答案 1 :(得分:1)

简短的回答似乎是:不。

我实际上希望你的代码可以工作,因为self.class应该返回具体的类类型。然而,你的间接证据指出NSString操纵其内部阶级,以至于他们对自己的内容撒谎。

答案 2 :(得分:1)

我认为@PercevalFARAMAZ涵盖了要点,但我想提出并扩展他的一个笔记。

字符串@"x"属于__NSCFConstantString类。那么你的代码就是(如果它按照你要求的方式工作):

[__NSCFConstantString stringWithFormat:@"#%@", self];

这是无稽之谈,因为常量字符串被编译成二进制文件。这个字符串会有错误的内存管理。类似地,如果您的字符串很短(并且您位于64位平台上),它可能存储在标记指针中。

[NSString stringWithFormat:@"%@", @"short"] // NSTaggedPointerString

但这只有在字符串足够小以适合标记指针时才有效(NSString在选择正确的子类时在运行时确定)。如果你的哈希值太长,那么再次,这将无法实现,你就会崩溃。没有承诺可以使用任意字符串初始化内部私有类型,因此您的基本方法将非常脆弱。

由于Core Foundation桥接,它变得更加怪异。实际的类可以是带有自定义分配器的CFStringRef。获取该分配器甚至可能可能(析构函数存储在结构中,但不存储在分配器中)。所以你再次陷入困境。当你进入类集群时,只有大量的极端情况。