クラスとモジュール

特異クラス

「オブジェクト」の項ではクラスオブジェクトのことに少しだけふれた。 ここではさらにその先へ進んでいく。まずはもう一度 struct RClassRBasic を見てみよう。

struct RClass {
    struct RBasic basic;
    struct st_table *iv_tbl;
    struct st_table *m_tbl;
    VALUE super;
};

struct RBasic {
    unsigned long flags;
    VALUE klass;
};

(ruby.h)

struct RClassstruct RBasic を要素に持つので、 RClass.basic.klass がある。ここには何が入っているのだろうか? つ まりオブジェクト Class のクラスは? それが「特異クラス」というもの だ。英語ならメタクラス。特異クラスと Class の関係は、以下のよう な図を見るとわかりやすい。


Ruby class hierarchy
origined from the object.c
                                      +--------+
                                      |        |
                            Object ---|---> (Object) --->
                            ^  ^      |      ^ ^
                            |  |      |      | |
     +----------------------+  |      |      | |
     |            +------------|------|------+ |
     |            |            |      |        |
 AnyClass --> (AnyClass)-------|------|--------|-------->
                               |      |        |              Class
 AnyModule -> (AnyModule)-> Module ---|---> (Module) --->
                               ^      |        ^
                               |      |        |
                               |      |        |
                               |      |        |       
                             Class ---|---> (Class)----->
                               ^      |
                               |      |
                               +------+

かっこつきのものが特異クラスだ。太い縦の矢印が継承関係で、細い 横の矢印がクラス──インスタンス関係である(矢印側がクラス)。重要な 点は二つだ。

  1. 全てのクラスオブジェクトはそれと一対一に対応する特異クラスを持つ
  2. 全ての特異クラスは Class の下位クラスであり同時にインスタンスである

リファレンスマニュアルの Class の項には 「より正確に言えば、…(略)…Class はそのメタクラスのクラスです」 と書いてあるのだが、それはこの点を指して言っているわけだ。

またこの図にはもう一つ 「クラスオブジェクトの特異クラスはクラスに対応して継承する」 という重要な特徴が書かれているのだが、これについてはあとで触れる。

特異クラスの生成

ではとりあえず「特異クラス」なるものの正体を見よう。特異クラスを 作る関数は rb_singleton_class_new() だ。

VALUE
rb_singleton_class_new(super)
    VALUE super;
{
    VALUE klass = rb_class_new(super);

    FL_SET(klass, FL_SINGLETON);
    return klass;
}

(class.c)

rb_class_new()Class のインスタンスを生成する関数だから、普通の クラスと特異クラスの違いは FL_SET(...) だけということになる。 FL_SETRBasicflags をセットするマクロなので、つまり実装の観 点からは特異クラスとは FL_SINGLETON フラグがセットされたクラスだと 言うことができる。

スクリプトから特異クラスにアクセスする

さてこれまでの知識から考えると Class.type(Class) を返してくれる はずだが、実際に試してみると普通の(特異クラスではない)Class を返す。 これはなぜかというと、メソッド type が特異クラスをスキップするように なっているからである。普通のメソッドは特異クラスを隠蔽するようになって おり、普通にスクリプトを書いている限りなかなか特異クラスにはお目に かかれない。 だが、唯一それが露出している部分がある。それは特異クラス定義文だ。

class << Object
  self
end

Rubyスクリプト実行中は、コードが書けるところならば必ずメソッドのデ フォルトのレシーバ(self)が存在する。クラス定義中はそのクラスが self になるので、特異クラス定義中は self が特異クラスになっている わけだ。例えば上の例の self(Object) だ。ただし to_sname で 見ても、type の場合と同じように特異クラスがスキップされて "Object" になってしまうので違いがわからない。__id__ を使って確かめよう。

では任意の特異クラスを入手するにはどうすればよいだろうか。あまり知 られていないことだが実はクラス定義にも返り値がある。スコープの最後 の式の値が特異クラス定義の返り値なので、次のようにすれば特異クラス を返すメソッドが定義できる。

