从任意哈希初始化Ruby类,但只有具有匹配访问器的键

时间:2009-11-04 19:55:47

标签: ruby class

是否有一种简单的方法可以列出已在Ruby类中设置的访问者/阅读器?

class Test
  attr_reader :one, :two

  def initialize
    # Do something
  end

  def three
  end
end

Test.new
=> [one,two]

我真正想要做的是允许初始化接受具有任意数量属性的哈希,但只提交已经定义了读者的哈希。类似的东西:

def initialize(opts)
  opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
    eval("@#{opt} = \"#{val}\"")
  end
end

还有其他建议吗?

6 个答案:

答案 0 :(得分:9)

这就是我使用的(我称之为idiom hash-init)。

 def initialize(object_attribute_hash = {})
  object_attribute_hash.map { |(k, v)| send("#{k}=", v) }
 end

如果你使用的是Ruby 1.9,你可以做得更干净(发送允许私有方法):

 def initialize(object_attribute_hash = {})
  object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }
 end

如果您尝试分配给foo并且方法“foo =”不存在,则会引发NoMethodError。如果你想干净(指定存在作者的attrs)你应该做一个检查

 def initialize(object_attribute_hash = {})
  object_attribute_hash.map do |(k, v)| 
    writer_m = "#{k}="
    send(writer_m, v) if respond_to?(writer_m) }
  end
 end

然而,这可能导致您向对象提供错误的键(例如从表单中)而不是大声失败而只会吞下它们的情况 - 前方的痛苦调试。所以在我的书中,NoMethodError是一个更好的选择(它表示合同违规)。

如果您只想要所有作家的清单(没有办法为读者做到这一点),那么

 some_object.methods.grep(/\w=$/)

是“获取一个方法名称数组,并将grep用于以单词后面的单个等号结尾的条目”。

如果你这样做

  eval("@#{opt} = \"#{val}\"")

val 来自网络表单 - 恭喜你,你刚刚为你的应用程序配备了一个开放的漏洞。

答案 1 :(得分:4)

您可以覆盖attr_reader,attr_writer和attr_accessor,为您的班级提供某种跟踪机制,这样您就可以拥有更好的反射功能。

例如:

class Class
  alias_method :attr_reader_without_tracking, :attr_reader
  def attr_reader(*names)
    attr_readers.concat(names)
    attr_reader_without_tracking(*names)
  end

  def attr_readers
    @attr_readers ||= [ ]
  end

  alias_method :attr_writer_without_tracking, :attr_writer
  def attr_writer(*names)
    attr_writers.concat(names)
    attr_writer_without_tracking(*names)
  end

  def attr_writers
    @attr_writers ||= [ ]
  end

  alias_method :attr_accessor_without_tracking, :attr_accessor
  def attr_accessor(*names)
    attr_readers.concat(names)
    attr_writers.concat(names)
    attr_accessor_without_tracking(*names)
  end
end

这些可以很简单地证明:

class Foo
  attr_reader :foo, :bar
  attr_writer :baz
  attr_accessor :foobar
end

puts "Readers: " + Foo.attr_readers.join(', ')
# => Readers: foo, bar, foobar
puts "Writers: " + Foo.attr_writers.join(', ')
# => Writers: baz, foobar

答案 2 :(得分:2)

尝试这样的事情:

class Test
  attr_accessor :foo, :bar

  def initialize(opts = {})
    opts.each do |opt, val|
      send("#{opt}=", val) if respond_to? "#{opt}="
    end
  end
end

test = Test.new(:foo => "a", :bar => "b", :baz => "c")

p test.foo # => nil
p test.bar # => nil
p test.baz # => undefined method `baz' for #<Test:0x1001729f0 @bar="b", @foo="a"> (NoMethodError)

这基本上就是当你将params哈希传递给new时Rails所做的事情。它将忽略它不知道的所有参数,它将允许您设置不一定由attr_accessor定义的东西,但仍然有一个合适的setter。

唯一的缺点是,这确实需要你定义一个setter(而不仅仅是访问者),这可能不是你想要的。

答案 3 :(得分:0)

访问者只是碰巧访问某些数据的普通方法。这里的代码大致可以满足您的需求。它检查是否存在为散列键命名的方法,如果是,则设置一个附带的实例变量:

def initialize(opts)
  opts.each do |opt,val|
    instance_variable_set("@#{opt}", val.to_s) if respond_to? opt
  end
end

请注意,如果某个键与方法同名但该方法不是简单的实例变量访问(例如{:object_id => 42}),则会触发此操作。但并非所有访问者都必须由attr_accessor定义,因此没有更好的方法来判断。我也把它改为使用instance_variable_set,它更加高效和安全,这很荒谬。

答案 4 :(得分:0)

没有内置的方式来获得这样的列表。 attr_*函数基本上只是添加方法,创建实例变量,而不是其他任何东西。你可以为他们写封皮来做你想做的事,但这可能有点过头了。根据您的具体情况,您可以使用Object#instance_variable_defined?Module#public_method_defined?

另外,尽可能避免使用eval:

def initialize(opts)
  opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
    instance_variable_set "@#{opt}", val
  end
end

答案 5 :(得分:0)

您可以查看定义了哪些方法(使用Object#methods),并从中识别出setter(这些方法的最后一个字符是=),但是没有100%确定的方法可以知道这些方法没有以一种涉及不同实例变量的非显而易见的方式实现。

尽管Foo.new.methods.grep(/=$/)会为您提供可打印的属性设置列表。或者,既然你已经有了哈希,你可以尝试:

def initialize(opts)
  opts.each do |opt,val|
    instance_variable_set("@#{opt}", val.to_s) if respond_to? "#{opt}="
  end
end