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

Using define_method and meta-programming to define instance methods dynamically in Ruby?

我有一些类似的代码:

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>'

我在这里错过了什么?

我猜你不需要 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 

您可以简单地使用 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

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

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

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

这并不完全等同于您的代码,因为它仅在实例变量未定义时设置实例变量,而不是在 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 class 的人就不会去 "Huh, so there is this loop which defines methods which sometimes get and sometimes set instance variables",而是认为 "Ah, this is a loop which creates lazy getters!"