writing attr_reader

クラスにおける属性のアクセサを自動的に生成するのはRubyの非常に便利な機能だが、どのように実装されているのだろう。
と、Moduleクラス(attr_reader, attr_writer, attr_accessorはModuleクラスのメソッド)のソースコードを探したのだが見つからない。それもそのはずで、カーネルとその周りの基本クラスの多くはCで書かれているようだ。Cのソースコードを徹底的に追うのも良いが、別に試してみたいことがあったので、取り敢えずRubyで書いてみたのが以下のコードだ。

  • accessor.rb
class Module
  #とりあえずreaderだけ
  def attr_reader(*symbols)
    symbols.each do |s|
      name = s.to_s
      define_method(name) do
        instance_variable_get("@#{name}")
      end
    end
  end
end

コードはXMLスキーマを扱うAttributeクラスに似た実装があったので、それを真似てModuleクラスに定義されているattr_readerを書き換えただけ。※
見た感じではよさげなのだが、このままではアクセサが置換わったどうかが解らないので、生成するメソッド名に敢えて変な名前をつけてテストしてみることにした。

  • accessor.rb
class Module
  def attr_reader(*symbols)
    symbols.each do |s|
      name = s.to_s
      define_method(name+"を取得") do
        instance_variable_get("@#{name}")
      end
    end
  end
end
class AttrTest < Object
  attr_reader :attr1, :attr2
  def initialize(attr1, attr2)
    @attr1 = attr1
    @attr2 = attr2
  end
end

a = AttrTest.new('地震', '雷')
puts AttrTest.instance_methods(a).find_all {|item| item =~ /attr/ }
puts a.attr1を取得 + '、' + a.attr2を取得 + '、火事親父'
  • 実行結果
>ruby -Ks accessor.rb
attr1を取得
attr2を取得
地震、雷、火事親父

define_methodを使ってメソッドを動的に追加しているため、実際にインスタンスを生成してattr_readerメソッドを呼び出さないとアクセサメソッドも生成されない。

試しに、アクセサを使っている行を

puts a.attr1 + '、' + a.attr2 + '、火事親父'

と変えてみると以下のようにエラーになるのを確認できるので、attr_readerの置き換えには成功しているようだ。

accessor.rb:23: undefined method `attr1' for # (NoMethodError)

※同様にwriterはinstance_variable_setメソッドを使えば同様に書けそうだ。

  def attr_writer(*symbols)
    symbols.each do |s|
      name = s.to_s
      define_method(name+"を設定=") do |param|
        instance_variable_set("@#{name}", param)
      end
    end
  end

何度も書いていることだが、この辺も実に面白い。特に define_methodにブロックを与えて、それがメソッドボディになるなんて目から鱗が100枚は落ちたよ。