2010年11月23日 星期二

Protocol & Delegate

Protocol

在Objective-C 中protocol 就是一個很多 method 的宣告集合的地方,感覺上和interface 有點像而和interface的差別在於protocol 裡面不會有變數的宣告。我們直接來看一個例子。
@protocol Omniprinter

-(void) printInt:(int ) intVar;
-(void) printObj:(NSString *) obj;

@end
這個例子就是,我們宣告了一個protocol 叫 Omniprinter 前面需要加上關鍵字@protocol,這個protocol 裡面就只有 printInt: 和 printObj: 這兩個 method。Protocol 就這樣簡單地宣告完成了。如果是要接受或叫遵循這個 protocol 的 class就要在@interface這樣寫。
@interface Hello: NSObject<Omniprinter>{
}
@end
這個就代表 Hello 這個 Class 有採用 Omniprinter 所定義的 method 。如果一次要採用很多個 protocol 就用 , 逗點分開每個 protocol 的名字,比如。
@interface Hello: NSObject<Omniprinter, protocol1, protocol2 >
當我們寫在 interface 說要採用某個 protocol 的時候我們就有可能會在 implementation 這樣寫。
@implementation Hello
-(void) printObj:(NSString *) obj{
    NSLog(@"%@", obj);
}
@end
就是把 protocol 所宣告的 method 實作在有採用這個 protocol 的 Class 的 implementation 的部分。在上面的例子筆者故意只寫一個 method 實作,是要來說明 protocol 裡的 method 還有分別的。比如剛剛的 protocol ,Omniprinter 我們加幾個關鍵字。
@protocol Omniprinter

@optional
-(void) printInt:(int ) intVar;
@required
-(void) printObj:(NSString *) obj;

@end
看名字就知道在@optional 之後的 method 是選擇要不要實作的,而在 @required 之後的 method 的是一定要實作的,如果什麼都沒有寫預設就是 @required 。如果不遵這樣的規則Xcode 會給警告。Protocol 本質上就是這樣簡單的一個機制,在 implementation 要實作在 interface 所採用的 protocol 的 method 。

型別裡的Protocol

接著要介紹的觀念是當Protocol 放到型別裡的時候是怎麼樣的情況,先來看這個例子。
id<Omniprinter> delegate ;
這樣子的寫法是代表說 delegate 這個變數,它可以是任意型別,但一定要遵循 Omniprinter 這個 protocol ,也就是說,delegate 的實體必需要實作 Omniprinter 所定義的@required 的  method 。如果我們限制 delegate 的型別呢?就會這樣寫。
Hello<Omniprinter> * delegate ;
這樣就代表 delegate 這個變數所存的實體必需是 Hello * 這個型別而且要有實作 Omniprinter 這個 protocol 裡面 @required 的 method 。

NSObject 與 <NSObject>

我們知道所有的自訂的Class 都要繼承自 NSObject 這個 Class ,而在 protocol 的世界裡也有一個叫 NSObject 的 protocol ,如果去查這個 protocol 所規範的 method 不外乎就是 init ,release ,retain 等等的在NSObject 有定義過的 method 。其實 NSObject 就是遵循了<NSObject> 而實作了重要的 method 。如果我們這樣寫。
@protocol Omniprinter<NSObject>
代表著這個 Omniprinter protocol 也採用了<NSObject> 這個 protocol 。那也就是說如果這樣寫。
id<Omniprinter> delegate ;
也就代表著 delegate 也要實作 <NSObject> 所宣告的 @required method。那是不是要寫很多程式?不用怕,還記得所有的我們自訂的或是系統提供的Class都繼承自 NSObject 這個 Class 嗎?自然也就有 <NSObject> 的實作了,只要我們照著規範做就可以省去很多事情。

委任 - Delegate

