objc’s category and class cluster(类簇)

来源:互联网 发布:改不了淘宝店名 编辑:程序博客网 时间:2024/05/17 08:51

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
得到运行结果:

2013-08-18 21:24:03.605 test[271:903] str's length is : 42013-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”来实现这种扩展,如果你没有非要继承这种“类簇”类的理由的话。


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 房子借款合同和抵押合同丢了怎么办 离婚判决不服上诉期限过了怎么办 法院判完执行厅不执行怎么办 民事判决书下来又有新的证据怎么办 民事判决书下来欠钱没钱法院怎么办 法院笔录没看清就签字了怎么办 有人模仿自己笔迹在法院签字怎么办 房屋未签合同中介定金不退怎么办 临时摊位买的东西想退怎么办 租房合同签了房东不想租了怎么办 租房合同签了不想租了怎么办 租房合同刚签一天不想租怎么办 买二手房房东不交物业费怎么办 买二手房原房主欠物业费怎么办 客人将物品遗留在餐厅时怎么办 工程签合同了又别人来干怎么办? 超市储存柜东西忘拿了怎么办 普通异性朋友出去玩在外面住怎么办 古墓丽影崛起绳箭突然没有了怎么办 pp材质的水杯用开水烫了怎么办 手上长水泡很痒怎么办有什么药膏 烧伤后水泡破了周围都肿了怎么办 烧伤的水泡破了碰到水怎么办 浓硫酸沾到皮肤上应该怎么办 将浓硫酸沾到皮肤上怎么办 刷厕所的盐酸弄到皮肤上怎么办 死刑犯被执行后发现被冤枉的怎么办 觉得老公对自己不够关心体贴怎么办 孕期老公不知道关心不体贴怎么办? 中国和伊朗做贸易美国制裁怎么办 土地被村民霸占村长解决不了怎么办 村支书霸占群众选举村长的票怎么办 苹果手机自带的音乐软件删了怎么办 不小心把手机系统软件删了怎么办 佳能打印机打相片是打不全怎么办 卖家说我寄回去的包是假的怎么办 辞职后原单位没把档案给转出怎么办 天下行以租代购要起诉我怎么办 顺丰收件人电话地址都写错了怎么办 领导问任务完不成怎么办怎么回答 我和我老婆感情出现问题了怎么办