def singleton_class_of( obj )
  class << obj; self end
end

p singleton_class_of( Object.new ).__id__
p Object.__id__

ただし特異クラス定義の構文は開始した時点で問答無用で特異クラスを作る ため、引数に渡されたオブジェクトすべてに特異クラスを導入してしまう。 あくまで実験して楽しむ範囲にとどめておき、実際に活用したりしないほうがよい。

特異メソッド

特異クラスはそもそも特異メソッドの実装のために作られたものだ。 ここでは実際に特異メソッドが定義される過程を見て、特異クラスについ てさらに理解を進めてゆこう。

まず最初は穏当に rb_define_singleton_method() から始める。

void
rb_define_singleton_method(obj, name, func, argc)
    VALUE obj;
    const char *name;
    VALUE (*func)();
    int argc;
{
    rb_define_method(rb_singleton_class(obj), name, func, argc);
}

(class.c)

rb_define_method() は拡張モジュールを定義するのにも使うごく普通の 関数だから、どうやら rb_singleton_class() というのがキモのようだ。 この関数はすぐ上に定義されている。

VALUE
rb_singleton_class(obj)
    VALUE obj;
{
    if (rb_special_const_p(obj)) {
        rb_raise(rb_eTypeError, "can't define singleton");
    }
    if (FL_TEST(RBASIC(obj)->klass, FL_SINGLETON)) {
        return RBASIC(obj)->klass;
    }
    RBASIC(obj)->klass = rb_singleton_class_new(RBASIC(obj)->klass);
    rb_singleton_class_attached(RBASIC(obj)->klass, obj);
    return RBASIC(obj)->klass;
}

(class.c)

rb_special_const_p()util.c にある関数で、VALUE が即値(ポインタ でない)であるオブジェクトすなわち nilFixnum でに対して真を返す。 これを弾いているのは、つくりを考えれば明らかだろう。対応する構造体 がなければ struct RBasic がないわけで、特異クラスを格納できない。

ということはしかし逆に言うと BignumFloatOK ってことになる。 もっともそんなことをしてもあまり役に立ちそうにはないが……

次の if でオブジェクトのクラスにフラグ FL_SINGLETON が立っているか (特異クラスかどうか)テストして、特異クラスだったらそれをそのまま返 す。そうでなかったら、rb_singleton_class_new() で新しく特異クラス を作成し、同時にそれをオブジェクトのクラスにする。このとき、特異ク ラスは現在のクラスの下位クラスとして作られるので、それまでに定義さ れているメソッドは自動的に継承されるわけだ。そのあとの rb_singleton_class_attached() はなんだろうか? 見てみよう。

void
rb_singleton_class_attached(klass, obj)
    VALUE klass, obj;
{
    if (FL_TEST(klass, FL_SINGLETON)) {
        if (!RCLASS(klass)->iv_tbl) {
            RCLASS(klass)->iv_tbl = st_init_numtable();
        }
        st_insert(RCLASS(klass)->iv_tbl, rb_intern("__attached__"), obj);
    }
}

(class.c)

フラグ FL_SINGLETON が立っているのが特異クラスだったから、この場合 最初の if は真だ。どういうときに偽になるのかわからなかったので 検索してみたところ、最新のバージョンだと nilfalse に特異メソッドを 定義できるようになっていて、そのときに偽になるようだ。

さて、if が真になったときは、特異クラスオブジェクトのインスタンス変数 __attached__ にそのインスタンスを代入している。実は暗黙のルールと して「特異クラスはひとつしかインスタンスを持たない」ことになっている ので、特異クラスとそのインスタンスは一対一に関連付けできるのだ。その 対応がここでなされている。

また __attached__ は「@」がついていないが、インスタンス変数テーブルに 格納されるのでいちおうインスタンス変数ということになる。このような インスタンス変数は Ruby スクリプトレベルからは絶対に参照できない 変数だ。

