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"

クラスはこのように動的に定義することもできる。 ただし、この定義だけでは、無名クラスが生成されるだけなので、定数に代入を行って、クラス名を確定させる必要がある。