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有两点要注意的:

  1. 如果使用category给类增加的方法和原来类的方法同名,则原来的类方法被覆盖,且你将访问不到原来的方法。
  2. 使用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