一方 __attached__ を使っているのは主に eval.c のようだ。特異メソッドが 削除されたり追加されたときにフックメソッドを(インスタンスに対して) 呼んだり、クラス変数を実装するために使ったりしている。

まとめると、rb_singleton_class() は、あるオブジェクトの特異クラスを (必要なら作成して)返すということがわかる。そのあとは通常通りの rb_define_method() で特異クラスにメソッドを定義すれば完了となる。 つまり特異メソッドが普通のメソッドと違うところは定義されるクラスだ けでメソッド側になんら変わりはない。そして特異クラスの定義とは「あ るインスタンス専用のクラスを普通のクラスとの間に一枚はさむ」作業だ と総括できる。

クラスツリーの初期化

Ruby には rb_define_class()class.c)など、クラスを定義するための 関数がいろいろと用意されているが、この初期状態を作るのにはそれらの 関数は使えない。というのは、クラスを作るためにはその特異クラスを作 る必要があるが、特異クラスを作るためには Class が必要だからだ。そ こでまず最初は関数 boot_defclass()ObjectModuleClass を作成 する。

static VALUE
boot_defclass(name, super)
    char *name;
    VALUE super;
{
    extern st_table *rb_class_tbl;
    VALUE obj = rb_class_new(super);
    ID id = rb_intern(name);

    rb_name_class(obj, id);
    st_add_direct(rb_class_tbl, id, obj);
    return obj;
}

(object.c)

rb_name_class()name は動詞「命名する」で、引数のクラスオブジェクト のインスタンス変数__classid__ にクラス名の ID をセットする(これもやは りスクリプトからは見えない変数)。また st_add_direct()ID からクラ スオブジェクトの対応を記憶する。この二つで ID とクラスオブジェクトの双 方向変換ができるようになる。

あとは rb_singleton_class_new()(Object) (Module) (Class) を作り、 直接 RClass 構造体をいじってセットすれば初期状態が完成する。

通常クラスの定義

通常のクラス(Object Module Class でも特異クラスでもないクラス)は すべて rb_define_class()rb_define_class_id()rb_define_class_under() で定義する。..._idrb_define_class() の 文字列のかわりに ID を使うもの。..._under はクラスやモジュールの「下」 にクラスを作るためのもの。..._class..._under はほとんど ..._id を呼んでいるだけなので、これを見てみよう。

VALUE
rb_define_class_id(id, super)
    ID id;
    VALUE super;
{
    VALUE klass;

    if (!super) super = rb_cObject;
    klass = rb_class_new(super);
    rb_name_class(klass, id);
    /* 特異クラスを作る */
    RBASIC(klass)->klass = rb_singleton_class_new(RBASIC(super)->klass);
    rb_singleton_class_attached(RBASIC(klass)->klass, klass);
    rb_funcall(super, rb_intern("inherited"), 1, klass);

    return klass;
}

(class.c)

rb_class_new()rb_name_class()boot_defclass() と同じ。 そのあとが問題だ。前述したとおりクラスのクラスは特異クラスなの だが、その特異クラスは、スーパークラスのクラス、つまり特異クラスだ。 どういうことかと言うと、クラス B (< A) を定義すると B のクラスは (B) だが、(B)(A) の下位クラスになる継承しているということだ。つまり 「クラスの特異クラスは、クラスの継承と同じように対応して継承する」 ということになる。まったく同じクラスツリーが、クラスと、そのクラスの 両方で形成されるのだ。これは最初のほうで見せたクラス関係図を見ると よくわかる。

これは普通の特異メソッド定義では起きない現象で、クラスの特異クラスの 場合だけ起こる。なんのためにこんなことをするかというと、実は クラスの特異メソッドを継承させるためである。最もわかり やすいのは Object.new であろう。Object.new は特異メソッドなので単純に 継承しただけでは SomeClass.new が定義されない。これでは不便だ。 そういうわけでクラスの特異クラスだけはクラスツリーと連動するように なっているのである。

