クラスローカルインスタンス変数

1.9 で最近導入されたクラスローカルインスタンス変数について少し調べてみた。

クラスローカルインスタンス変数とは、C++ のプライベート変数のようなもので、サブクラスから参照することができない。

現在の 1.9 の実装では @_ で始まるクラス変数がクラスローカルインスタンス変数と判断される。@@ に引き続き、先頭の2文字で判断される変数の種類ですな。
@_ については、互換性の問題もあって最終決定はされてないようだ。
たしかに、@_ で始まる変数は巷にゴロゴロ転がっていそうだ。参考までに自分の環境で @_ で始まる変数の数(正確には出現数)を数えてみたら 195 あった。あまりライブラリをインストールしていない我が家のシステムでこれだから、世間一般では @_ で始まる変数を使っているプログラムはかなり多いと思われる。
大抵のコードが「内部インスタンス変数」のつもりで使っていると想像されるから、問題はないような気もするが、実際のところ、この変更で問題を起こすプログラムはどのくらいあるだろう。

さて、クラスローカルインスタンス変数の実装について。
概念的には、@_ で始まる変数名をクラスで修飾することによって、クラス内部からのみのアクセスを保証している。他のクラスからアクセスしようとすると、クラスが違うのでアクセスできないという仕組みだ。
ruby 1.9 では、以下のようなコードで確認できる。

class C
  def initialize
    @_a = "Hello"
  end
end

p C.new.instance_variables   # [:@_a/C], 1.8 では ["@_a"]

1.9 では、変数名 @_a の後にスラッシュ(/) に続いてクラス名 C が付加されている。
今回の話題とは関係ないが、Object#instance_variables の返り値も文字列の配列からシンボルの配列に変更されているようだ。


関連してそうな ruby のコードを読んでみる。

/**
  @c variable
  @e set instance variable id of obj as val.
     if is_local is not 0, search as class local variable.
  @j obj のインスタンス変数を val にする。
     もし is_local が !0 ならクラスローカル変数を得る
 */
DEFINE_INSN
setinstancevariable
(ID id, num_t is_local)
(VALUE val)
()
{
    if (is_local) {
	id = rb_compose_ivar2(id, eval_get_cvar_base(th, GET_ISEQ()));
    }
    rb_ivar_set(GET_SELF(), id, val);
}

(insns.def)

コメントから察するに、 is_local が真の場合がクラスローカルインスタンス変数のようだ。実際にデバッガでも確認した。

setinstancevariable の引数の id は、変数 (例:@_a) を表わす。rb_compose_ivar2() は、この id と eval_get_cvar_base() から返されるクラスを引数に取って、新たな id を返す。
そして、この id が、オブジェクトのインスタンス変数テーブルに値を格納するのに使われているようだ。
この最後のステップは普通のインスタンス変数の場合と同じなので、変数名をクラスで修飾するという動作は、rb_compose_ivar2() の中で行われているはずだ。rb_compose_ivar2() の中身は以下のとおり

ID
rb_compose_ivar2(ID oid, VALUE klass)
{
    struct ivar2_key key, *kp;
    ID id;

    key.id = oid;
    key.klass = klass;
    if (st_lookup(global_symbols.ivar2_id, (st_data_t)&key, (st_data_t *)&id))
        return id;

    kp = ALLOC_N(struct ivar2_key, 1);
    kp->id = oid; kp->klass = klass;
    id = ID_INSTANCE2;
    id |= ++global_symbols.last_id << ID_SCOPE_SHIFT;
    st_add_direct(global_symbols.ivar2_id, (st_data_t)kp, (st_data_t)id);
    st_add_direct(global_symbols.id_ivar2, (st_data_t)id, (st_data_t)kp);
    return id;
}

(parse.y)

コードがやっていることをかいつまんでみると、

1. 引数として渡された id と klass をキーとして global_symbols.ivar2_id テーブルを調べる。要素がすでにあれば、その値 (id/key に対応する新たな id) を返す。
2. 要素がなければid と klass の組を表わす新たな id を作成。新たな id は global_symbols.last_id を加工して作成。
3. global_symbols.ivar2_id テーブルに、id/klass から新 id を検索するためのデータを格納。global_symbols.id_ivar2 テーブルに逆引きのためのデータを格納。
4. 新たに作られた id を返す。

つまり、変数の id と klass の組に対応する ID を作って、その ID をキーとしてインスタンス変数テーブルに格納することによって、クラス内部でしか参照できないインスタンス変数を実現しているらしい。

その klass は関数 eval_get_cvar_base() で得ている。この関数についてはまた後日あらためて。