Ruby:在另一个方法中定义一个方法是否有任何实际用途?

时间:2010-11-04 01:37:12

标签: ruby metaprogramming

我正在阅读一篇关于元编程的文章,它表明你可以在另一个方法中定义一个方法。这是我已经知道了一段时间的事情,但它让我问自己一个问题:这有什么实际应用吗?在方法中定义方法是否有任何实际用途?

例如:

def outer_method
  def inner_method
     # ...
  end
  # ...
 end

5 个答案:

答案 0 :(得分:11)

我最喜欢的元编程示例是动态构建一个方法,然后您将在循环中使用它。例如,我有一个用Ruby编写的查询引擎,其中一个操作是过滤。有许多不同形式的过滤器(子串,等号,< =,> =,交叉点等)。天真的方法是这样的:

def process_filter(working_set,filter_type,filter_value)
  working_set.select do |item|
    case filter_spec
      when "substring"
        item.include?(filter_value)
      when "equals"
        item == filter_value
      when "<="
        item <= filter_value
      ...
    end
  end
end

但是如果你的工作集可以变大,你就会为每个操作执行1000s或1000000次的大案例语句,即使它在每次迭代时都会采用相同的分支。在我的情况下,逻辑比只是一个case语句更复杂,所以开销更大。相反,你可以这样做:

def process_filter(working_set,filter_type,filter_value)
  case filter_spec
    when "substring"
      def do_filter(item,filter_value)
        item.include?(filter_value)
      end
    when "equals"
      def do_filter(item,filter_value)
        item == filter_value
      end
    when "<="
      def do_filter(item,filter_value)
        item <= filter_value
      end
    ...
  end
  working_set.select {|item| do_filter(item,filter_value)}
end

现在,一次性分支是在前面完成的,并且生成的单用途函数是内循环中使用的函数。

事实上,我的真实例子有三个层次,因为工作集和过滤器值的解释存在差异,而不仅仅是实际测试的形式。所以我构建了一个item-prep函数和一个filter-value-prep函数,然后构建一个使用它们的do_filter函数。

(我实际上使用的是lambdas,而不是defs。)

答案 1 :(得分:5)

是的,有。事实上,我敢打赌你每天至少使用一种定义另一种方法的方法:attr_accessor。如果您使用Rails,则会持续使用Rails,例如belongs_tohas_many。它通常也适用于AOP风格的结构。

答案 2 :(得分:5)

我认为使用内在方法有另一个好处,即清晰度。想一想:一个包含方法列表的类是一个扁平的,非结构化的方法列表。如果你关心关注点的分离并将东西保持在相同的抽象层次中,并且这段代码只在一个地方使用,那么内部方法会有所帮助,同时强烈暗示它们仅用于封闭方法。

假设您在课程中使用此方法:

class Scoring
  # other code
  def score(dice)
    same, rest = split_dice(dice)

    set_score = if same.empty?
      0
    else 
      die = same.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end
    set_score + rest.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code
end

现在,这是一种简单的数据结构转换和更高级别的代码,添加形成一组的骰子的分数和不属于该组的那些。但不是很清楚发生了什么。让我们更具描述性。下面是一个简单的重构:

class Scoring
  # other methods...
  def score(dice)
    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  def get_set_score(dice)
    die = dice.keys.first
    case die
    when 1
      1000
    else
      100 * die
    end
  end

  def get_rest_score(dice)
    dice.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code...
end

get_set_score()和get_rest_score()的想法是通过使用描述性(虽然在这个炮制的例子中不是很好)来记录这些部分的作用。但是如果你有很多像这样的方法,得分()中的代码并不容易理解,如果你重构任何一种方法,你可能需要检查其他方法使用它们(即使它们是私有的 - 其他同一类的方法可以使用它们。)

相反,我开始更喜欢这个:

class Scoring
  # other code
  def score(dice)
    def get_set_score(dice)
      die = dice.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end

    def get_rest_score(dice)
      dice.map { |die, count| count * single_die_score(die) }.sum
    end

    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  # other code
end

在这里,更明显的是get_rest_score()和get_set_score()被包装到方法中以保持score()的逻辑本身处于相同的抽象级别,不干涉哈希等。

请注意,从技术上讲,你可以调用Scoring#get_set_score和Scoring#get_rest_score,但在这种情况下它会是错误的样式IMO,因为在语义上它们只是单个方法得分的私有方法()< / p>

所以,有了这个结构,你总是可以阅读得分()的整个实现,而无需查看在得分#得分之外定义的任何其他方法。即使我没有经常看到这样的Ruby代码,我想我会用内部方法将更多内容转换为这种结构化样式。

注意:另一个看起来不干净但避免名称冲突问题的选项就是简单地使用lambdas,它在Ruby中一直存在。使用该示例,它将变为

get_rest_score  = -> (dice) do
  dice.map { |die, count| count * single_die_score(die) }.sum
end
...
set_score + get_rest_score.call(rest)

它不是那么漂亮 - 有人在看代码时可能想知道为什么所有这些lambdas,而使用内部方法是非常自我记录的。我仍然更倾向于lambdas,因为他们没有将潜在冲突的名称泄露到当前范围的问题。

答案 3 :(得分:3)

不使用def没有实际的应用程序,编译器可能会引发错误。

有理由在执行另一个方法的过程中动态定义方法。考虑使用C实现的attr_reader,但可以在Ruby中等效地实现:

class Module
  def attr_reader(name)
    define_method(name) do
      instance_variable_get("@#{name}")
    end
  end
end

在这里,我们使用#define_method来定义方法。 #define_method是一种实际的方法; def不是。这给了我们两个重要的属性。首先,它需要一个参数,它允许我们将name变量传递给它来命名方法。其次,它需要一个块,它关闭我们的变量name,允许我们在方法定义中使用它。

那么如果我们使用def会发生什么呢?

class Module
  def attr_reader(name)
    def name
      instance_variable_get("@#{name}")
    end
  end
end

这根本不起作用。首先,def关键字后跟一个文字名称,而不是表达式。这意味着我们正在定义一个名为#name的方法,这根本不是我们想要的。其次,方法的主体引用一个名为name的局部变量,但Ruby不会将它识别为与#attr_reader的参数相同的变量。 def构造不使用块,因此它不再关闭变量name

def构造不允许您“传入”任何信息来参数化您定义的方法的定义。这使得它在动态环境中无用。没有理由在方法中使用def定义方法。您始终可以将相同的内部def构造从外部def移出,并以相同的方法结束。


此外,动态定义方法会产生成本。 Ruby缓存方法的内存位置,从而提高性能。当您从类中添加或删除方法时,Ruby必须抛弃该缓存。 (在Ruby 2.1之前,该缓存是全局。从2.1开始,缓存是每个类。)

如果在另一个方法中定义一个方法,则每次调用外部方法时,它都会使缓存失效。这对于像attr_reader和Rails'belongs_to这样的顶级宏来说很好,因为这些都是在程序启动时调用,然后(希望)再也不会调用。在程序的持续执行过程中定义方法会让你慢下来。

答案 4 :(得分:0)

我在想一个递归的情况,但我认为它没有足够的意义。