Category in Objective-C

Category in Objective-C

OOP 的精神之一,就是如果你想研發一台新款式的車子,你並不需要重新發明輪子,通常的做法會去繼承某個現有的”車子類別”,然後加上你要的功能跟屬性,改一改就變成一款新的車子可以來騙錢了。

不過有時候你想幫原來的類別加功能,但又不想動到原來的程式碼,例如你可能下載了某款功能超強的 2D 物理引擎程式碼,但因為某些小地方寫的不合你的需求,於是你便動手改原始碼來加功能。這當然沒問題,但萬一原作出新的版本,你要不就選擇維持自己原來的版本不 update,不然就是 update 之後,你原來加在舊版本的程式碼得再重貼一次到新版。

Objective-C 裡有個叫做 category 的東西可以幫你在現有的類別加上新功能,這樣一來上面這個問題就可以搞定了。跟別的程式語言比較起來,category 的觀念有點像是在 Ruby 的 mixin 或是 Python 的 open class,都是在不影響或修改原來的類別或模組的情況下去修改原有的功能。

舉個例子,因為 NSObject 是所有物件的源頭,但我想要加一個方法讓所有的子類別都可用(把要加的功能放在繼承階層的最源頭並不是好的設計,在這裡只是舉個例子而已)。程式碼這樣寫:

// interface
@interface NSObject(MySuperObject)
-(void) printRetainCount;
+(void) sayHello;
@end

// implementation
@implementation NSObject(MySuperObject)

-(void) printRetainCount {
    NSLog(@"The retain count is %d", [self retainCount]);
}

+(void) sayHello {
    NSLog(@"Hello everybody!");
}

@end

這裡用的是在後面加個小括號以及 category 的名稱。要注意的是 category 只能加 method(instance method 或是 class method 都行),沒辦法增加 instance variable(其實也不是完全不行,只是可能要用一些怪招,不過如果要做到這種程度,是不是該考慮直接用一般的繼承就好?)。使用起來的樣子:

// 建立 Book 物件
Book *b = [[Book alloc] init];

// instance method from category
[b printRetainCount];

// 用完放掉
[b release];

// class method from category
[Book sayHello];

上面這段程式碼如果一般的情況下,在編譯階段就會跳出警告(認不得 printRetainCount 跟 sayHello 這兩個方法),硬是執行的話就會直接錯誤。但因為我們已經有了 category 的加持,所以執行結果會是:

The retain count is 1
Hello everybody!

因為 category 是在原來的類別裡加功能,所以你可能會想萬一原來的類別裡有個跟你的 category 同名的方法怎麼辦? 答案是,會以 category 的定義為主,也就是原來類別裡的那個方法就被你蓋掉了,當然這不見得是你想要的結果。所以通常這種方法擴充的,會建議可以在前面加個 prefix,避免跟原來的方法有重複而造成不幸的結果。

另外,前面有篇提到 protocol 的文章,也提到了 informal protocol,其實它就是一種依附在 NSObject 上,但是沒有把功能實作出來的 category。一般的 protocol 必需乖乖實作所有 @required 的方法(在 Objective-C 2.0 以前全部預設都是 @required),但在 informal protocol 並沒有強制規定全部都要實作出來。事實上,在 Objective-C 2.0 之後才在 protocol 裡加進來的@optional 語法,就是打算用來取代 informal protocol 的,在比較新版本的 SDK 大多都是用標準的 protocol 在寫了。