Ruby中是否有“do ... while”循环?

486

我正在使用这段代码,让用户输入名称,程序将它们存储在数组中,直到他们输入空字符串(每个名称后必须按下回车键):

people = []
info = 'a' # must fill variable with something, otherwise loop won't execute

while not info.empty?
    info = gets.chomp
    people += [Person.new(info)] if not info.empty?
end

使用do ... while循环可以让这段代码看起来更加优美:

people = []

do
    info = gets.chomp
    people += [Person.new(info)] if not info.empty?
while not info.empty?

在这段代码中,我不必将信息分配给某个随机字符串。
不幸的是,Ruby 中似乎不存在这种类型的循环。有人能否建议更好的方法?

1
我认为普通的while循环看起来更好看,而且更容易阅读。 - Magne
1
@Jeremy Ruten,你有没有兴趣将已接受的答案更改为申思维的答案,即loop do; ...; break if ...; end - David Winiecki
10个回答

703

注意:

Ruby的作者Matz拒绝使用begin <code> end while <condition>,而是建议使用Kernel#loop,例如:

loop do 
  # some code here
  break if <condition>
end 

这里是2005年11月23日的电子邮件交流,Matz在其中陈述:

|> Don't use it please.  I'm regretting this feature, and I'd like to
|> remove it in the future if it's possible.
|
|I'm surprised.  What do you regret about it?

Because it's hard for users to tell

  begin <code> end while <cond>

works differently from

  <code> while <cond>

RosettaCode wiki 有一个类似的故事:

在2005年11月,Ruby的创始人松本行弘后悔使用了这个循环特性,并建议使用Kernel#loop。


21
没错。这个“begin end while”方法似乎不太对。谢谢你提供给我说服团队其他成员所需的论据。 - Joshua Pinter
2
看起来 begin-end-while 循环实际上在运行循环之前评估条件。与常规 while 循环的区别在于它保证至少运行一次。它与 do...while 循环非常相似,因此容易引起问题。 - user1992284
4
据我所理解,begin-end-while之所以被“遗憾”,是因为违反了修饰符的语义,也就是说:在执行块之前会先检查它们,例如:puts k if !k.nil?这里的“if”是一个修饰符:它在执行“puts k”语句之前进行检查,以确定是否执行。而当while/until循环用作begin-end块的修饰符时,它们在第一次执行后才被计算。或许正是因为这个原因导致了这种遗憾,但我们是否有比旧论坛帖子更强大的东西,类似于许多其他场合使用的官方废止声明呢? - AgostinoX
3
我用Ruby的“do-while”循环卡了很长时间,最后才明白原因。你应该使用“unless”更接近C语言风格的do-while,否则你可能会像我一样忘记翻转条件:p - Connor Clark
3
@James 根据邮件中的链接,他说他“后悔”添加它。有时候,即使是语言设计师,人们也会犯错误。 - Xiong Chiamiov
显示剩余4条评论

190

I found the following snippet while reading the source for Tempfile#initialize in the Ruby core library:

begin
  tmpname = File.join(tmpdir, make_tmpname(basename, n))
  lock = tmpname + '.lock'
  n += 1
end while @@cleanlist.include?(tmpname) or
  File.exist?(lock) or File.exist?(tmpname)

At first glance, I assumed the while modifier would be evaluated before the contents of begin...end, but that is not the case. Observe:

>> begin
?>   puts "do {} while ()" 
>> end while false
do {} while ()
=> nil

As you would expect, the loop will continue to execute while the modifier is true.

>> n = 3
=> 3
>> begin
?>   puts n
>>   n -= 1
>> end while n > 0
3
2
1
=> nil

While I would be happy to never see this idiom again, begin...end is quite powerful. The following is a common idiom to memoize a one-liner method with no params:

def expensive
  @expensive ||= 2 + 2
end

Here is an ugly, but quick way to memoize something more complex:

def expensive
  @expensive ||=
    begin
      n = 99
      buf = "" 
      begin
        buf << "#{n} bottles of beer on the wall\n" 
        # ...
        n -= 1
      end while n > 0
      buf << "no more bottles of beer" 
    end
end

本文最初由Jeremy Voorhis撰写。因为该文章似乎已被原网站删除,所以将其复制到此处。副本也可以在Web ArchiveRuby Buzz Forum中找到。-Bill the Lizard


5
感谢使用Wayback Machine:http://web.archive.org/web/20080206125158/http://archive.jvoorhis.com/articles/2007/06/13/ruby-hidden-do-while-loop以下是需要翻译的内容:Ruby中隐藏的do-while循环 - bta
57
这就是为什么当链接到外部网站时,我总是确保将相关信息复制到我的答案中的原因。 - davr
11
为什么您希望while修饰符在begin...end的内容之前进行评估?这就是do..while循环应该工作的方式。你为什么会“很高兴永远不再看到这个习语”?它有什么问题吗?我感到困惑。 - bergie3000
2
begin...end 看起来像一个块,同样的 {...}。这没有任何问题。 - Victor Pudeyev
1
-1:申思维的回答解释了begin..end有些不受欢迎。应改用loop do..break if <condition> - Kevin
显示剩余4条评论

106

