instance_evalとclass_evalについての備忘録

メタプログラミングRubyを読んで書いています。 ちょっとした備忘録です。

instance_evalとclass_evalの挙動の違いについて。

instance_evalとは何か?

instance_evalは、オブジェクトの特異クラスにインスタンスメソッドを定義したり、そのオブジェクト自身が参照できるインスタンス変数を定義、または上書きしたりすることができる。

# instance_eval.rb

class Sample
  def initialize
    @x = 1
  end
end

sample = Sample.new
sample.instance_eval do
  @y = 1
  def output # => これは特異クラスのインスタンスメソッドになる
    @x + @y
  end
end

puts sample.output # => 2

puts sample.singleton_class.instance_methods.grep(/output/) # => output

特異クラスについてはこちら

class_evalとは何か?

class_evalは、クラスにインスタンスメソッドやクラスメソッド(特異メソッド)を追加したり、そのクラス自身のインスタンス変数(クラスインスタンス変数)やクラス変数を定義、または上書きしたりするときに利用できる。

# class_eval.rb

class Sample
  def initialize
    @x = 1
  end
end

Sample.class_eval do
  @y = 1 # クラスインスタンス変数
  @@a = 1 # クラス変数
  def output # クラス内のインスタンスメソッド
    @x
  end
end

sample = Sample.new
puts sample.instance_variables # => @x
puts Sample.instance_variables # => @y
puts Sample.class_variables # => @@a
puts sample.output # => 1
puts Sample.instance_methods.grep(/output/) # => output

このように、class_evalの中で、インスタンス変数を定義しようとすると、それはクラスのクラスインスタンス変数になる。 クラス変数はそのまま書けば、クラス変数として扱われる。 メソッドはクラス内のインスタンスメソッドとして定義されるので、もちろん、作成したオブジェクト(sample)はそのメソッドを利用することができる。

とりあえず理解してけば良いこと

instance_evalについて ・レシーバーであるインスタンスインスタンス変数を定義したり、上書きすることができる。 ・レシーバーであるインスタンスが利用できるメソッド(インスタンスの特異クラスのインスタンスメソッド。他のインスタンスは当然ながら、このメソッドを利用することはできない)を定義したり、上書きすることができる。

class_evalについて ・レシーバーであるクラスのインスタンス変数(クラスインスタンス変数)やクラス変数を定義したり、上書きすることができる。 ・レシーバーであるクラスのインスタンスメソッドを定義したり、上書きすることができる。

これだけわかっておけば、基本問題ない気がする。

試してみたこと

1. インスタンスには、インスタンス固有のメソッド

# instance_eval.rb

class MyClass
  def initialize
    @x = 1
  end
end

first = MyClass.new
first.instance_eval do
  @y = 1
  def calc
    @x + @y
  end
end
puts first.calc

second = MyClass.new
puts second.calc # => Error secondにはcalcメソッドが定義されていないから。

instance_evalのレシーバーであるオブジェクトだけが、定義や上書きの対象になる様子。 上のコードではオブジェクトfirstにinstance_evalをして、中身をいじくっているが、 そのあとに定義されたオブジェクトsecondには全く反映されていない。

2. インスタンスに定義されたメソッドの行方

# evals.rb

class InstanceEval
  def initialize
    @x = 1
  end
end
sample = InstanceEval.new
sample.instance_eval do
  def output
    @x
  end
end
puts sample.instance_variables # => @x
puts sample.output # => 1

class ClassEval
  def initialize
    @y = 1
  end
end
ClassEval.class_eval do
  @v = 1 # クラスインスタンス変数。クラスのインスタンス変数。
  @@a = 1 # クラス変数。initializeの外に書いても、クラス変数。
  def output
    @y
  end
end
another_sample = ClassEval.new

puts sample.singleton_methods.grep(/output/) # => output
puts sample.singleton_class.instance_methods.grep(/output/) # => output
puts InstanceEval.instance_methods.grep(/output/)
puts ClassEval.instance_methods.grep(/output/) # => output
puts ClassEval.instance_variables # => @v
puts ClassEval.class_variables # => @@a
puts another_sample.instance_variables # => @y

instance_evalで定義したメソッドは、instance_methodsメソッドで発見できなかった。 なので、singleton_classのインスタンスメソッドを探したところ、発見した。 instance_evalで定義したメソッドは、レシーバーの特異クラスのインスタンスメソッドになるみたい。 1つ前のコードで、あるインスタンスの中で定義されたメソッドが、他のインスタンスでは使えないという理由がここにあったみたいだ。