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

有没有一种简单的方法来列出已在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 

还有其他建议吗?

这是我使用的(我称之为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来自网络表单 – 恭喜你,你刚刚为你的应用程序配备了一个开放的漏洞。

您可以覆盖attr_reader,attr_writer和attr_accessor,为您的类提供某种跟踪机制,这样您就可以拥有更好的reflectionfunction。

例如:

 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 

这些可以很简单地certificate:

 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 

尝试这样的事情:

 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 # (NoMethodError) 

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

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

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

 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 ,这样效率更高,更安全,这很荒谬。

没有内置的方法来获得这样的列表。 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 

您可以查看定义了哪些方法(使用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