将块传递到 define_method

Pass block into define_method

问题

有个模式是我自己用的比较频繁的,想晒一下。我有这样的东西:

class InfoGatherer
  def foo
    true
  end

  def people
    unless @people
      @people = # Long and complex calculation (using foo)
    end
    @people
  end
end

我想把它晒干成这样:

class InfoGatherer
  extend AttrCalculator

  def foo
    true
  end

  attr_calculator(:people) { # Long and complex calculation (using foo) }
end

为此,我定义了一个模块 AttrCalculator 以扩展到 InfoGatherer。这是我尝试过的:

module AttrCalculator
  def attr_calculator(variable_name_symbol)
    variable_name = "@#{variable_name_symbol}"

    define_method variable_name_symbol do
      unless instance_variable_defined?(variable_name)
        instance_variable_set(variable_name, block.call)
      end
      instance_variable_get(variable_name)
    end

  end
end

不幸的是,当我尝试像 InfoGatherer.new.people 这样简单的操作时,我得到:

NameError: undefined local variable or method `foo' for InfoGatherer:Class

嗯,这很奇怪。为什么 block 运行 在 InfoGatherer:Class 的范围内,而不是它的实例 InfoGatherer.new

研究

我知道我不能使用 yield,因为那样会尝试捕获错误的块,如 here 所示。 我试图用 self.instance_exec(block) 代替上面的 block.call,但后来我收到一个新错误:

LocalJumpError: no block given

嗯?我在 this SO question 中看到了同样的错误,但我已经在使用括号表示法,所以那里的答案似乎并不适用。

我也尝试过使用 class_eval,但我不确定如何在字符串中调用 block。这肯定行不通:

class_eval("
  def #{variable_name_symbol}
    unless #{variable_name}
      #{variable_name} = #{block.call}
    end
    #{variable_name}
  end
")

该用例称为记忆。可以像这样轻松完成:

def people
  @people ||= # Long and complex calculation (using foo)
end

你不应该像现在这样陷入困境。

扩展最后的人

def people(varariable = nil)
  @people ||= ComplexCalculation.new(variable).evaluate
end

class ComplexCalculation

  def initialize(variable)
    @variable = variable
  end

  def evaluate(variable)
    #stuff
  end

end

通过提取此 class,您可以隔离复杂性并获得更好的体验。

问题是,在 define_method 中,self 令人惊讶地 InfoGatherer,而不是 InfoGatherer 实例 ].所以我在 self.instance_exec(block).

的正确轨道上

有效的解决方案是self.instance_exec(&block)(注意和号)。我想解释器不会识别 block 是一个块,除非你这样标记它?如果有人能比我更好地解释这一点,请这样做。

附带说明一下,这不是解决此特定问题的最佳方法。请参阅@sawa 的回答,了解记忆复杂计算的简洁方法。