2010/08/07

self.class

<http://midorex.blogspot.com/2010/08/singletonclass.html> 前回からの続き

class Test
   def initialize(h)
     h.each{|k,v|
       self.class.send(:attr_accessor, k)
       self.send("#{k}=", v)
     }
  end
end

h={:cat=>'cat'}
t = Test.new(h)
t.cat = 'ddd'
p t

これを実行すると
#<Test:0x00000 @cat="ddd">
となる。でもでも :cat を attr_reader にしたかった場合はこのままだとうまくいかない。んで考えてみたこと。

まずは通常の書き方

class Test
  attr_reader :cat, :dog
  def initialize
    @cat = 'meow'
    @dog = 'bowwow'
  end
end

t = Test.new
p t.instance_variables
p t.class.instance_methods[0..2]
p t.cat
t.cat = "cat"

実行結果

[:@cat, :@dog]
[:cat, :dog, :nil?]
"meow"
./draft.rb:15:in `<main>': undefined method `cat=' for #<Test:0x00000 @cat="meow", @dog="bowwow"> (NoMethodError)

最後の行でcat に代入しようとしても attr_reader だから代入できないことを確認。

この結果と同じ結果を導きだすには?

class Test1
  def initialize(h)
    h.each{|k,v|
      self.class.send(:attr_reader, "#{k}")
      self.send("#{k}=", v)
    }
  end
end

h = {:cat=>'meow', :dog=>'bowwow'}
t = Test1.new(h)
p t.instance_variables
p t.class.instance_methods[0..2]
p t.cat
t.cat = "cat"

実行結果

... `block in initialize': undefined method `cat=' for #<Test1:0x00000> (NoMethodError)

self.send("#{k}=", v) は attr_accessor の時は使えるけど attr_reader の時は使えない。
文字通り reader だから getter であって setter じゃないため instance 変数に"cat"を代入できないためにエラーがかえってきた。
ならば
self.send のかわりに普通に instance_variable_set を使ってみた。

class Test1
  def initialize(h)
    h.each{|k,v|
      instance_variable_set("@#{k}", v)
      self.class.send(:attr_reader, "#{k}")
    }
  end
end

h = {:cat=>'meow', :dog=>'bowwow'}
t = Test1.new(h)
p t.instance_variables
p t.class.instance_methods[0..2]
p t.cat
t.cat = "cat"

実行結果

[:@cat, :@dog]
[:cat, :dog, :nil?]
"meow"
./draft.rb:25:in `<main>': undefined method `cat=' for #<Test:0x00000 @cat="meow", @dog="bowwow"> (NoMethodError)

うまくいった。
instance_variable_set でインスタンス変数に値を代入しておいてその後に、getter を設定した。
self.class.send(:attr_reader, "#{k}")
を削除すると、[通常の書き方]から attr_reader を削除した場合と等しくなる。

# --------------------

次はインスタンス変数を定義しないし attr も必要ない場合。
定義したいのはinstance_method def cat と def dog だけ。

通常の書き方。

class Test
  def initialize
  end
  def cat
    "meow"
  end
  def dog
    "bowwow"
  end
end

t = Test.new
p t.instance_variables
p t.class.instance_methods[0..2]
p t.cat

実行結果

[]
[:cat, :dog, :nil?]
"meow"

上記と同じ結果を導くには?

class Test1
  def initialize(h)
    h.each{|k,v|
      self.class.send(:define_method, k){v}
    }
  end
end

h = {:cat=>'meow', :dog=>'bowwow'}
t = Test1.new(h)
p t.instance_variables
p t.class.instance_methods[0..2]
p t.cat

実行結果

[]
[:cat, :dog, :nil?]
"meow"


まとめ
1, getter setter をinitialize 内で使う
self.class.send(:attr_accessor, k)
self.class.send(:attr_reader, k)
self.class.send(:attr_writer, k)

2, @cat のようなインスタンス変数に値を代入する (k はシンボル)
instance_variable_set("@#{k}", v)
# setter が定義されていることが必要条件
self.send("#{k}=", v)

3, def cat のようなインスタンスメソッドを定義する
self.class.send(:define_method, k){v}

+++ 追記 +++

attr_accessor は通常は クラス内の def initialize の上とか下に書く。
initialize の中には書かない。つか普通にエラーになる。
ということは、class Test のinstance が保有するメソッドじゃなくて
class Test が知っているメソッドだとわかるわけだけども
p Test.methods
とかやったって表示されない。じゃ一体どこにあるんだ?ということで下記を作ってみた。

  4 module My
  5   def self.search_method(x, key)
  6     ary = x.methods
  7     print "=== OBJECT:#{x} ===\n"
  8     ary.each{|m|
  9       next unless /methods/.match(m.to_s)
 10       next if m == :methods
 11       eval "a = x.send(m)
 12         if a.index(key)
 13           print \"[:#{key}] exist in #{m}\n\"
 14         else
 15           print \"# #{m}\n\"
 16         end"
 17     }
 18   end
 19 end
 20
 21 class Test
 22   def initialize
 23     My::search_method(self.class, :attr)
 24     # My::search_method(self, :nil?)
 25   end
 26 end
 27
 28 Test.new


# 実行結果

=== OBJECT:Test ===
# instance_methods
# public_instance_methods
# protected_instance_methods
# private_instance_methods
# singleton_methods
# protected_methods
[:attr] exist in private_methods
# public_methods


クラスの「private_methods」に属していることがわかった。
結局...
self.class.send(:attr_accessor, k)
self.send("#{k}=", v)

こういう方法でインスタンス変数を定義するってことは、親の遺伝子を子供が外側から操作しているように思えてきてよくないような気もしてきた。

0 件のコメント: