高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Category in Objective-C

image

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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(其實也不是完全不行,只是可能要用一些怪招,不過如果要做到這種程度,是不是該考慮直接用一般的繼承就好?)。使用起來的樣子:

1
2
3
4
5
6
7
8
9
10
11
// 建立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在寫了。

範例原始檔下載

Set to Nil After Release

image

iOS的記憶體管理真的不少東西要學,在Objective-C裡有時候會看到一些程式碼這樣寫:

1
2
3
4
5
6
7
-(void) someMethod
{
  [mybook release];
  mybook = nil;

  [super dealloc];
}

先把它release掉了,然後下一行再把它指向nil。nil跟其它程式語言裡的null或none是一樣的東西,就是nothing、虛無飄渺、沒有任何東西的意思。

問題是,這個nil是必要的嗎? 不指向nil會出什麼包嗎?

先來釐清一個觀念:對一個物件發送release訊息並不會直接把物件佔用的記憶體還給記憶體,release這個動作只是減少retain count而已,當retain count變成0的時候會自動啟動dealloc,這時候才是真正的被消滅掉。

在Objective-C裡所有的物件變數都是指標,指向某一塊配置的記憶體,所以,下面這行程式碼:

1
mybook = nil;

意思是把mybook這個變數指向nil。這樣說來,是不是也不用什麼release了,直接把變數設定成nil不就好了?

我們先來看個例子:

1
UIImage *img = [[UIImage alloc] init];

這應該不陌生了,這建立一個名為img的指標變數,並指向某一個UIImage的實體。如果我在它下面再加一行:

1
img = [[UIImage alloc] init];

就是再做一次alloc/init,讓剛剛那個img變數指向這個新的實體。看起來很直覺,但指標的東西跟我們一般寫的程式的習慣不同,當你把img指向另一個新的實體,原來舊的實體並不會消失,它會存在記憶體裡,只是沒有人指向它,所以也沒有人能再存取它。當然在有支援GC的環境上,系統會自動回收那些沒人要的東西,但在不支援GC的環境,那個沒人要的孩子就會一個人孤獨的漂流著了,所以如果你要這樣做,請記得先把原來的那顆給release掉,再給它一顆新的。

那如果我再接著這樣做:

1
img = nil;

把img指向nil並不會減少retain count,那剛剛前面我產生的第二個物件就會跟第一個物件一樣,它不會消失,它只是在記憶體空間裡漂流,當然它佔用的記憶體也不會還給系統。所以請切記,如果你要把物件給丟掉,不要直接把變數設成nil,請使用release或autorelease

回到最一開始的問題,我的答案是:其實是沒什麼需要一定要在release之後再弄個nil給它,但這樣做是個好習慣。

那什麼時候會出問題?

在別的程式語言裡,當你要存取null物件的時候,通常會直接跳錯誤給你看,但在Objective-C裡如果你對nil送訊息的,即使是沒定義的method,它也只會靜靜的不回應而已。所以,回到最前面的程式碼來看,當你把mybook給release之後,也許有那個萬一的情況下,有別的method要存取它,但它可能因為已經被dealloc掉了,所以就會爆炸了(發生機會很小,但不代表永遠不會發生)。但如果把mybook指向nil,即使有人要再來存取這個mybook也不會出現錯誤了。

以上,我對記憶體管理的東西的了解相當有限,所以如果以上內容有誤還請不吝指正 :)

Protocol in Objective-C

image

前面提到了OOP的繼承,但不像C++可以有多重繼承,Objective-C是單一繼承的,如果想要做到一個類別同時擁有多種型別的能力,可以透過實作其它型別的interface來達成這個目的。在Java/AS3是用”interface”這個關鍵字,在Objective-C則是用”@protocol”。(有寫過Java/AS3的要特別注意不要把interface跟protocol搞混了,在Objective-C的interface等於Java/AS3的class,而protocol則是相當於interface)

直接來看看要怎麼做吧。如果你要新增一個自定的protocol的話,可以直接在你的專案裡新增一個protocol檔:

image

當然,你要全部寫在一起也沒人反對,只是為了模組化以及以後的可重複使用考量,建議獨立出來另外寫。新增完成之後(它是一個header檔),就可以開始來寫了,程式碼如下:

1
2
3
4
5
6
@protocol Drawable

-(void) draw;
-(void) changeColor;

@end

在Objective-C裡的protocol是用@protocol這個語法來定義的。在上面這段程式碼裡,我放了兩個方法,但沒有寫內容。接下來如果我要實作自這個protocol的話,所有定義在@protocol裡的方法都得實作出來。另外,在Objective-C 2.0之後加了@required@optional的語法,可以讓你設定這個method是不是必需一定要實作的項目。用法如下:

