Objective-Cのプロトコルとデリゲートのまとめ
Objective-Cの@protocol、@required、@optional、そして所謂Protocol-Delegateパターンの基本のまとめです。Javaが普通程度に出来る人を対象としてます。(Objective-C歴1週間程度なので要ツッコミ)
サンプルコード
百聞は一見にしかず、ということでサンプルコードです。
まずはヘッダex.h。本当はクラスごとに分けるべきですが、面倒なので一纏めにしています。
// ex.h #import <Foundation/Foundation.h> @protocol MyProtocol1 @required - (void)foo; @optional - (void)bar; @end @protocol MyProtocol2 - (void)hoge; @optional - (void)piyo; @end @interface MyClass1 : NSObject <MyProtocol1, MyProtocol2> @end @interface MyClass2 : NSObject <MyProtocol1, MyProtocol2> @end @interface MyClass3 : NSObject <MyProtocol1, MyProtocol2> - (void)test; @end @interface Runner : NSObject @property(nonatomic, assign) id <MyProtocol1, MyProtocol2, NSObject> delegate; - (void)run; @end
次に、実装ex.m。
// ex.m #import "ex.h" @implementation MyClass1 - (void)foo { printf("%s\n", __PRETTY_FUNCTION__); } - (void)hoge { printf("%s\n", __PRETTY_FUNCTION__); } - (void)piyo { printf("%s\n", __PRETTY_FUNCTION__); } @end @implementation MyClass2 - (void)foo { printf("%s\n", __PRETTY_FUNCTION__); } - (void)bar { printf("%s\n", __PRETTY_FUNCTION__); } - (void)hoge { printf("%s\n", __PRETTY_FUNCTION__); } @end @implementation MyClass3 - (void)foo { printf("%s\n", __PRETTY_FUNCTION__); } - (void)hoge { printf("%s\n", __PRETTY_FUNCTION__); } - (void)test { Runner *runner = [[Runner alloc] init]; runner.delegate = self; [runner run]; [runner release]; } @end @implementation Runner @synthesize delegate = _delegate; - (void)run { [_delegate foo]; if ([_delegate respondsToSelector:@selector(bar)]) [_delegate bar]; [_delegate hoge]; if ([_delegate respondsToSelector:@selector(piyo)]) [_delegate piyo]; } @end
最後にmain関数を含むmain.m。
// main.m #import <Foundation/Foundation.h> #import "ex.h" int main() { Runner *runner = [[Runner alloc] init]; [runner run]; runner.delegate = [[MyClass1 alloc] init]; [runner run]; [runner.delegate release]; runner.delegate = [[MyClass2 alloc] init]; [runner run]; [runner.delegate release]; [runner release]; MyClass3 *instance = [[MyClass3 alloc] init]; [instance test]; [instance release]; return 0; }
コンパイルして実行します。
$ gcc main.m ex.m -framework Foundation $ ./a.out -[MyClass1 foo] -[MyClass1 hoge] -[MyClass1 piyo] -[MyClass2 foo] -[MyClass2 bar] -[MyClass2 hoge] -[MyClass3 foo] -[MyClass3 hoge]
ポイント
@protocol
@protocolはJavaのinterface的なものです。「的な」と表現しているのは、次で述べますが、@required/@optionalコンパイラディレクティブを指定することにより、実装すべきものと、実装しなくて良いものを、プロトコル内で分けることができるためです。Javaのinterfaceは多態性(ポリモーフィズム)を実現するための機能で、interfaceとそれを実装するクラスが必ずis-a関係になりますが、Objective-Cでは@optionalなら実装しなくてもよいことになるので、必ずしもis-a関係になるわけではありません。それにより、後述しますが、delegateがid型という緩い型であったり、メソッドを実装しているかのチェックが必要であったりします。これが、Javaのinterfaceと、Objective-Cの@protocolの大きな違いの1つだと思います。
@requiredと@optional
プロトコルには、そのプロトコルを採用*1した時に実装が必須のメソッドと、必須でないメソッドを分けて定義することが出来ます。デフォルト(何も指定しない場合)は、@requiredです。
@protocol MyProtocol2 - (void)hoge; // これはrequired @optional - (void)piyo; @end
プロトコルの採用
Javaでは「interface(インターフェース)はimplements(実装)する」ものですが、同様な感じで、Objective-Cでは「protocol(プロトコル)はadopt(採用、"conform to"とも)する」といった表現をするようです。
Javaのinterfaceと同様、複数のプロトコルを採用することが可能です。
// @interface クラス名 <プロトコルs> // 継承するなら // @interface クラス名 : 親クラス名 <プロトコルs> @interface MyClass1 : NSObject <MyProtocol1, MyProtocol2> @end
デリゲート
プロトコルとセットで出てくる概念がデリゲート(delegate、委譲)です。しばしば「Objective-Cのdelegate」的な表現がされるので、C#のように言語機能にdelegateなる機能があるのかと勘違いしそうですが、そういうわけではなく、Objective-Cの公式デザインパターン*2として確立している*3ため、度々取り沙汰されています。
まず、宣言側を見てみます。
@interface Runner : NSObject // デリゲート用のプロパティを用意。名前は慣例としてdelegate。 // "id <この部分>"は必須ではないが入れるとコンパイラがメソッドが存在するか // チェックしてくれる。入れないと、実行してみて例外がでた、みたいなことになる // ので、入れておくのが無難(だし分かりやすいと思う)。 @property(nonatomic, assign) id <MyProtocol1, MyProtocol2, NSObject> delegate; - (void)run; @end
このid型という巷の動的スクリプト言語*4のような緩い型が特徴的で、これによりdelegateプロパティが任意のオブジェクトを格納できるようになります。delegateしたいときには、外部からdelegateプロパティにプロトコルを採用したインスタンスをセットします。
実装側は、
@implementation Runner @synthesize delegate = _delegate; - (void)run { [_delegate foo]; if ([_delegate respondsToSelector:@selector(bar)]) [_delegate bar]; [_delegate hoge]; if ([_delegate respondsToSelector:@selector(piyo)]) [_delegate piyo]; } @end
@requiredなメソッドは、そのまま呼び出せば良いのですが(Objective-Cではnilにメッセージを投げても問題ないので_delegate
がnilかどうかのチェックは必要ありません)、@optionalなメソッドについては、存在確認をする必要があります。それがrespondsToSelector
です(SEL型、@selectorについては別途まとめる予定です。)。
以上をもって、サンプルのmain関数のように利用できます。
Runner *runner = [[Runner alloc] init]; [runner run]; // runner.delegateがnilなので何も表示されない runner.delegate = [[MyClass1 alloc] init]; [runner run]; // MyClass1で実装されているプロトコルのメソッドが実行される runner.delegate = [[MyClass2 alloc] init]; [runner run]; // MyClass2で実装されているプロトコルのメソッドが実行される
ここではdelegate
に別途インスタンス化したオブジェクトをセットしていますが、実際にはMyClass3のようにself
をセットすることが多いかと思います。
プロトコルの継承
サンプルコードには載せませんでしたが、プロトコルは継承できます。
例えば、サンプルのRunnerクラスでは、
@property(nonatomic, assign) id <MyProtocol1, MyProtocol2, NSObject> delegate; @end
のようにNSObjectプロトコル*5をdelegateが採用しているプロトコルとして指定していますが、これを指定するのが嫌なら、MyProtocol1もしくはMyProtocol2に継承させるのが良いでしょう。(ちなみに、ここでNSObjectを指定しているのは、respondsToSelector
がNSObjectプロトコルの持つメソッドだからです。)
@protocol MyProtocol1 <NSObject> //... @end @protocol MyProtocol2 <NSObject> //... @end @interface Runner : NSObject @property(nonatomic, assign) id <MyProtocol1, MyProtocol2> delegate; // ... @end
delegateプロパティの属性
さらっとスルーしていましたが、原則、delegateプロパティにはassignを指定するべきだそうです。
@property(nonatomic, assign) id <MyProtocol1, MyProtocol2, NSObject> delegate;
もう少し詳細を書くと*6、まず復習になりますが、@propertyのassignとretainのsetterの擬似コードはそれぞれ、
// assign - (void)setDelegate:(id)delegate { _delegate = delegate; } // retain - (void)setDelegate:(id)delegate { if (_delegate != delegate) { [_delegate release]; _delegate = [delegate retain] } }
になります(参考)。よって、retainの場合、delegateプロパティに代入した時点で、代入されるオブジェクトの参照カウントが増えます。
// runner.delegateプロパティがretainの時 id instance = [[MyClass1 alloc] init]; // instance.retainCount = 1 runner.delegate = instance; // instance.retainCount = 2 [runner run]; [instance release]; // instance.retainCount = 1 // もう一回[instance release]しないといけない…
対策としては、release前にdelegateにnilを入れる方法が考えられます。
// runner.delegateプロパティがretainの時 id instance = [[MyClass1 alloc] init]; // instance.retainCount = 1 runner.delegate = instance; // instance.retainCount = 2 [runner run]; runner.delegate = nil; // instance.retainCount = 1 [instance release]; // instance.retainCount = 0
一応、このように対応出来ますが、Appleは、このような事態を総合的に鑑みて、delegateは原則assignであることを奨励しています*7。
…が!
ここから大変ややこしい話になるのですが、オブジェクトのdeallocでdelegatorをreleaseする場合、releaseの直前にdelegateプロパティにnilを代入する定石があります。
// somewhere in class A - (void) someFunc { b = [[B alloc] init]; b.delegate = self; } - (void) dealloc { b.delegate = nil; // ← [b release]; [super dealloc]; }
これは、先に述べたメモリ管理の問題ではなく、タイミングの問題です*8。上のコードでb.delegate = nil;
が無いと、「bがreleaseされる前に、bがaにメッセージを投げていたけれども、aが既にdeallocされてしまっていて、EXC_BAD_ACCESS例外が発生」といったシチュエーションが起こりうる可能性があります。そうならないように、b.delegate = nil;
として、bがaにメッセージを投げられないようにするわけです。
参考URL
- Objective-Cの言語仕様(プロトコル編) - bi_naの日記
- Objective-C2.0文法メモ プロトコル - white wheelsのメモ
- プロトコルに関するメモ
- Delegateについて | iPhoneメモ
- シンプルなデリゲートパターンのサンプル。
- [iOS] Protocol – Delegateパターン | Objective-C イベント伝達 その1 « きんくまデザイン
- プロトコルデリゲートパターンの実践的なサンプル。
- Objective-Cの基礎(プロトコル定義)@eiKatou Blog
- 同上
- UIViewのタッチイベントをUIViewControllerにデリゲートする - yoshida_eth0の日記
- 実践的なデリゲートに関するメモ
- delegate オブジェクトは retain すべきではない - 24/7 twenty-four seven
- delegateプロパティをretainにしないほうが良い理由
-
- 同上。内容はほぼ同じだけど、もう少し詳細に見ている。
- NSObject Protocol Reference
- NSObjectプロトコルの公式リファレンス
- objective c - Should you set the delegate to nil in the class using the delegate or in the class itself - Stack Overflow
- 何故release前にdelegateにnilを入れるのかについてのQ&A
- [iPhone] UIWebView のリリース前に delegate に nil をセットする必要がある | Sun Limited Mt.
- delegateにnilを入れないといけない具体例
- iOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール | Zero4Racer PRO Developer's Blog
- メモリ管理について総合的に書かれた記事。delegateの話もあり。
- だんだん好きになってきた - Kazzzの日記
- 私のObjective-C全体に対する印象は、この記事に近い。デリゲートについても触れられている。
次は、カテゴリについて勉強したいと思います。
*1:そういう言い方をする。後述。
*2:他にもKVOなどがある
*3:ライブラリで多用されている
*4:JavaScriptとか
*5:NSObjectはプロトコルでもありクラスでもある。言語仕様的に、同名で@protocolと@interface両方を宣言することが可能。
*6:自分のために
*7:Objective-C的にも、相互に参照を保持(retain)することがあまり良くない、という事だと思います
*8:繰り返しになるが、メモリ管理の問題についてはassignにすることで解決している。