【コラム】

ダイナミックObjective-C

31 ランタイムAPIでさらに動的に(5) - インスタンス変数に動的にアクセス

    木下誠  [2006/04/05]

    前回、Class構造体のobjc_ivar_listを調査することで、そのクラスが持つインスタンス変数の名前を調べることを説明した。今回は、ここで得られた名前をもとにして、インスタンス変数の値にアクセスする方法を説明しよう。

    動的なインスタンス変数へのアクセス

    Objective-Cでのインスタンス変数のアクセス権は、だいたいほかのメジャーな言語と同じであり、@private、@proteced、@publicという3つの可視性で定義することができる。@publicを指定すれば、「->」を使って、構造体と同じようにアクセスできる。

    また、クラスを構造体とみなす、@defsという指示子も用意されている。これを使うと、クラスの宣言で@protectedや@privateを使っていても、どこからでもアクセスできてしまう。たとえば、NSWindowクラスに対して、次のように@defsを使って構造体の定義を行う。

    struct ns_window {
        @defs(NSWindow);
    };

    これで、NSWindowのオブジェクトを(struct ns_window*)でキャストすることで、すべてのインスタンス変数に「->」を使ってアクセスできてしまう。

    だが、これらはどちらも、コンパイルの時点で変数名が分かっている場合のアクセス方法である。ここでは、実行時に動的に名前を指定して、インスタンス変数にアクセスする方法を紹介しよう。

    いつものように、ランタイムAPIを使う。オブジェクトから、インスタンス変数を取得するのがobject_getInstanceVariable、設定するのがobject_setInstanceVariableだ。

    objc-class.h

    OBJC_EXPORT Ivar object_getInstanceVariable(id object, const char *name, void **value);
    OBJC_EXPORT Ivar object_setInstanceVariable(id object, const char *name, void *value);

    第一引数が対象となるオブジェクト、第二引数はインスタンス変数の名前だ。最後の引数に、取得するときは変数のアドレス、設定するときは変数の値を指定する。どちらの場合も、あらかじめ変数の大きさに合わせてメモリを確保しておく必要がある。

    インスタンス変数の隠蔽は不可能か?

    ところで、このようにどこからでもインスタンス変数に好き勝手にアクセスできるとなると、不安を感じる向きもあるかもしれない。オブジェクト指向の文脈では、オブジェクトのカプセル化や、情報の隠蔽が重要な要素として挙げられることが多い。@defsやランタイムAPIは、これらのことをわざわざ壊しているのではないだろうか。

    正直なところ、筆者もそのように思う。Objective-Cの言語仕様をつぶさに見ていくと、その根底にある姿勢として、「言語として制約を与える」というところが極端に少ない、と感じることがある。インスタンス変数にしても、クラスの継承関係にしても、実行中に触り放題である。

    このような言語では、オブジェクト指向の方法論として語られる情報隠蔽を実践するのは、言語レベルではなく、プログラムレベルということになるだろう。Objective-Cは、言語としてクラスやメソッドといったオブジェクト指向の道具立てを用意するが、それはかなり自由に使える状態にある。プログラマはそれを使い、ある箇所では継承やカプセル化に基づく抽象度の高いオブジェクト指向でプログラミングし、ある箇所ではスピードを最大限に出すためC言語に近いプログラミングをする。

    このような使い分けができる訳である。この辺りが、Objective-Cが自由で動的な言語と呼ばれることもあれば、いいかげんな言語と揶揄されたりすることもある所以であろう。

    サンプル

    では、ランタイムAPIを使ってインスタンス変数にアクセスする例を紹介しよう。

    Cocoaでは、ウィンドウを表すNSWindowというクラスがあり、このオブジェクトからはビューのルートをcontentViewで取得できる。このルートには、実はFrameViewという呼ばれるさらに親がある。FrameViewは、titleCellというインスタンス変数を持ち、これはウィンドウのタイトルを表示するときに使われるセルである。

    そこで、このtitleCellを取得して、ウィンドウのタイトルに使われているフォントを変更してみよう。

        // NSFrameViewを取得する
        id frameView;
        frameView = [[_window contentView] superview];
        
        // titleCellインスタンス変数を取得する
        NSCell* titleCell;
        object_getInstanceVariable(frameView, "titleCell", &titleCell);
        
        // フォントを設定する
        [titleCell setFont:[NSFont fontWithName:@"Impact" size:14.0f]];

    titleCellはヘッダには定義されていないので、object_getInstanceVariableを使って動的に取得している。このコードの動作結果は、次の図のようになる。

    ウィンドウタイトルのフォントを変更

    このコードの優れている点は、インスタンス変数を動的に調査しているため、仮にtitleCellというインスタンス変数が無くとも、クラッシュはしないことである。もちろん、フォントの変更は機能しないが、クラッシュしたり起動できなかったりという、最悪の事態は避けられるのである。

    動的にインスタンス変数を追加するときの問題点

    今までの流れでいくと、次はインスタンス変数の動的な追加ということになる。しかし、これは大きな問題をはらむ。

    理屈としては、追加することは可能である。新しいobjc_ivar_listを作成して、Class定義で置き換えてやればいい。だが、これにはいくつかの問題がある。

    1つは、インスタンス変数の宣言は、オブジェクトをallocしたときのサイズを定義している点である。仮に動作の途中でインスタンス変数の宣言を追加したとしても、すでにインスタンス化されたオブジェクトが確保しているメモリは増えない。したがって、同じクラスのインスタンスなのにサイズが違うという、つまり追加したインスタンス変数を持つものと持たないものがあるという、危険な状態が生じる。

    もう1つは、継承関係の途中になるクラスに追加するときである。インスタンス変数を宣言するときは、その変数がメモリ上のどこに確保されるか、オフセットを指定しなくてはいけない。通常、サブクラスは親クラスと被らないようにオフセットを割り当てていく。だが、親クラスでインスタンス変数を追加すると、サブクラスの変数と被ってしまう危険が出てくる。

    このようなことを考えると、インスタンス変数の動的な追加は、まだインスタンスを作成しておらず、継承関係の一番最後、つまりサブクラスが存在しないクラスに限る、ということになってしまう。これではあまりに限定された使い方になってしまうので、この記事ではインスタンス変数の動的な追加はお勧めしない、ということを述べるにとどめておこう。

    新着記事

    特設サイトの情報

      人気記事

      一覧

        イチオシ記事

        新着記事

        特別企画

        マイナビニュースマガジン