1
2
3
4
5
6
7
8
9
10
@protocol Drawable

@required
-(void) draw;
-(void) changeColor;

@optional
-(void) whateverMethod;

@end

如果沒特別標明的,預設是@required。如果你要實作這個protocol的話,照英文字面來看,@required的部份是規定要實作的,@optional的話就隨你高興了。要注意的是@required跟@optional這兩個語法的影響範圍,是從它以下所有的method都會被影響,直到另一個directive或是@end為止,所以如果你要省略@required的話,記得那些method要寫在@optional前面。接下來來看看要怎麼實作這個protocol:

1
2
3
4
5
6
7
8
9
10
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

實作protocol的方法就是用”<>”標記,裡面放protocol的名稱。並不限定只能實作一個protocol,如果要實作多個protocol的話,則是用逗點分開:

1
@interface Book : NSObject <Drawable, Openable>

因為到目前為止,我們都還沒實作那個protocol裡定義的方法,所以這時候如果直接按下Build的話,就會跳出警告訊息:

image

接著來把該做的填一填吧。因為在protocol的地方已經有定義好了方法,所以在@interface的地方就不用再特別寫一次,只要在@implementation裡補上該實作的方法就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// --------------
// interface
// --------------
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

// --------------
// implementation
// --------------
#import "Book.h"
@implementation Book

@synthesize price;

// 實作方法draw
-(void) draw
{
  NSLog(@"draw me!");
}

// 實作方法changeColor
-(void) changeColor
{
  NSLog(@"change color!");
}

@end

如果你實作了所有@required的方法的話,則稱為遵守(conform)或採納(adopt)這個protocol(硬翻成中文還是覺得怪怪的,還是英文比較簡潔直接)。若要檢查某物件是否有乖乖遵守某個protocol的規定:

1
2
3
4
5
Book *book = [[Book alloc] init];
if ([book conformsToProtocol:@protocol(Drawable)] == YES)
{
  NSLog(@"the book is conform to Drawable protocol");
}

protocol本身也可以像一般類別的繼承,例如:

1
2
3
4
5
6
7
@protocol A
-(void) methodA;
@end

@protocol B <A>
-(void) methodB;
@end

這時如果你要實作protocol B,則methodA跟methodB都需要實作。

另外,你也可以把protocol拿來當一般的型別定義來用,例如:

1
id <Drawable> some_object;

表示說這個some_object是個有實作Drawable這個protocol的物件,在編譯階段就可以先做型別檢查。當然也可以一次多個,一樣用逗點分開:

1
id <Drawable, Openable> some_object;

上面提到的這種用@protocol來定義方法的,稱做formal protocol,從名字看大概猜得出來一定也有叫做informal protocol的東西,不過這個會在category的部份再做說明。

@class directive in Objective-C

前面幾篇有提到了在Objective-C裡的OOP要怎麼寫,也知道要怎麼在@interface裡設定instance variable(以下簡稱ivar),雖然範例程式碼裡只是用了int這個簡單型態,但實際上更常看到的是在A類別裡有放一個B類別的物件的情況(Composition)。通常你如果要用到一個A類別的時候,你會用:

1
#import "A.h"

的方法把header檔給import裡來,但是來假想一下下面這個情境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A類別裡有放了一顆B ivar
@interface A : NSObject
{
  B *b;
}

@end

@implementation A
@end

// B類別裡有放了一顆A ivar
@interface B : NSObject
{
  A *a;
}
@end

@implementation B
@end

當然舉這個例子是比較極端啦,這樣的情況應該很少發生。如果你跑一下以上的的程式碼,會得到錯誤訊息”expected specifier-qualifier-list before ‘B’”,意思就是說這個B類別並不存在。A跟B兩個類別互相有需要,但B還沒出生,變成是雞生蛋蛋生雞的問題,這時候就可以用上@class這個語法了。只要在@interface A前面加一行:

1
@class B;

這行就是告訴編譯器說「這個B是一個類別,至於這個B類別的內容你不用管,反正遲早會告訴你,你先用就是了」,這樣一來就可以順利通過編譯了。不過寫了@class不代表就可以不用實作那個類別的內容,如果到時候沒有import B的header進來或是沒有實作B類別,硬是要用的話在編譯過程就會跳出一個訊息”warning: receiver ‘B’ is a forward class and corresponding @interface may not exist”。

優點:

  1. 跟#import語法比起來,用@class的編譯速度比較快。
  2. 可解決互相參照的問題。

缺點:

