【问题标题】:Ruby for loop a trap?Ruby for 循环陷阱?
【发布时间】:2012-05-10 21:47:10
【问题描述】:

在讨论 Ruby 循环时,Niklas B. 最近谈到了与每个循环相比,for 循环“不引入新范围”。我想看看一些例子,说明人们对此有何感受。

好的,我扩展了这个问题:在 Ruby 中,我们还能在哪里看到 apears 的作用/结束块分隔符,但实际上里面没有范围?除了 for ... do ... end 还有什么?

好的,问题的另一个扩展,有没有办法用花括号 { block } 编写 for 循环?

【问题讨论】:

  • 只是一个旁注(我已经在另一条评论中说过类似的话):for 的问题不仅在于它与使用do/end 的其他构造有细微的不同,但它也只能用于一个原因:遍历可枚举。通常,来自函数式编程的构造(例如 selectmap)比直接从 C 继承的普通旧循环更适合表达问题。
  • 反过来呢:您给出了您认为需要for 循环的真实示例,而我们为您提供了更好的选择? :) 否则我认为这个问题只是 stackoverflow.com/questions/3294509/for-vs-each-in-ruby 的重复
  • 性能如何?不是比进入闭包更快吗?如果您想要在 for 循环中定义局部变量以供以后使用 :) 怎么办? (只是快速思考一下可能有什么好处:)
  • 通常你应该更关心可读性而不是性能,否则你为什么要首先使用 Ruby。在循环中定义一个变量供以后使用,这听起来像是非常糟糕的设计。

标签: ruby


【解决方案1】:

让我们通过一个例子来说明这一点:

results = []
(1..3).each do |i|
  results << lambda { i }
end
p results.map(&:call)  # => [1,2,3]

酷,这是预期的结果。现在检查以下内容:

results = []
for i in 1..3
  results << lambda { i }
end
p results.map(&:call)  # => [3,3,3]

咦,怎么回事?相信我,这类错误很难追踪。 Python 或 JS 开发人员会明白我的意思 :)

这本身就是我避免像瘟疫一样的循环的一个原因,尽管有更多好的论据支持这个立场。正如 Ben 正确指出的那样,使用来自 Enumerable 的正确方法几乎总是比使用普通的旧命令式 for 循环或更高级的 Enumerable#each 生成更好的代码。例如,上面的例子也可以简洁地写成

lambdas = 1.upto(3).map { |i| lambda { i } }
p lambdas.map(&:call)

我扩展了这个问题:在 Ruby 中,我们还能在哪里看到 apears 的作用/结束块分隔符,但实际上里面没有范围?除了 for ... do ... end 还有什么?

每个循环结构都可以这样使用:

while true do
  #...
end

until false do
  # ...
end

另一方面,我们可以在没有do 的情况下编写每一个(这显然是更可取的):

for i in 1..3
end

while true
end

until false
end

问题的另一个扩展,有没有办法用花括号 { block } 编写 for 循环

不,没有。另请注意,术语“块”在 Ruby 中具有特殊含义。

【讨论】:

  • 我也会对此进行扩展(很棒的答案,Niklas B.)。永远不要说永远,但我从不写也不想在 Ruby 代码中看到 for 循环。它们根本不是惯用的,它们为不知情的开发人员引入了令人讨厌的陷阱。请改用Enumerable 提供的东西。
  • @Ben:当然,我完全同意你的观点,我在 OP 所指的讨论中明确表示:)
  • 啊,我知道你有!感谢您开车回家。
  • @NiklasB.:现在,基于此,您是否会建议像 for 循环一样,while 和 until 循环也不建议认真使用?
  • @Boris:是的,通常你也可以避免使用这些。但是,与for 不同,它们没有明显的替代方案,因此您需要考虑一下以用更好的构造替换它们,或者得出结论,它们实际上是特定问题的最佳解决方案。跨度>
【解决方案2】:

首先,我会解释为什么你不想使用for,然后解释为什么你可能会。

您不想使用for 的主要原因是它不习惯。如果您使用each,您可以轻松地将each 替换为mapfindeach_with_index,而无需对代码进行重大更改。但是没有for_mapfor_findfor_with_index

另一个原因是,如果您在 each 的块内创建一个变量,并且之前没有创建它,那么只要该循环存在,它就会一直存在。一旦你不再使用变量就摆脱它们是一件好事。

现在我将提到您可能想要使用for 的原因。 each 为每个循环创建一个闭包,如果重复该循环太多次,该循环可能会导致性能问题。在 https://stackoverflow.com/a/10325493/38765 中,我发布了使用 while 循环而不是块使其变慢的帖子。

RUN_COUNT = 10_000_000
FIRST_STRING = "Woooooha"
SECOND_STRING = "Woooooha"

def times_double_equal_sign
  RUN_COUNT.times do |i|
    FIRST_STRING == SECOND_STRING
  end
end

def loop_double_equal_sign
  i = 0
  while i < RUN_COUNT
    FIRST_STRING == SECOND_STRING
    i += 1
  end
end

times_double_equal_sign 始终需要 2.4 秒,而 loop_double_equal_sign 始终快 0.2 到 0.3 秒。

https://stackoverflow.com/a/6475413/38765 中,我发现执行一个空循环需要 1.9 秒,而执行一个空块需要 5.7 秒。

知道你为什么不想使用for,知道为什么你想使用for,并且只在需要时使用后者。除非你对其他语言感到怀旧。 :)

【讨论】:

    【解决方案3】:

    好吧,在 1.9 之前的 Ruby 中,即使是块也不是完美的。他们并不总是引入新的范围:

    i = 0
    results = []
    (1..3).each do |i|
      results << lambda { i }
    end
    i = 5
    p results.map(&:call)  # => [5,5,5]
    

    【讨论】:

    • 在 Ruby 1.9 中,这应该会产生 [1,2,3],正如预期的那样(此外,它可能会触发有关阴影变量的警告)。
    • @NiklasB。谢谢,很高兴知道,这是 R1.8 的一个真正弱点,顺便说一下,R​​1.9 中没有警告。只是好奇它如何与遗留代码一起工作,我记得在 1.9 讨论它时这是一个严肃的问题
    • 我认为他们在 1.9 的早期版本中有一个警告,让遗留代码的用户意识到这个问题。不过,它似乎在以后的版本中被删除了。
    • 我补充说这仅适用于您的答案的 1.8。希望你不要介意。
    • 可能有点吹毛求疵,但我真的不喜欢使用“不要引入新的范围”这个短语。他们引入了一个新的作用域,但变量在外部作用域中被引用。由于块在 variables 而不是 values 上关闭,所以当 lambda 被 调用 时,您会得到 i 的任何值,而不是当它是 定义.
    猜你喜欢
    • 2020-06-22
    • 2011-03-27
    • 2010-09-22
    • 1970-01-01
    • 2011-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多