談到Protocol 就不得不談一個Design Pattern 叫做 Delegate  (委任),在 Objective-C 裡面經常用 Protocol 來完成 Delegate 所要做的事情。委任就是某 A 要完成一件事情,A 沒辦法自己獨力完成,而需要借用其他人,比如 B 的能力來完成。此時,A 就是委任者,B 就是受委任者。換成程式語言來說能力就是 method ,當 A 的能力不足的時候,就是 method 的功能不多,當 A 委任給 B 來做事的時候,就是利用 B 的 method 來完成事情。我們來看一個很簡單的例子。假設 B 是另一個 Class 。
@interface A {
    B * delegate;
}
@end

@implementation A
-(void) doSomething {
    [ delegate  actionOfB ];
}
@end
這個例子很簡子的只是在 doSomething 這個 method 裡呼叫 delegate 的 actionOfB 這個 method 。但其中隱含的概念是,A 這個 Class 借用 B 的能力 ( method ) 來完成 A 的 doSomething 這件事。delegate 這個變數的型別是 B * ,無論其在什麼時候給一個 B 的實體,在 A 的 method ,doSomething 裡被借用了能力( method ) 就是受了 A 的委任來完成事情。這就是一個很簡單委任的例子。接著我們來看看在 Objective-C 裡怎麼用委任這個概念。尤於要寫的內容太多,這個例子就分成幾個檔案。把 @interface 都放在 .h 檔,@implementation 都放在 .m  檔裡。
新增 MyWallet.h 以及 MyWallet.m 檔。
在 MyWallet.h 打上這樣的程式碼:
#import <Foundation/Foundation.h>

@protocol examMoney;
@interface MyWallet : NSObject {
    NSString * title;
    float money;
    id<examMoney> delegate;
}
-(void ) showTheMoney;
@property (assign) id<examMoney> delegate;
@property (retain) NSString * title;
@property (assign) float money;
@end

@protocol examMoney<NSObject>

-(void) getWallet:(MyWallet *) wallet withMoney:(float) money;

@end
在 MyWallet.h 裡我們把先寫了這樣的宣告 @protocol examMoney; 這列是要和 compiler 說 examMoney 這串字不是別的正是 protocol 。然後接著宣告 MyWallet 這個 Class 的 interface 的部分。也就是 @interface 到 @end ,這中間的程式碼。MyWallet 這個 Class 有三個實體變數 title , money 和 delegate ,分別都有各自的property 宣告。其中筆者要強調的當然就是 delegate 這個變數,其型別為 id<examMoney> 也就是就說這個變數所要存放的實體必需要實作 <examMoney> 這個 protocol ,雖然看到這列還不知道 protocol 內容長什麼樣,但是就語法來說,這樣就是合法的。再來看一下 delegate 的 property 的 attribute 為 assign ,還記得筆者說過,有 protocol 型別的 property ,其 attribute 要用 assign 就和 money 這個 primitive 型別的變數一樣要用 assign 。在這個例子 NSString * title 的 property attribute 筆者習慣用 retain 。再往下看程式碼,就看到了 <examMoney> 這個 protocol 的內容,真正需要實作的 method 叫什麼名字了。把這個檔案從頭看到尾就發現我們其實把 Class 的宣和 Protocol 的內容放在一起,而且,注意看 Protocol 的 method 第一個傳入的參數是什麼型別?就是這個 Class 的型別。這樣的寫法雖然是 MyWallet 要委任 <examMoney>   給別人完成,但看起來也很像是 MyWallet 把資料傳給另一個物件。所以也有人說這是一種在 Objective-C 裡面物件之間傳遞資料的方法。至於 showTheMoney 的用途在接下來會解說。
接著看 MyWallet.m 裡面要寫什麼。
#import "MyWallet.h"
@implementation MyWallet
@synthesize delegate, title, money;
-(void) showTheMoney {
    if ([delegate respondsToSelector:@selector(getWallet:withMoney:)]) {
        [delegate getWallet:self withMoney:money];
    }else {
        NSLog(@"Please implement getWallet:withMoney: ");
    }

}
@end
在這個檔案裡,除了把 property ,synthesize 之外,還有寫了 showTheMoney 的實作。showTheMoney 這個 method 實作的內容就是把 delegate 這個有實作 <examMoney> 的物件直接拿來用,也就是呼叫 @selector(getWallet:withMoney:) ,但是在此之前要先檢查一下 delegate 所存的物件是不是真的有實作 getWallet:withMoney: 於是先用 responseToSelector: 來判斷 delegate 是否有實作,否則就印出訊息在 Console。委任者寫完了,再來看看受委任者的寫法。再新增 OmniExamer.h 和 OmniExamer.m 兩個檔案。
OmniExamer.h 的內容會是
#import <Foundation/Foundation.h>
#import "MyWallet.h"

