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

age*_*217 13 ruby metaprogramming

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

例如:

def outer_method
  def inner_method
     # ...
  end
  # ...
 end
Run Code Online (Sandbox Code Playgroud)

gle*_*ald 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
Run Code Online (Sandbox Code Playgroud)

但是如果你的工作集可能变得很大,你就会为每个操作执行1000次或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
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 如最后所述,这是*lambdas*的一个很好的例子,而不是用于定义方法.为此目的在类上定义一个新方法是严重的过度杀伤. (7认同)
  • 请问为什么这是"严重的矫枉过正"?根据我的理解,将方法变为正确的区别只是一个区别? (3认同)

Chu*_*uck 5

是的,有.事实上,我敢打赌你每天至少使用一种定义另一种方法的方法:attr_accessor.如果你使用Rails,那么经常使用的还有更多,例如belongs_tohas_many.它通常也适用于AOP风格的结构.


Edv*_*rdM 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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

因此,拥有这种结构,您始终可以阅读score()的整个实现,而无需查看Scoring#score之外定义的任何其他方法.即使我没有经常看到这样的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)
Run Code Online (Sandbox Code Playgroud)

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