Objective-C 格致余论 2 - Category

Objective-C 中的 Category 和 Swift 中的 Extension 还是挺类似的,但是仍有一些区别,所以具体了解一下,也是有必要的。


一般来说,在 C++ 或者 Java 中,如果我们想改变一个已有类型的行为,可以继承之后重写或者添加对应的方法。不过在 Objective-C 中,可以不用继承,就直接添加新的方法或替换已有的方法。

当然,前一篇文章中提到过 class_addMethod 方法,不过还是用 category 更加规范和方便。这里需要注意,正常情况下,我们只能添加新的方法,却不能增加新的变量。

什么时候用

一般来说,继承是最正规的做法,可以很方便地重用。但是也有一些情况,继承不大容易,这时候用 category 就比较合适了,比如:

  1. Foundation 对象
  2. 用工厂模式创造的对象
  3. 单例对象
  4. 在 app 中已经使用很多次的自定对象(继承的话很容易因为漏改而出错)

下面我们分别介绍这几种情况

Foundation 对象

Foundation 中的类,比如 NSString, NSArray, NSDictionary 除了可以通过 Objective-C 的接口调用外,其实也可以用另一个 C 的接口调用。例如 NSString 会对应到 Core Foundation 中的 CFStringRef

所以在实际生成的时候,其实具体的类别是难以确定的,也就是说,我们继承了一个 NSString 类创建了一个 MyString, 新建实例的时候,其实并不能保证新建的就是一个 MyString,所以这种情况下,不是很适合用继承,而是 category 直接进行拓展比较合适。

工厂模式对象

工厂模式本身的机制就是,不用关心具体的子类,只要传入所需要的类型,会自动生成我们需要的类。

就拿 UIButton 来说,即使我们继承了,在具体生成的时候,也不能保证得到我们继承后的子类,从这个角度来看,和上一个情境是类似的。

或者说,我们想要改变一个父类的行为,让所有的子类都增加新的方法,当时实际上我们没办法改动这个父类的时候,就应该采用 category。

这种因为具体生成什么类不确定的情况,会导致很多奇奇怪怪的问题,一定要小心使用,或者直接用 Swift,真心的。

单例对象

这也是非常出名的设计模式了,比如 UIApplication, NSUserDefault, NSNotificationCenter 等都是这种设计。因为单例实现机制的问题,使其本身很难被继承,我们先来看看如何声明一个单例。

@interface MyClass : NSObject
+ (MyClass *)sharedInstance;
@end
// 实现部分
static MyClass *sharedInstance = nil;
@implementation MyClass
+ (MyClass *)sharedInstance
{
return sharedInstance ? sharedInstance : (sharedInstance = [[MyClass alloc] init]);
}
@end

(注意,其实一般 Singleton 会使用 GCD 的 dispatch_once 实现,不过暂时为了理解简单,先用上面的写法)

假如我们继承 MyClass 之后却没有重写覆盖 sharedInstance 方法,首先可能还是会返回原来的对象,另外如果我们覆盖的话,那么可能就会漏过放在原来的实现中的一些操作(因为我们可能看不到源代码),产生与预期不符的结果。

Category 的写法

语法还是很简单的,只要记住括号里是 category 的名称即可,我们用给 NSString 添加一个 strokeCompare: 为例:

@interface NSString (CustomCompare)
- (NSComparisonResult)strokeCompare:(NSString *)anotherString;
@end
@implementation NSString (CustomCompare)
- (NSComparisonResult)strokeCompare:(NSString *)anotherString
{
NSLocale *strokeSortingLocale = [[[NSLocale alloc]
initWithLocaleIdentifier:@"zh@collation=stroke"]
autorelease];
return [self compare:anotherString
options:0
range:NSMakeRange(0, [self length])
locale:strokeSortingLocale];
}

惯例的命名方式是 NSString+CustomCompare.hNSString+CustomCompare.m

除了添加新方法,以下情况也很适合用 category

  • 将一个很大的类切分成几个部分,代码组织更清晰,也更易于跨平台
  • 替换原来的实现,实现方法的重写,但是这种方式比较危险,推荐不要使用

Extensions

另一个类似的设计是 extensions,可以认为 extensions 是一个没有名字的 category,在 extensions 中定义的方法,需要放在原本的类的实现中,是类名后面跟一对空括号,下面是一个例子:

@interface MyClass : NSObject
@end
@interface MyClass()
- (void)doSomething;
@end
@implementation MyClass
- (void)doSomething
{
}
@end

具体有什么用呢,比方说可以:

  • 拆分 header
  • 管理私有方法

Swift 中的做法就更加简单粗暴,直接用类名,加上不同的关键字即可,如:

class MyClass{
}
extension MyClass {
}

在 Swift 中,extension 也可以用来扩展 protocol 和 struct,不过这里暂时先不展开。

增加变量与属性

虽然前面提到不能增加变量或者属性,但是其实还是有办法的,既然我们是用 virtual table 来记录相关的方法,同样可以用另一个表格来记录相关的变量和方法嘛,这就是 Associated Objects。具体的使用方式如下:

另外,虽然 category 不能增加成员变量,但是 extension 可以,甚至也可以直接在 @implementation 中加入。

参考资料

  1. KKBOX iOS/Mac OS X 基本开发教材
  2. Customizing Existing Classes
  3. Category
  4. Associated Objects
  5. Objective-C Associated Objects
  6. Objective-C Runtime Reference
捧个钱场?