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

次は、カテゴリについて勉強したいと思います。

*1:そういう言い方をする。後述。

*2:他にもKVOなどがある

*3:ライブラリで多用されている

*4:JavaScriptとか

*5:NSObjectはプロトコルでもありクラスでもある。言語仕様的に、同名で@protocolと@interface両方を宣言することが可能。

*6:自分のために

*7:Objective-C的にも、相互に参照を保持(retain)することがあまり良くない、という事だと思います

*8:繰り返しになるが、メモリ管理の問題についてはassignにすることで解決している。