如果没有输入,这段代码不会将空字符串添加到数组中吗? - AndrewR
1
虽然它在这里并不适用,但 begin-end-while 结构的一个问题是,与所有其他 Ruby 结构不同,它不会返回最后一个表达式的值:“begin 1 end while false” 返回 nil(而不是 1 或 false)。 - tokland
9
你可以使用 until info.empty? 而不是 while not info.empty? - Andrew Grimm
实际上@AndrewR,这正是重点所在..在比较之前做一些事情..当(count>0)时执行display("remaining=#{count}")...我得到了一个显示“remaining=0”的结果..完美! - baash05

44

这个怎么样?

people = []

until (info = gets.chomp).empty?
  people += [Person.new(info)]
end

26
但这不是“do ... while”循环。 :) - Alexander Prokofyev
4
除非我搞错了,否则在这种情况下它所做的事情是相同的。 - Blorgbeard
21
@Blorgbeard,do...while循环总是会运行一次,然后评估是否应继续运行。传统的while/until循环可能不运行任何次。这并不是一个巨大的区别,但它们是不同的。 - Scott Swezey
5
@Scott,没错 - 我的意思只是这段代码与OP的代码等效,尽管它没有使用do/while。实际上,这段代码在条件语句中完成了循环的一半工作,因此它并不是一个传统的while循环 - 如果条件不匹配,仍然会执行一些操作。 - Blorgbeard
问题是是否有人能够建议一种更好的编写代码的方式,这不一定需要使用do while。由于这个回答优雅地回答了问题,+1。 - Eva
显示剩余3条评论

14

现在这个已经正确地运行:

begin
    # statment
end until <condition>

但是,将来可能会删除它,因为begin语句很反直觉。请参考

Matz(Ruby的创建者)建议以这种方式实现:

loop do
    # ...
    break if <condition>
end

12

以下是我从hubbardr的已失效链接所在博客中找到的完整文章。

我在阅读Ruby核心库中Tempfile#initialize源代码时发现了以下片段:

begin
  tmpname = File.join(tmpdir, make_tmpname(basename, n))
  lock = tmpname + '.lock'
  n += 1
end while @@cleanlist.include?(tmpname) or
  File.exist?(lock) or File.exist?(tmpname)

乍一看,我以为while修饰符会在begin...end的内容之前被评估,但事实并非如此。请看:

>> begin
?>   puts "do {} while ()" 
>> end while false
do {} while ()
=> nil

正如你所预期的那样,只要修改器为真,循环就会继续执行。

>> n = 3
=> 3
>> begin
?>   puts n
>>   n -= 1
>> end while n > 0
3
2
1
=> nil

虽然我希望再也不要见到这个习惯用法,但是begin...end非常强大。以下是一种常见的用法,用于备忘一个没有参数的单行方法:

def expensive
  @expensive ||= 2 + 2
end

这里介绍了一种丑陋但快速的方式来记忆化更复杂的东西:

def expensive
  @expensive ||=
    begin
      n = 99
      buf = "" 
      begin
        buf << "#{n} bottles of beer on the wall\n" 
        # ...
        n -= 1
      end while n > 0
      buf << "no more bottles of beer" 
    end
end

6
据我所知,Matz 不喜欢这个结构。
begin
    <multiple_lines_of_code>
end while <cond>

因为它的语义与其他不同

<single_line_of_code> while <cond>

第一种构造先执行代码再检查条件,而第二种构造在执行代码之前首先测试条件(如果有必要)。我认为Matz更喜欢保留第二种构造,因为它与if语句的单行构造相匹配。

我从来不喜欢第二种构造,即使是对于if语句。在所有其他情况下,计算机都是从左到右(例如||和&&)从上到下执行代码。人类读取代码时也是从左到右、从上到下。

我建议使用以下构造代替:

if <cond> then <one_line_code>      # matches case-when-then statement

while <cond> then <one_line_code>

<one_line_code> while <cond>

begin <multiple_line_code> end while <cond> # or something similar but left-to-right

我不知道这些建议是否与语言的其他部分相容。但无论如何,我更喜欢保持从左到右的执行顺序以及语言的一致性。


3
a = 1
while true
  puts a
  a += 1
  break if a > 10
end

3
这看起来有点像goto。这段代码使你的意图变得模糊不清。 - Gerhard
看起来很不错,除了while true可以替换为loop do - David Winiecki
@DavidWiniecki,确实可以用loop do替换while true。但是我在循环内进行了大量迭代的测试,发现while true至少比loop do快2倍。无法解释这种差异,但它确实存在。(在测试2017年Advent of Code第15天时发现。) - Jochem Schulenklopper

2
这是另一个例子:
people = []
1.times do
  info = gets.chomp
  unless info.empty? 
    people += [Person.new(info)]
    redo
  end
end

我更喜欢这种写法,因为 unless 直接放在前面,我不需要读完一堆代码(可能比这里展示的还要多),才发现一个“悬挂”的 unless 在结尾处。在编码中,这是一个普遍的原则,即修改器和条件放在前面使用更容易。 - Michael Durrant
有时候我希望我们程序员每多一次比较就得付出现金。并且,对于使用它一百万次的东西来说,它的“外观”不如它的实际效果重要。 - baash05

-3
ppl = []
while (input=gets.chomp)
 if !input.empty?
  ppl << input
 else
 p ppl; puts "Goodbye"; break
 end
end

2
这看起来有点像goto语句。代码混淆了你的意图,看起来非常不像Ruby风格。 - Gerhard
混淆*而不是混沌。 - qedk

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