Class定義
指定したクラスのコンテキストでブロックを評価するclass_eval
というメソッドがある。
これはクラスに新しいメソッドを追加したりするのに用いることが出来る。すでに全く同じようなメソッドinstance_eval
も学習している。
instance_evalはブロックを使って、クラスやメソッドの中のローカル変数やプライベート変数にアクセスすることが出来る。ブロックの中で、指定したクラスやメソッドのレシーバーをselfにできるため、あたかもクラスやメソッドの内部のコンテキストにいるように感じられる。
ブロック - Pyons Tech Blog
instance_evalの場合はself
を指定したクラス内やオブジェクト内に指定できるのが売りだった。しかしこれでは、クラスに対する新しいメソッドの追加は出来ない。
class MyClass def initialize @v = 1 end end MyClass.instance_eval do def show_number return @v * 10 end end obj2 = MyClass.new obj2.show_number # undefined method `show_number' for #<MyClass:0x00007fffba166ec8 @v=1> (NoMethodError)
selfをクラス内に変更すれば、新しいメソッドの宣言もできそうなもので、出来なかった。
これはclass_eval
を用いて解決できる。
lass MyClass def initialize @v = 1 end end MyClass.class_eval do def show_number return @v + 10 end end obj = MyClass.new p obj.show_number # 11
ここで「クラスインスタンス変数」について確認する。
class MyClass @my_var = "I belongs to ????" def self.read @my_var = "I belongs to class." end def write @my_var = "I belongs to instanse." end def read @my_var end end obj = MyClass.new p obj.read # nil obj.write p obj.read # "I belongs to instanse." p MyClass.read # "I belongs to class."
インスタンス変数はあくまで、「インスタンス」に所属している。最初のreadでnilが返ってきたのは、クラス定義内に書かれた最初の@my_var
への代入がインスタンスの変数に影響を及ぼさなかったからである。クラス定義内の変数に関してはローカル変数でも同じ現象が発生している。
ここで気づいたのは「クラス内で宣言したローカル変数はprivateメソッドのように振る舞う」ということである。instance_evalを用いずにクラス内でレシーバーを付けて同じクラス内のローカル変数にアクセスしようとしてもundefinedになってしまう。
ブロック - Pyons Tech Blog
次のwriteではじめて、インスタンスにインスタンス変数がセットされ、文字が代入された。そのため次のreadは期待通り作動している。
最後のreadはクラス自体がselfになるようなインスタンス変数の定義が行われている。Rubyではクラス自体もインスタンスに過ぎないので、当然インスタンス変数がセットされうる。
class_evalを利用した例として以下のような例が取り上げられている。
現在時刻を利用するテストのユニットテストを書く際に、現在時刻が実行時間によってばらつきが出てしまう。
これはclass_eval(今回は新たにメソッドを定義しているわけではないのでinstance_eval
も利用可だが)を用いて、テスト先のクラスを再オープンし、時刻を取得するメソッドを書き換えることで回避できる。
これがテスト対象のコードである。
class Loan def initialize(book) @book = book @time = Loan.time_class.now end def self.time_class @time_class || Time end def to_s "#{@book.upcase} loadned on #{@time}" end end
そしてこれが、テストコード。
class FakeTime def self.now "Sun Oct 06 16:19:20" end end require "test/unit" require "./bookworm.rb" class TestLoan < Test::Unit::TestCase def test_conversion_to_string # Loan.instance_eval{@time_class = FakeTime} Loan.class_eval{@time_class = FakeTime} loan = Loan.new("Ruby") assert_equal "RUBY loadned on Sun Oct 06 16:19:20", loan.to_s end end
後半は特異メソッドの話になるので、最後に特殊なクラス定義の方法にだけ触れる。
no_name = Class.new(Array) do # 引数は継承元クラス def my_method "HelloWorld" end end NewArray = no_name p NewArray.new.my_method # "HelloWorld"
クラスはこのように動的に定義することもできる。 ただし、この定義だけでは、無名クラスが生成されるだけなので、定数に代入を行って、クラス名を確定させる必要がある。