カテゴリーとクラスエクステンション

カテゴリーとクラスエクステンションは、Objective-Cで使えるクラス拡張の方法です。 既存のクラスにメソッドを追加したりクラスの分割をしたりすることができるので、中級以上のObjective-Cユーザーには必須の機能です。

カテゴリーによるメソッド追加

カテゴリーを使うと、既存のクラスにメソッドを追加することができます。 自分でつくったクラスにメソッド追加することもできますし、システムのフレームワークの中のクラスに追加することもできます。 例えば、NSStringクラスに二つの新しいメソッドを付け加えた例を見てみましょう。

まず、追加メソッドのヘッダーファイルはこうなります。

// NSString+ToyshipMode.h
@interface NSString (ToyshipMode)

+ (void)ToyshipMode1;
- (void)ToyshipMode2;

@property NSString* ToyshipText;

@end

追加したい元クラスの「@interface NSString」の後ろに独自名のカテゴリー(ここでは「ToyshipMode」としています)を追加して、その後ろに追加するメソッドを記述します。 クラスメソッドとインスタンスメソッドのどちらでも追加できます。 (上の例ではToyshipMode1がクラスメソッド、ToyshipMode2がインスタンスメソッドですね。) メソッドと同様にpropertyも追加することができます。 (上記ではToyshipTextというNSStringのpropertyを追加しています。) ただし、インスタンス変数は追加することができません。

次に、実装ファイルはこうなります。

// NSString+ToyshipMode.m
@implementation NSString (ToyshipMode)

+ (void)ToyshipMode1{
    NSLog(@"ToyshipMode1!!!");
}

- (void)ToyshipMode2{
    NSLog(@"ToyshipMode2!!!");
}

- (void)setToyshipText:(NSString *)NewText{
    // cannot store in ToyshipText
}

- (NSString*)ToyshipText{
    return @"Hello!";
}
@end

メソッドの実装は通常と同様ですが、propertyの扱いはちょっと注意が必要です。 propertyのsynthesizeが使用不可能になるので、自分でsetter/getter関数をかかなければいけません。 また追加したpropertyへの値のストアは不可能なので、かなり使い勝手が限定されそうです。

なお、カテゴリーで追加されたヘッダー・実装ファイルは既存クラスとカテゴリー名を「+」で連結して「NSString+ToyshipMode.h」などとするのが慣習です。

名前の衝突に注意

カテゴリーを使ってメソッドを追加する時に、追加するメソッド名が元のクラスのPrivateメソッドと同じだった場合、動作が不定となります。 Privateメソッドの名称を調べて同じ名前にしないという方法もありますが、フレームワークのバージョンアップで新規メソッドが追加されることもあるので、完全にふせぐのは困難です。

この対策として、Appleはカテゴリーにはプリフィックスをつけることをすすめています。

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

カテゴリーによるファイル分割

カテゴリーは、自分の作っているファイルのクラスが大きくなってしまった時のファイル分割にも使えます。

たとえば、こんな感じの「Person」クラスがあったとします。

// Person.h
@interface Person

+ (void)initPerson;
+ (void)initPerson:(BOOL)animated;
- (void)showPerson;
- (void)showPerson:(BOOL)animated;
- (void)showPerson:(BOOL)animated withSender:(id)sender;
- (void)changeName:(NSString*)name;
- (void)changeName:(NSString*)name withAnimation:(BOOL)animated;
- (void)changeName:(NSString*)name withLocale:(NSLocale*)locale;

@end

このファイルをカテゴリーを使って分割してみましょう。

show系のカテゴリー名を「Show」、changeName系を「ChangeName」としてファイルをわけてみると、上の「Person.h」ヘッダーファイルが、下記の「Person.h」「Person+Show.h」「Person+ChangeName.h」の3ファイルになります。

// Person.h
@interface Person

+ (void)initPerson;
+ (void)initPerson:(BOOL)animated;

@end
// Person+Show.h
#import "Person.h"

@interface Person (Show)

- (void)showPerson;
- (void)showPerson:(BOOL)animated;
- (void)showPerson:(BOOL)animated withSender:(id)sender;

@end
// Person+ChangeName.h
#import "Person.h"

@interface Person (ChangeName)

- (void)changeName:(NSString*)name;
- (void)changeName:(NSString*)name withAnimation:(BOOL)animated;
- (void)changeName:(NSString*)name withLocale:(NSLocale*)locale;

@end

実装ファイルも、ほぼ同様に「Person.m」から「Person.m」「Person+Show.m」「Person+ChangeName.m」の3ファイルにわけることができます。

クラスエクステンションとは

クラスエクステンションはカテゴリーと大変似ている機能です。 ただし、カテゴリーと違って自分が作ったクラスだけにしか適用できません。 (システムフレームワークのクラスには使えません。)

まず、クラスエクステンションを使わずにMonsterクラスを書いてみます。 ヘッダーファイル、実装ファイルはこんな感じになります。

// Monster.h
@interface Monster

+ (void)initMonster;
- (void)showMonster;
- (void)showMonster:(BOOL)animated;

@property  (readonly) NSString *hitpoint;
@end
// Monster.m
#import "Monster.h"
@implementation Monster

+ (void)initMonster{...}
- (void)showMonster{...}
- (void)showMonster:(BOOL)animated{...}

@end

上記に定義されているメソッドのうち、「initMonster」だけ外部から実行できて、「showMonster」と「showMonster:(BOOL)animated」はprivateなメソッドにしたい場合に、クラスエクステンションを使って次のように書くことができます。

// Monster.h
@interface Monster

+ (void)initMonster;

@property  (readonly) NSString *hitpoint;
@end
// Monster.m
#import "Monster.h"

@interface Monster ()
- (void)showMonster;
- (void)showMonster:(BOOL)animated;

@property  (readwrite) NSString *hitpoint;

@end

@implementation Monster

+ (void)initMonster{...}
- (void)showMonster{...}
- (void)showMonster:(BOOL)animated{...}

@end

「initMonster」だけヘッダーファイルに残して外部から見えるようになっていますが、「showMonster」と「showMonster:(BOOL)animated」は実装ファイルに移したので、「Monster.h」をインポートした他のクラスからは見ることができません。

クラスエクステンションの記述はカテゴリーに似ていますが、カテゴリー名がはいっているはずの「@interface Monster」の後ろの括弧に何もいれません。

propertyについてもメソッドと同様に隠すことができます。 上記のコードでは、は外部からhitpointを見るとreadonlyなpropertyですが、内部からみるとreadwriteのpropertyになっています。 (クラスエクステンションでは、カテゴリーと違いインスタンス変数やpropertyも使えます。)

上記のように、クラスエクステンションは主にプライベートなpropertyやメソッドを隠蔽するために使用します。 (最近はXcodeのプロジェクトテンプレートでファイルを生成するとデフォルトでクラスエクステンションが入っているので、使っている人も多いと思います。)

まとめ

  • カテゴリーは大きいクラスを分割したりするのに使えます
  • またカテゴリーは、既存クラスにメソッドを追加するのに使うと便利です
  • クラスエクステンションはプライベートなpropertyやメソッドを記述するのに便利です

参考資料