使用define_method和元编程在Ruby中动态定义实例方法?

时间:2015-10-15 13:57:19

标签: ruby metaprogramming

我有一些类似的代码:

class Country
  attr_reader :name

  def initialize
    @name     = "MyName".freeze
  end

  def government
    @government ||= Government.new(self)
  end

  def symbols
    @symbols ||= Symbols.new(self)
  end

  def economy
    @economy ||= Economy.new(self)
  end

  def education
    @education ||= Education.new(self)
  end

  def healthcare
    @healthcare ||= Healthcare.new(self)
  end

  def holidays
    @holidays ||= Holidays.new(self)
  end

  def religion
    @religion ||= Religion.new(self)
  end

end

如何动态创建方法?我试过了:

class Country
  attr_reader :name

  COMPONENETS = %w(government symbols economy education healthcare holidays religion)


  COMPONENETS.each do |m|
    define_method(m) do |argument|
      instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
    end
  end

  def initialize
    @name     = "MyName".freeze
  end

end 

如果我尝试:

puts Country.new.education.inspect

我收到以下错误:

country.rb:16:in `block (2 levels) in <class:Country>': wrong number of arguments (0 for 1) (ArgumentError)
    from country.rb:27:in `<main>'

我在这里缺少什么?

3 个答案:

答案 0 :(得分:2)

在原始代码中,您定义了所有不带参数的方法:

def education
#            ^^^
  @education ||= Education.new(self)
end

在元编程代码中,您定义所有方法以采用名为argument的单个参数:

define_method(m) do |argument|
#                   ^^^^^^^^^^
  instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end

但是,你用零参数调用它:

puts Country.new.education.inspect
#                        ^^^

显然,你的方法是懒惰的getter,所以他们不应该参数:

define_method(m) do
  instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end

请注意,您的代码还有其他问题。在原始代码中,如果实例变量未定义nilfalse,则使用条件赋值仅执行赋值,而在元编程代码中,您始终无条件地设置它。它应该是更像这样的东西:

define_method(m) do
  if instance_variable_defined?(:"@#{m}")
    instance_variable_get(:"@#{m}")
  else
    instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self))
  end
end

注意:我还从Object.的调用中删除了const_get,以使用常规常量查找规则查找常量(即,首先在词典中向外向上然后在继承层次结构中),因为这对应如何查找原始代码段中的常量。

这并不完全等同于您的代码,因为它仅在未定义时才设置实例变量,而不是在falsenil时设置实例变量,但我想这更接近您的意图反正。

我会封装此代码以使其意图更清晰:

class Module
  def lazy_attr_reader(name, default=(no_default = true), &block)
    define_method(name) do
      if instance_variable_defined?(:"@#{name}")
        instance_variable_get(:"@#{name}")
      else
        instance_variable_set(:"@#{name}",
          if no_default then block.(name) else default end)
      end
    end
  end
end

class Country
  attr_reader :name

  COMPONENTS = %w(government symbols economy education healthcare holidays religion)

  COMPONENTS.each do |m|
    lazy_attr_reader(m) do |name|
      const_get(name.capitalize).new(self))
    end
  end

  def initialize
    @name = 'MyName'.freeze
  end
end

这样,有人在阅读你的Country课程时不会去哼哼,所以有这个循环定义了有时获取并有时设置实例变量的方法#34; &#34;啊,这是一个创造懒惰吸气剂的循环!&#34;

答案 1 :(得分:1)

我猜你不需要argument

class Country
  attr_reader :name

  COMPONENETS = %w(government symbols economy education healthcare holidays religion)


  COMPONENETS.each do |m|
    define_method(m) do
      instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
    end
  end

  def initialize
    @name     = "MyName".freeze
  end

end 

答案 2 :(得分:1)

您只需使用eval:

class Country
  attr_reader :name

  COMPONENETS = %w(government symbols economy education healthcare holidays religion)

  COMPONENETS.each do |m|
   eval <<-DEFINE_METHOD
   def #{m}
     @#{m} ||= #{m.capitalize}.new(self)
   end
DEFINE_METHOD
  end

  def initialize
    @name = "MyName".freeze
  end
end