これは…
いつまで続くかわからないシリーズである.
今回はテストを書くかは置いといて, class_eval でメソッドを定義した時の定数参照 (定数参照のスコープ) についてドキュメントを読んでも, イマイチ理解出来ないので色々と調べたことを書きなぐっていく.
class_eval
Module#class_eval はドキュメントによると, 以下のように記載されている.
モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。 モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。 ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。 文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。
以下, class_eval の利用例. 文字列としてメソッドを与えていている.
irb(main):001:0> class Cls; end => nil irb(main):002:0> Cls.class_eval %Q{ def foo; puts "aaaa"; end} => :foo irb(main):003:0> Cls.new.foo aaaa => nil
Cls クラス内で foo メソッドを定義するのと同義で, 普通にインスタンスメソッドとして呼び出すことが出来る. 以下も同じような感じ.
irb(main):001:0> class Cls; end => nil irb(main):002:0> Cls.class_eval do irb(main):003:1* def foo irb(main):004:2> puts "aaaa" irb(main):005:2> end irb(main):006:1> end => :foo irb(main):007:0> Cls.new.foo aaaa => nil
定数の参照について解らない
改めて class_eval は
class_eval は以下のようにメソッドの定義を文字列でも与えることが出来るのは先述の通りで, ブロックで与えることも可能である.
irb(main):001:0> class Cls1; end => nil irb(main):002:0> Cls1.class_eval "def foo; puts 'class_eval dayo'; end" => :foo irb(main):003:0> Cls1.new.foo class_eval dayo => nil
メソッドの定義方法によって変わる定数のスコープ
で, 以下のようなコードがあった場合, 定数 CONST 参照の可否がメソッドの定義方法によって変わるという挙動の理由がイマイチぴんと来ていない.
# パターン 1 class Cls CONST = 'class_eval dayo' end module Mod Cls.class_eval "def foo; CONST; end" end # パターン 2 class Cls CONST = 'class_eval dayo' end module Mod Cls.class_eval do def foo CONST end end end
パターン 1 とパターン 2 をそれぞれ, irb で実行してみる.
# パターン 1 メソッドを文字列として与えている irb(main):001:0> class Cls irb(main):002:1> CONST = 'class_eval dayo' irb(main):003:1> end => "class_eval dayo" irb(main):004:0> irb(main):005:0* module Mod irb(main):006:1> Cls.class_eval "def foo; CONST; end" irb(main):007:1> end => :foo irb(main):008:0> Cls.new.foo => "class_eval dayo" # パターン 2 ブロックでメソッドを与えている irb(main):001:0> class Cls irb(main):002:1> CONST = 'class_eval dayo' irb(main):003:1> end => "class_eval dayo" irb(main):004:0> irb(main):005:0* module Mod irb(main):006:1> Cls.class_eval do irb(main):007:2* def foo irb(main):008:3> CONST irb(main):009:3> end irb(main):010:2> end irb(main):011:1> end => :foo irb(main):012:0> Cls.new.foo NameError: uninitialized constant Mod::CONST
以下のような違いを確認している.
- メソッドを文字列で与えた場合には, Cls 内で定義している定数を参照出来ている
- ブロックでメソッドを与えた場合には, Mod 内の定数を参照しようとして NameError 例外となっている
そんなものなのか…で終わらすのもアレなので, 改めて, ドキュメントの一部を引用.
文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。
んー, 自分の読解力の無さを恨みたいけど, 以下のような解釈で良いのかな…(自信無い
- メソッドをブロックで定義した場合には, class_eval ブロックの外側, 上記の例だと, module 内で定数, クラス変数を探索する
- メソッドを文字列で定義した場合には, class_eval で定義するメソッドを定義するクラス内の定数, クラス変数を探索する
んんんー, 解らないというか, なんだろうこのモヤモヤは…
特に, 以下の部分が解らない…
定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。
一応, テストも書く
以下のようなテストを書いた.
# file name: 12.rb require 'minitest/autorun' require "minitest/reporters" Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new] class Cls CONST = 'class_eval dayo' end module Mod Cls.class_eval "def foo; CONST; end" end module Mod Cls.class_eval do def bar CONST end end end class GakushuTest < Minitest::Test def test_const_reference_1 assert_equal Cls.new.foo, 'class_eval dayo' end def test_const_reference_2 assert_raises NameError do Cls.new.bar end end end
Cls.new.foo
は文字列で Cls クラスにメソッドを定義している為, 定数は参照出来るので, 定数の内容が返ってくることを期待する. Cls.new.bar
はブロックで定義されたメソッドを呼び出していて, 定数のスコープは Cls クラスを外れる為, NameError 例外が発生することを期待している.
テストを実行すると, 以下のように出力される.
$ bundle exec ruby 12.rb Started with run options --seed 32749 GakushuTest test_const_reference_2 PASS (0.00s) test_const_reference_1 PASS (0.00s) Finished in 0.00102s 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
一応, いい感じ.
以上
まだモヤモヤが治まらないけど, メモでした.
元記事はこちら
「Ruby の組み込みライブラリ (クラス) の「学習テスト」を書いて, 出来るだけ多くのメソッドと出会いたい (4) 〜 class_eval 定数参照が解らない ~」