Инициализировать класс 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

Любые другие предложения?

6 ответов

Это то, что я использую (я называю эту идиому хэш-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

Это вызовет NoMethodError, если вы попытаетесь присвоить foo и метод "foo =" не существует. Если вы хотите сделать это чистым (назначьте 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, чтобы обеспечить какой-то механизм отслеживания для вашего класса, чтобы вы могли лучше использовать такие возможности отражения.

Например:

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


Попробуйте что-то вроде этого:

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)
</test:0x1001729f0>

Это в основном то, что делает Rails, когда вы передаете хеш params в new. Он будет игнорировать все параметры, о которых он не знает, и это позволит вам устанавливать вещи, которые не обязательно определяются attr_accessor, но все еще имеют соответствующий набор.

Единственным недостатком является то, что на самом деле это требует, чтобы у вас был установлен сеттер (по сравнению с только аксессуаром), который может и не быть тем, что вы ищете.


Вы можете посмотреть, какие методы определены (с помощью Object#methods), а от тех, которые идентифицируют сеттеры (последний символ - =), но нет 100% уверенного способа узнать, что эти методы weren 't реализуется неочевидным способом, который включает в себя различные переменные экземпляра.

Тем не менее 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


Нет никакого встроенного способа получить такой список. Функции 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


Аксессоры - это просто обычные способы доступа к некоторым частям данных. Здесь код, который будет делать примерно то, что вы хотите. Он проверяет, есть ли метод, названный для хэш-ключа, и устанавливает соответствующую переменную экземпляра, если это так:

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, который намного эффективнее и безопаснее, чем смешно.

licensed under cc by-sa 3.0 with attribution.