それと最後の rb_funcall() は Ruby のメソッドを呼ぶ関数で、ここでは Class#inheritedklass とと もに呼んでいる。こんなもん何に使うのかなぁ、と思ったのだが……使ってた。 rb_class_s_inheritedobject.c)だ。Class の特異メソッド(すなわち (Class) のメソッド)で、Class が継承されたら例外を発生するようになって いる。ということは、このメソッドを再定義すれば Class の下位クラスを作 れるわけだ。やろう。

def Class.inherited( klass )
end

class AClass < Class
end

p AClass.ancestors

だからどうってこともないけど、禁止されてるとやりたくなるんだよね。

インクルード

ところで、これまでモジュール(Module クラスのインスタンス)がほとんど 出てこなかった。ModuleClass の上位クラスなのに、クラスのほうがたくさん 出てきて偉そうだ。これからモジュールの特徴であるインクルードについて 調べよう。

オブジェクトの章では、それぞれのオブジェクトが ポインタ(VALUE)と構造体の組で表されること、その構造体には自身の クラスを格納するメンバ value->basic->klass があることを知った。 klass はただひとつしかなかったので、Ruby は単一継承らしいとわかる。 しかし実はモジュールのインクルードは概念的には多重継承であり、イン クルードの際に多少加工して単一継承のように見せかけているにすぎない。

では早速その様子を見てみよう。インクルード操作の実体は rb_include_module() だ。……ただし 本当に正確に言うと、rb_mod_inclucde() (eval.c)include の実体で、 そこから Module#append_features が呼ばれ、そのデフォルトの実装が rb_include_module() を呼ぶようになっている。 やや長いので、少しづつ。最初にいろいろとチェック部分があって、

void
rb_include_module(klass, module)
    VALUE klass, module;
{
    VALUE p;
    int changed = 0;

    rb_frozen_class_p(klass);
    if (!OBJ_TAINTED(klass)) {
        rb_secure(4);
    }

    if (NIL_P(module)) return;
    if (klass == module) return;

    switch (TYPE(module)) {
      case T_MODULE:
      case T_CLASS:
      case T_ICLASS:
        break;
      default:
        Check_Type(module, T_MODULE);
    }

以下が本処理。klass に、module をインクルードする。

    OBJ_INFECT(klass, module);
    while (module) {
        /* 既にスーパークラスで module をインクルードしていたらスキップする */
        for (p = RCLASS(klass)->super; p; p = RCLASS(p)->super) {
            if (BUILTIN_TYPE(p) == T_ICLASS &&
                RCLASS(p)->m_tbl == RCLASS(module)->m_tbl) {
                if (RCLASS(module)->super) {
                    rb_include_module(p, RCLASS(module)->super);
                }
                if (changed) rb_clear_cache();
                return;
            }
        }
/*A*/   RCLASS(klass)->super = include_class_new(module, RCLASS(klass)->super);
/*B*/   klass = RCLASS(klass)->super;
        module = RCLASS(module)->super;
        changed = 1;
    }
    if (changed) rb_clear_cache();
}

(class.c)

最初の for 文がやっていることはコメントに書いてあるので、まずは 読みとばそう。その次を読むと、どうやらインクルード対象のクラスのスーパークラスを 差し替えているようである(A)。さらにそのあとは、差し替えたばかりの クラスと modulesuper に対して繰り返しているようだ。

modulesuper に入っているのは、module にインクルードした モジュールだろう(勘)。ということは B の部分はただのループ カウンタのインクリメントにすぎないので、Ainclude_class_new() が 重要なようだ。

