Rubyなんもわからん
環境
- Ruby 2.6
経緯
例えば以下のようなUserクラスがあったとする。ここでは検証のために attr_reader
ではなく attr_accessor
を使っているのでご了承
class User attr_accessor :name, :age def initialize @name = "太郎" @age = 20 end def name "#{@name}さん" end end
この状態で、以下を出力実行
u = User.new puts u.name # => 太郎さん puts u.age # => 20
まぁ納得である。
ここでは attr_accessor :name, :age
と予め書いたが、このnameとageが変則でインスタンス化されたクラスごとに動的に定義させたい場合はどうすればいいのか
Rubyの大抵は send()
を使うと解決するってマックのJKが言ってた。
class User def initialize self.class.send(:attr_accessor, "name") self.instance_variable_set("@name", "太郎") self.class.send(:attr_accessor, "age") self.instance_variable_set("@age", 20) end def name "#{@name}さん" end end
一見動きそうだが、動かない 実際に試してみると
u = User.new puts u.name # 太郎 (???? puts u.age # 20
おかしい。インスタンス変数で定義したのが反映されれば本来「太郎さん」になるはず
おそらく、attr_accessorでnameのセッターが自動で定義されるので、 def name
が上書きされてしまったんじゃないか。。。
変数をセットする側の self.instance_variable_set("@name", "太郎")
を @name = "太郎"
や self.send("name=", "太郎")
に変更しても変わらずだった。。
うまく行った例
instance_variable_set("@#{k}", v) unless respond_to? k define_singleton_method(k) { instance_variable_get("@#{k}") } end
完成サンプルも載せとく
# 完成版 class User def initialize(args = []) args.each do |k ,v| instance_variable_set("@#{k}", v) unless respond_to? k define_singleton_method(k) { instance_variable_get("@#{k}") } end end end def self.fetchs # APIで取得したのを想定 [ {name: "太郎", age: 20 }, {name: "次郎", age: 30 } ].map do |hash| User.new(hash) end end def name "#{@name}さん" end end User.fetchs.each do |a| puts a.name puts a.age end
すると
# 太郎さん # 20 # 次郎さん # 30
と表示される。
追記
やはり attr_accessor
がゲッターを上書きしてしまってる説は正しかった。以下でもうまく動作する。
class User def initialize self.class.send(:attr_reader, "name") unless respond_to? "name" self.class.send(:attr_writer, "name") unless respond_to? "name=" self.class.send(:attr_reader, "age") unless respond_to? "age" self.class.send(:attr_writer, "age") unless respond_to? "age=" self.instance_variable_set("@name", "太郎") self.instance_variable_set("@age", 20) end def name "#{@name}さん" end end u = User.new puts u.name # => 太郎 puts u.age # => 20
書き直したテンプレも一応入れとく
class User def initialize(args = []) args.each do |k ,v| self.class.send(:attr_reader, k) unless respond_to? k instance_variable_set("@#{k}", v) end end def self.fetchs # APIで取得したのを想定 [ {name: "太郎", age: 20 }, {name: "次郎" } ].map do |hash| User.new(hash) end end def age @age.to_i end end User.fetchs.each do |a| puts a.name puts a.age end