其實我也不知道這樣會有什麼缺點

Property and Synthesize

image

上篇,因為在類別裡,instance variable(以下簡稱ivar)預設是protected的,也就是說只有該類別以及它的子類別才能存取它,如果要給外部使用的話,則需要來幫它加個setter/getter。但每次只為了一個ivar就要寫一對的setter/getter也太麻煩了,在Objective-C 2.0之後加入了@property@synthesize的語法,讓這個工作簡單多了。借用上一篇的例子:

1
2
3
4
5
6
7
8
9
10
@interface Book : NSObject
{
  int price;
}

-(int) price;
-(void) setPrice: (int) p;
+(void) printBookInfo

@end

如果改用@property來寫:

1
2
3
4
5
6
7
8
9
@interface Book : NSObject
{
  int price;
}

@property int price;
+(void) printBookInfo;

@end

原來的setter/getter就可以省下來不用寫,然後在@implementation的部份則是使用@synthesize語法:

1
2
3
4
5
6
7
8
9
10
@implementation Book
// 這個@synthesize語法幫忙產生setter/getter
@synthesize price;

+(void) printBookInfo
{
  NSLog(@"Hello, This is a book");
}

@end

這裡的@synthesize price,其實就相當於自動產生了我們在上一篇寫的那一對setter/getter的程式碼:

1
2
3
4
5
6
7
8
9
-(int) price
{
  return price;
}

-(void) setPrice: (int) value
{
  price = value;
}

這樣程式碼就簡潔許多了。雖然@synthesize會自動幫忙產生setter/getter,但如果你想要寫自己的setter/getter,或是想要額外添加功能在裡面,只要照著它預設生成的setter/getter方法命名規則就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// interface
@interface Book : NSObject
{
  int price;
}

@property int price;
+(void) printBookInfo;

@end

// implementation
@implementation Book

@synthesize price;

// 自定setter
-(void)setPrice:(int)p
{
  // 故意讓傳入值變2倍
  price = p * 2;
}

+(void)printBookInfo
{
  NSLog(@"Hello, This is a book");
}
@end

@synthesize雖然會自動幫你建立一對setter/getter,但還是會以你建立的為主。

另外,記得在別的程式語言裡可以用點”.”來存取物件的屬性嗎? Objective-C 2.0之後也可以這樣做了,直接看看語法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Book *b = [[Book alloc] init];

// 一般的setter用法
[b setPrice:300];

// 一般的getter用法
NSLog(@"the price of the book is %d", [b price]);

// Objective-C 2.0之後可使用點語法,它會自動呼叫setter並將值傳給它
b.price = 200;

// 這裡則是呼叫getter把值傳回來
NSLog(@"the price of the book is %d", b.price);

[b release];

這樣有比較習慣了嗎? 我相信很多人對Objective-C的方括號的語法很感冒,至於程式碼的可讀性就看個人了,方括號語法看久了也是滿習慣的。不過用點語法的時候要注意幾件事,例如你自己寫了一個setter,又在裡面用了點語法:

1
2
3
4
5
-(void) setPrice: (int) p
{
  // 如果這樣寫的話,會啟動setPrice這個method
  self.price = p;
}

然後..就變無窮迴圈了! 這點要特別注意一下。

還有個問題是其實這個點語法光從程式碼其實看不太出來到底是物件還是結構還是變數..再來看另一個例子:

1
2
3
CGPoint pos = player.position;
pos.x += acceleration.x * 10;
player.position = pos;

這是一段Cocos2D的語法,內容大意是說:「我要建立一個CGPoint變數叫做pos,而這個pos是由player這個CCSprite的position來的」。acceleration是一個UIAcceleration物件,取得acceleration的x的值,修改pos裡的x之後再把整個pos變數塞回去給player這個角色。

這段程式碼對老手來說大概會覺得很弱,覺得這大概是新手,還得要建一個暫存變數,遜! 要是他來寫根本就可以直接併成一行:

1
player.position.x += acceleration.x * 10;

但事實上這樣寫的話會丟出一個”lvalue required as left operand of assignment”的編譯錯誤。為什麼會這樣? 分段來看:

1
player.position.x

其實這種”點語法”是一種”語法糖衣(syntactic sugar)”,事實上它是:

1
[player position].x

這邊position是一個getter,可以讓你從player身上取得一個CGPoint型態的position(r-value),但因為並沒有把指定給某個變數(l-value),所以當你要想用setter把它寫回去的時候,這個值已經消失了。

感想:看來Objective-C不只語法不同,連一些習慣寫法也不同了,眉眉角角真多!

建議閱讀: