Module#freezeの謎

これは読んでも役に立たない記事です。


RubyのModuleクラスは、Object#freezeを使わずにわざわざ独自にModule#freezeを実装しています。なんで?

Rubyソースコードを見ると、Module#freezeは関数rb_class_nameを呼んでからObject#freeze(関数rb_obj_freeze)を呼び出しています(ruby-1.9.1-p376のobject.c 1176行目)。

static VALUE
rb_mod_freeze(VALUE mod)
{
    rb_class_name(mod);
    return rb_obj_freeze(mod);
}

関数rb_class_nameを遡って関数rb_class_pathを見ると、次のような記述があります(ruby-1.9.1-p376のvariable.c 186行目)。また、関数rb_class_nameは、Module#to_sでも使われています。

VALUE
rb_class_path(VALUE klass)
{
    VALUE path = classname(klass);

    if (!NIL_P(path)) return path;
    if (RCLASS_IV_TBL(klass) && st_lookup(RCLASS_IV_TBL(klass),
					   tmp_classpath, &path)) {
	return path;
    }
    else {
(中略)
	rb_ivar_set(klass, tmp_classpath, path);

	return path;
    }

無名のモジュールやクラスでto_sが呼び出されると、文字列pathを作ってインスタンス変数tmp_classpathにとりあえず保存し、次にto_sが呼び出されたら、インスタンス変数の文字列を返す、という実装になっています。

Module#freezeがObject#freezeをそのまま呼び出す実装だと、凍結されたモジュールのto_sを呼び出したときにインスタンス変数への書き込みが発生し、エラーになってしまいます。なので、Module#freezeを新しく定義し、to_sの文字列を作っておいてからObject#freezeを呼ぶ、となっています。

次のコードでModule#freezeを削除すると、エラーが起きるのを確認できます。

Module.send :remove_method, :freeze
c = Class.new
c.freeze
c.to_s rescue p $!
#<RuntimeError: can't modify frozen object>