initializeの引数をインスタンス変数に引き渡す
initialize に渡された値を自動的にインスタンス変数にするを読んで。
ちょっと主旨が違うような気もしますが、書いてみました(1)。
# (1) class Rubyco def initialize(name, age) %w(name age).each do |s| eval("@#{s} = #{s}") end end def to_s "name:#{@name}, age:#{@age}" end end p Rubyco.new("Alice", 23).to_s #=> "name:Alice, age:23"
nameとageを2回書くのがいやなので、無理矢理メソッドにしてみました(2)。Array#zipが泣かせます♪
# (2) class Object def make_instance_variables(names, *values) names.zip(values).each do |nv| eval("@#{nv[0]} = #{nv[1].inspect}") end end end class Rubyco def initialize(*args) make_instance_variables(%w(name age), *args) end def to_s "name:#{@name}, age:#{@age}" end end p Rubyco.new("Alice", 23).to_s #=> "name:Alice, age:23"
課題:(2)だとinitializeのarityチェックができない。
疑問:make_instance_variablesのようなメソッドはどのクラス(またはモジュール)に入れるべきか。現在はObjectにしている。
追記:mputさんから「newの引数の数が違ったときに例外が発生しない」というコメントをいただきました。そうですね。おっしゃるとおりです。いちおう課題のところにあるarityチェック(メソッドの引数の数のこと)というのがそのつもりでした。コメントありがとうございます。
追記:上記のmputさんの指摘を受けて、(これまた無理矢理に)arityチェックを入れてみました(3)。
# (3) class Object def make_instance_variables(names, *values) if names.length != values.length raise ArgumentError.new("wrong number of arguments (#{values.length} for #{names.length})"); end names.zip(values).each do |nv| eval("@#{nv[0]} = #{nv[1].inspect}") end end end class Rubyco def initialize(*args) begin make_instance_variables(%w(name age), *args) rescue ArgumentError => e raise ArgumentError.new(e.to_s) end end def to_s "name:#{@name}, age:#{@age}" end end p Rubyco.new("Alice", 23).to_s #=> "name:Alice, age:23" p Rubyco.new("Alice", "Liddel", 23).to_s #=> wrong number of arguments (3 for 2) (ArgumentError)
上のプログラムでは、コールスタックが気になったので、例外の再スローをしています(汚い)。
新たな課題:メソッドからraiseするとき、callerのコールスタックでraiseする方法を考える。絶対すでにどこかで誰かが考えていてTipsになっているはず。PerlでいうところのCarpのような処理。
追記:さっそく、なかださんからご教示いただきました(4)。$@.shiftで頭落としする方法と、caller(1)を使う方法。そうか、まさにcallerというのがあるのですね!変数に代入して比較というコンパクトな書き方と共に勉強になりました。ありがとうございます。
…そうか、代入で変数が生まれるというのはなかなか素敵仕様ですね…。
# (4) class Object def make_instance_variables(names, *values) if (nl = names.length) != (vl = values.length) raise ArgumentError, "wrong number of arguments (#{vl} for #{nl})", caller(1) end names.zip(values).each do |nv| eval("@#{nv[0]} = #{nv[1].inspect}") end end end class Rubyco def initialize(*args) make_instance_variables(%w(name age), *args) end def to_s "name:#{@name}, age:#{@age}" end end p Rubyco.new("Alice", 23).to_s #=> "name:Alice, age:23" p Rubyco.new("Alice", "Liddel", 23).to_s #=> wrong number of arguments (3 for 2) (ArgumentError)