objective-c有一个feature,可以给已有的类添加方法,而无需改变类名。传统的语言可能需要通过继承或者组合实现,但是obj-c只需要用这个feature就好,这就是category。
Category:
举个例子,NSString是一个常用的类,NSString是原生支持unicode,比如NSString* str = @”感谢国家”; 要获得string的length,在大部分语言中获得的是字节数(比如python),如果文字编码是utf-8,那么得到的是12(4*3)。但是 NSString是原生支持unicode,所以当使用str.length时,获得的长度是4。
有这一特性很好,但这里不是讨论的重点,假设我们需要给NSString增加一个获得字节长度的方法,假设方法名为:byteLengthWithEncoding,使用category可以给NSString类增加如下代码:
@interface NSString (StringLength) - (NSUInteger) byteLengthWithEncoding:(NSStringEncoding)encoding; @end @implementation NSString (StringLength) - (NSUInteger) byteLengthWithEncoding:(NSStringEncoding)encoding { if (self == nil) { return 0; } const char * byte = [self cStringUsingEncoding:encoding]; return strlen (byte); } @end |
开始表明我们要扩展的类是NSString,并且把这个方法归到(StringLength)的分组中。这个category组名称随便你命名。然后增加了一个方法名:byteLengthWithEncoding;
接下来在implementation中实现这个方法,我们使用了NSString原生的方法cStringUsingEncoding,获得char*的指针,然后使用c里面的函数strlen来获得字节数。
来测试一下:
int main( int argc, char * argv[]){ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSString* str = @ "感谢国家" ; NSLog(@ "str's length is : %d" , [str length]); NSLog(@ "str's byte length is: %d" , [str byteLengthWithEncoding:NSUTF8StringEncoding]); [pool drain]; return 0; } |
编译
gcc -framework Foundation main.m -o test
得到运行结果:
2010-08-18 21:24:03.605 test[271:903] str's length is : 4 2010-08-18 21:24:03.608 test[271:903] str's byte length is: 12
一切都如预期运行,可以感受到category的威力了吧~
category用于给一个类增加类方法如此好用,但是对于category有两点要注意的:
- 如果使用category给类增加的方法和原来类的方法同名,则原来的类方法被覆盖,且你将访问不到原来的方法。
- 使用category只能增加类方法,不能增加类变量(ivar)
对于第二点,如果我们要增加类方法,同时也要增加类变量,该怎么办?嗯,你可能想到了使用类继承。好,那就来写个类继承NSString,不过我们先不增加类变量,然后给这个类增加上面的那个类方法byteLengthWithEncoding,我们的实现大概是这样:
@interface NSStringWithByteLength: NSString { } - (NSUInteger) byteLengthWithEncoding:(NSStringEncoding)encoding; @end @implementation NSStringWithByteLength - (NSUInteger) byteLengthWithEncoding:(NSStringEncoding)encoding { if (self == nil) { return 0; } const char * byte = [self cStringUsingEncoding:encoding]; return strlen (byte); } @end int main( int argc, char * argv[]){ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSStringWithByteLength* str = (NSStringWithByteLength*)[NSString stringWithString:@ "感谢国家" ]; NSLog(@ "str's length is : %d" , [str length]); NSLog(@ "str's byte length is: %d" , [str byteLengthWithEncoding:NSUTF8StringEncoding]); [pool drain]; return 0; } |
代码有问题的是main函数的第二行,我们强行将返回NSString*的指针转成NSStringWithByteLength类指针,这样应该在调用
类方法byteLengthWithEncoding时出问题。urh,先不管它,gcc编译,输出,运行到输出[str
length]时,运行正常,输出4;下一行,调用byteLengthWithEncoding时果然crash了,看出错提示:
-[NSCFString byteLengthWithEncoding:]: unrecognized selector sent to instance 0x100001058
说的是NSCFString类没有byteLengthWithEncoding方法,wait a minute,哪里来的NSCFString这个类?
Class Cluster:
原因在于NSString是个class cluster,一个类簇。什么是一个类簇?简单的来说,NSString是个“工厂类”,然后它在外层提供了很多方法接口,但是这些方法的实现是由具体 的内部类来实现的。当使用NSString生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会生成这个具体 的类对象返回给你。这种又外层类提供统一抽象的接口,然后具体实现让隐藏的,具体的内部类来实现,在设计模式中称为“抽象工厂”模式。
这里有一篇老外写的,更详细的介绍objective-c class cluster的文章。
回过头来看上面的代码,实际上在使用[NSString stringWithString:]的方法时,返回的就是NSCFString* 这个具体类的指针,当然这个类没有后面我们指定的类方法 byteLengthWithEncoding,自然调用时也就出错了。
其实即使不是返回NSCFString的指针,上面的代码也有问题,假设是返回NSString的指针,直接使用
(NSStringByteWithLength)去进行强制转换也有问题,毕竟NSStringByteWithLength是子类。这样一来,可能想
到正确的写法应该是将第main函数的第2行初始对象是换为:
NSString* str = [[NSStringWithByteLength alloc] initWithBytes: "感谢国家" length:12 encoding:NSUTF8StringEncoding]; |
这里我们换了一个初始化string的方法,用了NSString原有的initWithBytes的方法。因为
NSStringWithByteLength继承了NSString,即使NSString是个类簇,由于我们没有在
NSStringWithByteLength里重写alloc和init的方法,这么接下来的alloc和initWithBytes的方法调用应该都
是还是NSString里的方法吧,按我们之前关于类簇的分析,这里返回的可能仍然是NSCFString*的类型,然后我们将返回值赋给
NSString,由于NSString*是NSCFString的父类,这种赋值应该没问题。ok,接下来就make & run 。
又crash了~ 看报错的信息:
-[NSStringWithByteLength initWithBytes:length:encoding:]: unrecognized selector sent to instance 0x10010c980
好像是说NSStringWithByteLength没有initWithBytes的方法调用。没错,我们是没有在 NSStringWithByteLength中定义这个方法,但是按我们之前的期望,第一步[NSStringWithByteLength alloc] 这里应该调用的是NSString的alloc,然后返回一个对象后再次调用NSString的initWithBytes方法,看起来和直接使用 [[NSString alloc] initWithBytes: length: encoding] 没什么区别啊,为什么这里就说没有initWithBytes这方法了呢?为什么[[NSString alloc] initWithBytes:length:encoding] 调用时没有问题,而用我们自己的 NSStringWithByteLength的派生类调用就出了问题呢?
其实又是NSString这个类簇在底下搞鬼,把[[NSString alloc] initWithBytes:length:encoding]拆开看,相当于:
id someClass = [NSString alloc]; [someClass initWithBytes:length:encoding]; |
先看第一行,我们把这someClass的class打出来看看:[someClass
className],又出来个新玩意:NSPlaceholderString。先不管它,不过这里我们至少知道了,这里alloc返回的一个
NSPlaceholderString类型的指针。
然后我们再看,把第一行改为:
id someClass = [NSStringWithByteLength alloc] |
按照我们之前的分析,由于这里仍然调用的是NSString的alloc方法,那么返回的someClass的className应该仍然是
NSPlaceholderString才对,但是把这个打印出来,返回的居然是:NSStringWithByteLength,没错,还是我们自己的
类的指针。
这是怎么回事?如果是在alloc这一步已经返回不同的类型指针的话,那么刚才的报错提示没有 initWithBytes:length:encoding的方法的提示就不难理解了,因为NSPlaceholderString这个类里定义了这个 方法,而我们自己的NSStringWithByteLength的类没有这个方法。但是同样调用的是NSString的alloc,为啥两次返回不同 呢?
Under the hood
接下来就是见证奇迹的时刻,NSString alloc时有个中间层,就是我们上面看到的NSPlaceholderString,alloc的对象先统一为这个类对象之后,在后面调用 NSPlaceholderString的类方法时,比如initWithBytes:length:encoding 才返回具体的类,即在NSPlaceholderString这一层做个“代理工厂”,根据调用的不同init方法再返回具体的类,比如 NSCFString。
那么为什么我们自己的类调用alloc时,就不返回NSPlaceholderString这个类对象了呢?关键就在于NSString
alloc方法的实现。NSString的alloc方法实现类似这样(这里只写简单的逻辑,Cocoa实际的代码实现未必和这个相同,不过逻辑应该是类
似的):
@ class NSPlaceholderString; @interface NSString:(NSObject) + (id) alloc; @ end @implementation NSString +(id) alloc { if ([self isEquals:[NSString class ]]) { return [NSPlaceholderString alloc]; } else return [super alloc]; } @end @interface NSPlaceholderString:(NSString) @end |
关键就在于alloc的实现,可以发现,当只用NSString调用alloc的时候,由于self == [NSString
class],所以这时返回的是NSPlaceholderString的类对象;而使用其他类(比如派生类)调用alloc时,返回的是super的
alloc,这里也就是[NSObject
alloc],而NSObject的alloc方法返回的是调用类的类对象,所以在我们用我们自己的NSStringWithByteLength类调用
时,返回的就是NSStringWithByteLength类的类对象了。
综述
只扩展类方法的时候,Category已经足够好用了,而上面也解释了Class Cluster和NSString alloc的“怪异”实现。可见继承一个“class cluster”类型的类是多么不容易,如果不熟悉,可能处处是陷阱。所以在有的书上就提出这样的建议:最好不要继承NSString这样的“类簇”类, 同样的还有NSArray,NSDictionary,NSNumber等等。在apple的文档中也提到,建议使用“组合”或者“catogery”来 实现这种扩展,如果你没有非要继承这种“类簇”类的理由的话。
转自: http://web2.0coder.com