@interface OmniExamer : NSObject<examMoney> {

}

@end
OmniExamer 這個 Class 最主要的目的就是實作 <examMoney> 的 method ,一開始先 #import "MyWallet.h" 讓 OmniExamer 知道 <examMoney> 和 MyWallet 的存在。然後在 Class 最後寫 :NSObject<examMoney> 就是繼承 NSObject 而且遵循 <examMoney>  的規範,就這樣就可以了。
接著看 OmniExamer.m 的寫法。
#import "OmniExamer.h"

@implementation OmniExamer

-(void) getWallet:(MyWallet *) wallet withMoney:(float) money{
    NSLog(@"The wallet : %@ with Money %f ", wallet.title, money);
}
@end

一開始就是 import OmniExamer.h 然後就是重點,實作 <examMoney> 的 getWallet:withMoney: ,在這個例子就是把 money 印在 Console 而且因為第一個參數就是MyWallet 的物件,所以我們可以甚至可以把MyWallet 物件的property 拿來用,就好像這個列子一樣,在印 money 之前把 wallet.title 也印在 Console。
當我們這四個檔案就準備好了,就剩下在 main 測試了。main.m 請如下打字。


#import <Foundation/Foundation.h>
#import "OmniExamer.h"
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    MyWallet * wallet = [MyWallet new];
    wallet.title = @"New Wallet";
    wallet.money = 500.0;
    OmniExamer * oe = [OmniExamer new];
    wallet.delegate = oe;
    [wallet showTheMoney];

    [pool drain];
    return 0;
}
一開始當然要先 import OmniExamer.h 這樣才可以知道 OmniExamer Class 而 OmniExamer.h 又有 import MyWallet.h 所以也可以知道 MyWallect Class 和 examMoney Protocol 。在 main function 裡就是產生 MyWallet 的實體放在 wallet 這個變數裡,然後設定 title 和 money ,再產生 OmniExamer 的實體放在 oe 。接者就是重點了,把 wallet.delegate = oe ; 這個程式就是表示 wallet.delegate 目前和 oe 指向相同的物件實體,也就是委任關系的成立,wallet 可以透過 delegate 來使用 oe 所實作 <examMoney> 的 method 。最後就是 [wallet showTheMoney]; 呼叫 showTheMoney ,還記得此 method 用了 delegate 來做事吧,執行到此 delegate 就有 oe 所指的物件實體,也就是 oe 在幫 wallet 做事了,而且 wallet 的資料也傳到 oe 上面。

3 則留言:

  1. Hello,

    請問一下,這樣的做法要用在什麼狀況?
    有什麼意義嗎?

    回覆刪除
  2. 在無論是 iOS 或是 Mac 的 SDK 提供的元件中,經常會用到,而我們要寫的事情就是去 adapt 那個元件的 protocol 。比如說 UIImagePickerController ,初始化的之後,要設定其 delegate 變數要指向一個有實作 UIImagePickerControllerDelegate 的物件,常會在 ViewController 的 .m 實作其 method 才可以拿到使用者選到的圖片。

    回覆刪除