static VALUE
include_class_new(module, super)
    VALUE module, super;
{
    NEWOBJ(klass, struct RClass);                 /* A */
    OBJSETUP(klass, rb_cClass, T_ICLASS);

    if (!RCLASS(module)->iv_tbl) {                /* B */
        RCLASS(module)->iv_tbl = st_init_numtable();
    }
    klass->iv_tbl = RCLASS(module)->iv_tbl;
    klass->m_tbl = RCLASS(module)->m_tbl;
    klass->super = super;                          /* C */
    if (TYPE(module) == T_ICLASS) {                /* D */
        RBASIC(klass)->klass = RBASIC(module)->klass;
    }
    else {
        RBASIC(klass)->klass = module;
    }
    OBJ_INFECT(klass, module);
    OBJ_INFECT(klass, super);

    return (VALUE)klass;
}

まず新しいクラスを作り(A)、続いて module のインスタンス変数 テーブル・メソッドテーブルをそれに移植(B)、スーパークラスを これまでのスーパークラス(super)にする(C)。つまり、module の 「化身」になるクラスを作って返すようだ。 また A のところでは構造体フラグを T_ICLASS というのにしており、 これが化身クラスの印らしい。ICLASSIinclude だろうか?

さらに D は、ちょっとわかりにくいのだが、rb_include_module に あった while ループの二回目以降に対処している。一回目は moduleT_ICLASS ではない(T_MODULE だ)ので RBASIC(klass)->klass には その実体のモジュールがセットされる(if の偽側)。二回目以降は module は最初の module にインクルードされているモジュールだから、 それはすでに T_ICLASS 化されているはずで、その RBASIC(klass)->klass には 実体がセットされているはずである。なのでそれを取り出して使う(if の真側)。

具体例で見よう。まず m1m2 をインクルードすると

im2 ---- m2
 |
m1

となるだろう。ここから c1m1 をインクルードすると……

parent    im2 ---- m2
 |         |
c1        m1

まず一回目のループ (module = m1) で以下のように変わり、

parent   im2 ---- m2
 |        |
im1 ---- m1
 |
c1                  

二回目のループ (module = im2) でこうなる。

parent
 |
im2' ---------------------- m2
 |                 im2 ----
 |                  |
im1 -------------- m1
 |
c1

さてここまでくると rb_include_module() の読み飛ばしていた部分が 理解できる。

/* 既にスーパークラスで module をインクルードしていたらスキップする */
for (p = RCLASS(klass)->super; p; p = RCLASS(p)->super) {
    if (BUILTIN_TYPE(p) == T_ICLASS &&
        RCLASS(p)->m_tbl == RCLASS(module)->m_tbl) {
        if (RCLASS(module)->super) {
            rb_include_module(p, RCLASS(module)->super);
        }
        if (changed) rb_clear_cache();
        return;
    }
}

親(p)のうち T_ICLASS で(つまりモジュールで)同じメソッドテーブルを 持つものがあったらそれは同じモジュールを二回インクルードしたという ことなのでスキップする。ただしそのモジュールがインクルードしている モジュールがあったらそっちも再確認する、というわけだ。

p はすでにインクルードしたモジュールなのだから、それにインクルード されたモジュールもすでにインクルードしているはず……と一瞬思ったが、 以下のような状況がありうる。

module M1; end
module M2; end
class C
  include M2   # まだ M1 はインクルードされてない
end

module M2
  include M1
end
class C
  include M2   # 今度はインクルードされている!
end

逆に言うと、include は「リアルタイム」でない。つまり、include した 結果が即座に反映しないことがある。

また以上のような仕組みなので、モジュールの特異メソッドはインクルード 先クラスオブジェクト(またはモジュール)に継承しない。継承させるには 特異クラスもいっしょにつなぎかえないといけないが、それはやっていな かったからだ。というわけで特異メソッドを定義したいときは Module#append_features をオーバーライドするのが常套手段だ。

もうひとつ考えることがある。どうやら RBasic(iclass)->klass は単に 実体のモジュールを示すためだけに使われているようなので、ICLASS に 対してメソッドが呼ばれたりする状況は非常にまずい。だから ICLASS は絶対にスクリプトから見えてはならないはずである。そして実際 Object#type など全てのメソッドは ICLASS をスキップする。特異クラス の場合のような例外的な手段もない。