Objective-Cのカテゴリ

Friday, March 23rd, 2012

Objective-Cの機能のネーミングは、わかりにくい気がします。今回は名前からは想像できない機能を持つ「カテゴリ」です。

カテゴリは既存のクラスを、継承することのなく機能拡張してしまう機能です。なんと恐ろしい。

@interface MyClass : NSObject
@end

@implementation MyClass
@end

@interface MyClass (MyCategory)
-(void)print;
@end

@implementation MyClass (MyCategory)
-(void)print;
{
  NSLog(@"Hello World!");
}
@end

これで、MyClassを拡張して、新たにprintメソッドを追加しています。@interfaceと@implementationを以下のように書くところがポイントです。

@interface 拡張したいクラスの名前 (カテゴリ名)
...
@implementation 拡張したいクラスの名前 (カテゴリ名)
...

同じファイルに基のクラス定義とカテゴリの定義を書いてしまうと、すごさがわかりずらいけど、このクラスの拡張はラインタイム時に行われるので、この定義を参照しているところだけでなく、すべてのコードに反映されます。
そして、これはNSObjectやNSStringに対してもできてしまう!!
機能としてはC#のpartialクラスが近いだろうけど、C#は基のクラスもpartialにしなければならないから、最初から拡張されると定義されたクラスにしかできない。でもObjective-Cのカテゴリは関係なく、どのクラスに対してもできてしまいます。

利用シーンとしては、C#のpartialと同様に、ツールによる生成部分とソースコードを分けるために使うというのがあります。後は、それこそ、継承せずに既存のクラスを拡張してしまうという使い方ですが、これをしてしまうとコードの可読性はおそらく悪くなるのでおすすめはしません。

おそらく多くの人にとって有用なのはGoogleのコーディング規約にもあるように、クラスが内部で利用するメソッドと、外部へ公開するメソッドに分割するためにカテゴリを使うという手法でしょう。
特にObjective-C 2.0からはカテゴリ名を無名にしたクラスエクステンションという機能により、これを実現できるようになりました。MyClassを新たにクラスエクステンションを用いて、公開メソッドとプライベートメソッドに分けて定義してみます。まずはヘッダーファイル(.h)です。
MyClass.h:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
-(void)print;
@end

公開メソッドであるprintしか定義がありません。次に、実装ファイル(.m)です。
MyClass.m:

#import "MyClass.h"

@interface MyClass ()
-(void)innerMethod;
@end

@implementation MyClass
-(void)print
{
    [self innerMethod];
}
-(void)innerMethod
{
    NSLog(@"Hello World!");
}
@end

ここで注意するのが以下の部分です。

@interface MyClass ()
-(void)innerMethod;
@end

括弧内にカテゴリ名を記述しないことでクラスエクステンションとなります。そして内部メソッドであるinnerMethodを定義しています。@implementationには、公開メソッドであるprintと、プライベートメソッドであるinnerMethodの両方の実装を記述しています。これで、printのみが外部からアクセスでき、innerMethodにはアクセスできなくなります。