2012年8月29日水曜日

カテゴリでプロパティを実装する(関連参照による擬似インスタンス変数)

mファイルに@interface ClassName()と書くことを、「プライベートカテゴリ」とか「匿名カテゴリ」と呼びますが、多分正確な用語では「クラス拡張」と呼ばれるもので、カテゴリとはクラスの実装をカテゴライズして複数のファイルに分散する機能です。

この記事におけるカテゴリは、クラス拡張ではなく、本来の意味のカテゴリを指します。

カテゴリは実装を分散するだけでなく、既に存在するクラスに対して、継承することなく新たなメソッドを追加できる柔軟性を持っていますが、インスタンス変数を保持できない制約が存在します。

それが問題になることは通常では稀です。なぜなら、カテゴリがインスタンス変数を持てなくても、クラス本体の@privateを含めたインスタンス変数にアクセス可能だからです。

(もっとも、XCode4.4の今、privateメンバはクラス拡張でプロパティ宣言することが基本となり、@interfaceで@privateインスタンス変数を宣言することが減ったため、インスタンス変数にアクセスできるメリットはほぼないのですが…。当然、クラス拡張で宣言したプロパティの実体である、@implementationで宣言されるインスタンス変数は直接触ることはできません)

---

カテゴリでプロパティを宣言した場合、@sythesizeは使えません。synthesizeとはインスタンス変数の宣言とsetter/getterの実装を簡略化する糖衣構文に他ならないので、インスタンス変数を宣言できないカテゴリでは、@dynamicを宣言してsetter/getterを自分で書く必要があるのです。

クラス本体のインスタンス変数を使える場合は問題になりません。例えばクラス本体が弧度法(ラジアン)で角度のプロパティを持っているとき、それを度数法に変換して取得できるdynamicプロパティをカテゴリに記述したい、などのケースです。

---

もしカテゴリで実装したい振る舞いにデータの保持が必要で、しかし何らかの理由でクラス本体のヘッダや実装ファイルにインスタンス変数を追加することができない(例えばUKitのクラスにプロパティを持たせたいなど)場合、関連参照というテクニックが使えます。

関連参照については、Appleの公式ドキュメント、「Objective-C プログラミング言語」でもちゃんと解説されているのですが、初学の時点ではおそらく理解できないので、そのままになっているのかもしれません。

「OSX v10.6以降使用可能」とありますが、iOS5現在ちゃんと動作することを確認しています。

---

関連参照とは何かというと、オブジェクトと一意のIDをキーにして、値をストアする仕組みのことです。実質的には、既存クラスに擬似インスタンス変数を追加します。

その値はインスタンスが生存している間有効であることが保証され、オブジェクトを保持(retain)することができ、その場合、インスタンスが解放されると同時に自動的に解放(release)されます。

使い方は簡単です。

まずランタイム関数を使用するので、<objc/runtime.h>をインポートします。

値の設定に使うのはobjc_setAssociatedObjectです。

objc_setAssociatedObject (
   保持したいオブジェクト(selfでOK),
   キー(staticなNSStringでOK),
   値,
   関連ポリシー(プロパティの属性に合わせて選択)
);

保持したいオブジェクトは、プロパティでは自分自身なのでselfでOKです。インスタンスメソッドでのselfは、自身のオブジェクトを指します。(クラスメソッドでのselfはメタなクラスオブジェクトなので注意です)

一意なキーはNSStringをstaticで宣言します(NSStringでなくても、voidポインタならばなんでもOKです)。UITableViewCellのreuseIdentifierと同じような感じです。

値に保持したいデータを指定します。もしintや構造体などのプリミティブ型を保持したい場合は、NSNumberやNSValueでラップする必要があります。

関連ポリシーはプロパティのretain, nonatomicなどの属性に合わせて指定します。例えば通常のオブジェクトであればOBJC_ASSOCIATION_RETAIN_NONATOMIC、文字列であればOBJC_ASSOCIATION_COPY_NONATOMICです。

この関数を実行すると値が保持されます。

値を取得するには、objc_getAssociatedObject関数を使います。引数として、値を保持しているオブジェクトと、キーを指定します。

---

エアコードですがこんな感じでプロパティを実装できます。オブジェクトの保持も、破棄もランタイムが面倒を見てくれるので、すごく簡単です。

hogeClass+AdditionProperty.h

@property (retain, nonatomic) id fuga;

---

hogeClass+AdditionProperty.m

static NSString *theKey = @"fugaKey";

@dynamic fuga;

-(void)setFuga:(id)fuga
{
    objc_setAssociatedObject (
      self, theKey, fuga, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id)fuga
{
    return  objc_getAssociatedObject(self, theKey);
}

もし値を明示的に破棄したい場合にはnilをsetします。

ただしインスタンスと共に破棄されるので、その必要性に迫られることは稀でしょう。普通のインスタンス変数ですらMRCでは明示的にリリースする必要があることを考えると、擬似インスタンス変数ですが、部分的には本家を超えていますね…。

カテゴリのヘッダファイルもちゃんとimportしないと、警告が出たり、ドット構文でのアクセスができなかったりするので注意して下さい。

0 件のコメント:

コメントを投稿