Ruby的for循环是一个陷阱吗?

16
在讨论Ruby循环时,Niklas B.最近谈到for循环“不引入新的作用域”,与每个循环相比。我想看看有哪些示例可以说明这一点。
好的,我进一步扩展问题:除了for ... do ... end之外,在Ruby中还有哪些地方我们看到类似于do/end块分隔符,但实际上内部没有作用域?还有其他吗?
好的,再扩展一个问题,是否有一种方式可以使用花括号{block}编写for循环呢?

只是一个小提示(我已经在另一个评论中说过类似的话):for 的问题不仅在于它与使用 do/end 的其他结构有微妙的差异,而且它只能用于一个原因:遍历可枚举对象。通常,来自函数式编程的结构,如 selectmap,比起从 C 直接继承的普通循环更适合表达问题。 - Niklas B.
那么反过来呢:您可以给出实际的例子,说明您认为需要使用 for 循环,我们再给您提供更好的替代方案? :) 否则我认为这个问题只是 https://dev59.com/aXA75IYBdhLWcg3wdIl0 的重复。 - Niklas B.
1
性能如何?普通循环难道不比输入闭包更快吗?如果你想在for循环中定义局部变量以供稍后使用怎么办呢?(只是对可能有用的东西的快速思考 :)) - Boris Stitnicky
2
通常情况下,你应该更加注重代码的可读性而非性能,否则你一开始为什么要使用Ruby。在循环中定义变量以便稍后使用,这种做法会导致非常糟糕的设计。 - Niklas B.
3个回答

14

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

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 中,除了 for ... do ... end 之外,我们是否还能看到类似的出现 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 中术语“block”有特殊的含义。


3
我也会进一步扩展(很棒的回答,Niklas B.)。虽然不要说永远,但我 从不 写也不想看到 Ruby 代码中的 for 循环。它们完全不符合惯用语法,并且会给不知情的开发人员带来麻烦。改用 Enumerable 中提供的东西代替。 - Ben Kreeger
@Ben:当然,我完全同意你的观点,并且在OP所提到的讨论中已经明确表达了我的看法 :) - Niklas B.
啊,我明白了,你已经做到了!感谢你的努力。 - Ben Kreeger
@NiklasB.:基于这个,您会推荐像for循环一样,while和until循环也不建议严肃使用吗? - Boris Stitnicky
1
@Boris:是的,通常你也可以避免使用它们。然而,与“for”不同,它们没有明显的替代方案,因此你需要思考一下如何用更好的结构来替换它们,或者得出它们实际上是解决特定问题的最佳方案的结论。 - Niklas B.

3
首先,我将解释为什么您不想使用for,然后再解释为什么您可能想要使用它。
您不想使用for的主要原因是它不符合惯用语法。如果您使用each,则可以轻松地将该each替换为mapfindeach_with_index而不会对代码进行重大更改。但是没有for_mapfor_findfor_with_index
另一个原因是,如果您在each中的块内创建了一个变量,并且它之前没有被创建过,那么它只会在该循环存在的时间内存在。一旦您不再需要变量,将其清除是一件好事。
现在我将提到您可能想要使用for的原因。每次循环each都会创建一个闭包,如果您重复执行该循环太多次,该循环可能会导致性能问题。在https://dev59.com/CWkv5IYBdhLWcg3w6lHj#10325493中,我发帖说使用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://dev59.com/0ljUa4cB1Zd3GeqPQlpF#6475413中,我发现执行一个空循环需要 1.9 秒,而执行一个空块需要 5.7 秒。
了解何时不应使用 for,何时应该使用 for,并且只有在必要的情况下才使用后者。除非你想怀旧其他编程语言。 :)

1

嗯,在 Ruby 1.9 之前,即使是块也不完美。它们并不总是引入新的作用域:

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](同时可能会触发有关被屏蔽变量的警告)。 - Niklas B.
@NiklasB。谢谢,知道了,这是R1.8的一个非常薄弱的点,但在R1.9中没有警告。只是好奇它如何处理遗留代码,我记得当它被讨论到1.9时,这是一个严重的问题。 - Victor Moroz
我认为在1.9的早期版本中,他们对此发出了警告,以使使用遗留代码的用户意识到问题。尽管在后来的版本中似乎已经删除了这个警告。 - Niklas B.
我在你的回答中补充了这只适用于1.8版本的事实。希望你不介意。 - Niklas B.
也许这只是一个小问题,但我真的不喜欢使用“不要引入新的_作用域_”这个短语。它们确实引入了一个新的_作用域_,但变量被引用在外部作用域中。由于块关闭变量而不是,所以当lambda被_调用_时,你得到的是i的值,而不是它被_定义_时的值。 - Jon Wingfield

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接