定数の自動読み込みと再読み込みを読んでいて、よくわからなかったことを調べてみました。この章はRubyとRailsの定数に関する挙動を説明していますが、今回はRubyの挙動について調べます。
$ ruby -v
ruby 2.2.0p0 (2014-12-25 revision 49005)
です。
はじめに
最近こそこそとRailsガイドの翻訳をお手伝いしています。自分自身で翻訳することはほとんどなくて、主に出来上がった翻訳のチェックをしています。この章は内容が難しいわりに例となるコードが少ないため、(翻訳が適切でも)意味がとりにくい箇所があります。そこで、コードを書きながら補足説明をしようと思い、まとめました。
2 定数更新
定数更新を読む上でのポイントは
- ネスト(Module.nestingの結果)は名前空間と一致しないことがある
- 相対定数(
A
)と修飾済み定数(A::B
)の違いは大きい - ブロックは特別
- トップレベルも変わり者
です。
2.1 ネスト
Ruby内のある場所のネストを調べるにはModule.nestingを使用します
とのことなので、Module.nesting
を使って、ネストを確認していきます。
ちなみに[A::B, X::Y]
のところはRailsのmasterでは修正されていて、
module X
module Y
end
end
module A
module B
end
end
module X::Y
module A::B
# (3)
# 例えばここで p Module.nestingすると[A::B, X::Y]が表示される
end
end
が正しいそうです。
ここではmodule A::B
が修飾済み定数か相対定数かでネストは変わります。
module X
module Y
end
end
module A
module B
end
end
module X::Y
module C
p Module.nesting
#=> [X::Y::C, X::Y]
end
end
module X::Y
module A::D
p Module.nesting
#=> [A::D, X::Y]
end
end
C
のように相対定数のときはcrefに属しているのに対して、A::D
のように修飾済み定数のときは独立しているものとして扱われます。X::Y::A::D
ではないんですよね。
特異クラスはclass << objectでオープンされるときにスタックにプッシュされ、後でスタックからポップされる。
class A
class << self
p Module.nesting
#=> [#<Class:A>, A]
end
p Module.nesting
#=> [A]
end
確かに特異クラスがプッシュされていますね。
*_evalで終わるメソッドが文字列を1つ引数に取って呼び出されると、そのレシーバの特異クラスはevalされたコードのネストにプッシュされる。
class A
end
A.class_eval("p Module.nesting")
#=> [A]
A.module_eval("p Module.nesting")
#=> [A]
A.instance_eval("p Module.nesting")
#=> [#<Class:A>]
A.new.instance_eval("p Module.nesting")
#=> [#<Class:#<A:0x007f94dc0c7150>>]
あれ、・・・
class A
end
A.class_eval("p Module.nesting.first.singleton_class?")
#=> false
A.module_eval("p Module.nesting.first.singleton_class?")
#=> false
A.instance_eval("p Module.nesting.first.singleton_class?")
#=> true
A.new.instance_eval("p Module.nesting.first.singleton_class?")
#=> true
class_eval
やmodule_eval
のときはレシーバをプッシュしているようですね(Railsのmasterでは修正されています)。
ちなみにブロックを渡したときはネストにプッシュされません。
class A
end
A.class_eval { p Module.nesting }
#=> []
A.module_eval { p Module.nesting }
#=> []
A.instance_eval { p Module.nesting }
#=> []
A.new.instance_eval { p Module.nesting }
#=> []
そのためブロック内で定数を定義しようとすると、トップレベルに定義されたりします。
class A
end
A.class_eval("FOO = 10")
class B
end
B.class_eval { BAR = 20 }
p A::FOO
#=> 10
p B::BAR
#=> const.rb:12: warning: toplevel constant BAR referenced by B::BAR
#=> 20
Kernel#loadによって解釈されるコードのトップレベルにあるネストは空になる。ただしload呼び出しが第2引数としてtrueという値を受け取る場合を除く。この値が指定されると、無名モジュールが新たに作成されてRubyによってスタックにプッシュされる。
# const2.rb
module A
p Module.nesting
end
p Module.nesting
load "const2.rb"
#=> [A]
#=> []
load "const2.rb", true
#=> [#<Module:0x007f8054137ac8>::A, #<Module:0x007f8054137ac8>]
#=> [#<Module:0x007f8054137ac8>]
たしかに無名モジュールが追加されていますね。
ブロックがスタックに何の影響も与えないという事実です。特に、Class.newやModule.newに渡される可能性のあるブロックは、newメソッドによって定義されるクラスやモジュールをネストにプッシュしません。
A = Class.new do
p Module.nesting
end
#=> []
これもたしかにプッシュしていません。
2.2 クラスやモジュールの定義とは定数への代入のこと
RubyはObjectにCという定数を作成し、その定数にクラスオブジェクトを保存します。
トップレベルで定義した定数はObject
の子として扱われます。詳しくはこちらもしくはこちらをみてください。
無名クラスや無名モジュールにひとたび名前が与えられた後は、定数とインスタンスで何が起きるかは問題ではありません。たとえば、定数を削除することもできますし、クラスオブジェクトを別の定数に代入したりどの定数にも保存しないでおくこともできます。名前はいったん設定された後は変化しなくなります。
まずは定数を削除してみます。削除してもKlass.name
は変わりません。
klass = Class.new
p klass.name
#=> nil
Klass = klass
p klass.name
#=> "Klass"
p Klass
#=> Klass
Object.send(:remove_const, :Klass)
p klass.name
#=> "Klass"
p Klass
#=> const.rb:10:in `<main>': uninitialized constant Klass (NameError)
次に他の定数にも代入してみます。p Klass2
がKlass
と表示されます。へぇー。
klass = Class.new
Klass = klass
Klass2 = klass
p Klass.name
#=> "Klass"
p Klass2.name
#=> "Klass"
p Klass
#=> Klass
p Klass2
#=> Klass
2.4 解決アルゴリズム
3. 見つからない場合、crefに対してconst_missingが呼び出される。const_missingのデフォルトの実装はNameErrorを発生するが、これはオーバーライド可能。
module A
def self.const_missing(name)
p "const_missing of A"
super
end
module B
p Module.nesting
#=> [A::B, A]
def self.const_missing(name)
p "const_missing of B"
super
end
C
#=> "const_missing of B"
#=> const.rb:10:in `const_missing': uninitialized constant A::B::C (NameError)
end
end
C
を呼び出した場所でのcrefはA::B
なので、たしかにB
のconst_missing
が呼ばれています。
2. 探索の結果何も見つからない場合、親のconst_missingが呼び出される。const_missingのデフォルトの実装はNameErrorを発生するが、これはオーバーライド可能。
特に、ネストが何の影響も与えていない点にご注意ください。また、モジュールは特別扱いされておらず、モジュール自身またはモジュールの先祖のどちらにも定数がない場合にはObjectはチェックされない点にもご注意ください。
E = "e"
module X
E = "xe"
end
module C
# E = "ce"
def self.const_missing(name)
p "const_missing of C"
super
end
end
module X
p Module.nesting
#=> [X]
p E
#=> "xe"
p C::E
#=> "const_missing of C"
#=> const.rb:27:in `const_missing': uninitialized constant C::E (NameError)
end
E
は相対定数なのでネストを順に探索し、X::E
を見つけています。一方でC::E
は修飾済み定数なのでC
もしくはC
の継承チェーンしか探索しません。そのため定数を発見できず、C
(親)のconst_missing
が呼ばれています。
おわりに
class_eval
にブロックを渡したときと文字列を渡したときで、どうしてネストが変わるんでしょう。どちらのケースもdef
をするとインスタンスメソッドが定義できる(つまりどちらもクラスのコンテキストにいる)のに。
class A
end
A.class_eval("def test; p 'test'; end")
A.class_eval do
def test2
p "test2"
end
end
A.new.test
#=> "test"
A.new.test2
#=> "test2"
rubyのコードを読んでみるとNODE_FL_CREF_PUSHED_BY_EVAL
というフラグが影響しているようです。
Module.nesting
ではNODE_FL_CREF_PUSHED_BY_EVAL
フラグのたっているcrefを無視しています。
# eval.c
static VALUE
rb_mod_nesting(void)
{
VALUE ary = rb_ary_new();
const NODE *cref = rb_vm_cref();
while (cref && cref->nd_next) {
VALUE klass = cref->nd_clss;
if (!(cref->flags & NODE_FL_CREF_PUSHED_BY_EVAL) &&
!NIL_P(klass)) {
rb_ary_push(ary, klass);
}
cref = cref->nd_next;
}
return ary;
}
ブロックを渡したclass_eval
ではpushしたcrefのNODE_FL_CREF_PUSHED_BY_EVAL
フラグを必ずたてるのに対して、文字列のときはSPECIAL_CONSTの時以外はNODE_FL_CREF_PUSHED_BY_EVAL
フラグをたてません。
#vm_eval.c
/* block eval under the class/module context */
static VALUE
yield_under(VALUE under, VALUE self, VALUE values)
{
rb_thread_t *th = GET_THREAD();
rb_block_t block, *blockptr;
NODE *cref;
if ((blockptr = VM_CF_BLOCK_PTR(th->cfp)) != 0) {
block = *blockptr;
block.self = self;
VM_CF_LEP(th->cfp)[0] = VM_ENVVAL_BLOCK_PTR(&block);
}
cref = vm_cref_push(th, under, NOEX_PUBLIC, blockptr);
cref->flags |= NODE_FL_CREF_PUSHED_BY_EVAL;
if (values == Qundef) {
return vm_yield_with_cref(th, 1, &self, cref);
}
else {
return vm_yield_with_cref(th, RARRAY_LENINT(values), RARRAY_CONST_PTR(values), cref);
}
}
/* string eval under the class/module context */
static VALUE
eval_under(VALUE under, VALUE self, VALUE src, VALUE file, int line)
{
NODE *cref = vm_cref_push(GET_THREAD(), under, NOEX_PUBLIC, NULL);
if (SPECIAL_CONST_P(self) && !NIL_P(under)) {
cref->flags |= NODE_FL_CREF_PUSHED_BY_EVAL;
}
SafeStringValue(src);
return eval_string_with_cref(self, src, Qnil, cref, file, line);
}
メソッド定義用のコンテキストと定数定義用のコンテキストを同じcrefで管理しながら、NODE_FL_CREF_PUSHED_BY_EVAL
フラグで調整しているのでしょうか。
あ、_eval
ファミリの修正PRがマージされました:D