控制结构

(与C等语言不同的是)Ruby的控制结构是表达式,其中的一部分还会返回值(也有不返回值的,若把这些不返回值的表达式放在赋值表达式右边的话,就会引发 parse error)。

Ruby中包括从C和Perl那里继承来的控制结构,还包括一种可以将控制结构抽象化的功能,即 带块的方法调用。带块的方法调用使类的设计者可以自己定义一些包括循环在内的控制结构。

条件分支

if

例:

if age >= 12 then
  print "adult fee\n"
else
  print "child fee\n"
end
gender = if foo.gender == "male" then "male" else "female" end

语法:

if 表达式 [then]
  表达式 ...
[elsif 表达式 [then]
  表达式 ... ]
...
[else
  表达式 ... ]
end

若条件表达式的计算结果为真时,将计算then以下的表达式。若if的条件表达式为伪时,将计算elsif的条件部分。可以存在若干个elsif部分,若所有的if以及elsif的条件表达式都为伪的话,如果有else部分,就计算它的表达式。

if 表达式的结果取决于条件成立部分(或else部分)中最后被计算的表达式的结果。若没有else部分,且所有条件均不成立的话,就返回nil。

Ruby中只有falsenil代表伪,其他都是真,甚至0或空字符串也是如此。

请注意,在Ruby中,和if对应的是elsif,而并非else if(C的语句)或者elif(sh的语句)。

另外,当if 条件表达式中出现正则表达式字面值时,将作如下处理

$_ =~ 字面值

if 修饰句

例:

print "debug\n" if $DEBUG

语法:

表达式 if 表达式

当右边的条件成立时,计算左边的表达式,并返回其结果。若条件不成立则返回nil。

unless

例:

unless baby?
  feed_meat
else
  feed_milk
end

语法:

unless 表达式 [then]
  表达式 ...
[else
  表达式 ... ]
end

unlessif相反,当条件表达式结果为伪时,才计算then后面的表达式。unless表达式中不能安插elsif语句。

unless 修饰句

例:

print "stop\n" unless valid(passwd)

语法:

表达式 unless 表达式

当右边的条件不成立时,计算左边的表达式,并返回其结果。若条件不成立时返回nil。

case

例:

case $age
when 0 .. 2
  "baby"
when 3 .. 6
  "little child"
when 7 .. 12
  "child"
when 13 .. 18
  "youth"
else
  "adult"
end

语法:

case [表达式]
[when 表达式 [, 表达式] ...[, `*' 表达式] [then]
  表达式..]..
[when `*' 表达式 [then]
  表达式..]..
[else
  表达式..]
end

case先对一个表达式进行匹配判断,然后根据匹配结果进行分支选择。它使用"==="操作符比较when的指定值和最初那个表达式的计算值,若一致的话就计算when部分的内容。

也就是说

case 表达式0
when 表达式1, 表达式2
  stmt1
when 表达式3, 表达式4
  stmt2
else
  stmt3
end

基本上等同于下面的if表达式。

_tmp = 表达式0
if 表达式1 === _tmp or 表达式2 === _tmp
  stmt1
elsif 表达式3 === _tmp or 表达式4 === _tmp
  stmt2
else
  stmt3
end

when 部分的计算顺序同上面这个if句的计算顺序是相同的。即从上到下(从左到右)地计算"==="。另外,“表达式0”只计算一次。

若when 部分中的最后一个表达式前带"*"的话,该表达式将被当作数组展开。

ary = [1,2,3]

case v
when *ary
 ..
end

等同于

case v
when 1, 2, 3
 ..
end

请参考描述各个类中"==="方法技术细节的文档,来了解"==="在何种条件下为真。

case的“表达式”部分被省略时,将计算第一个when条件部分为真的表达式。

foo = false
bar = true
quu = false

case
when foo then puts 'foo is true'
when bar then puts 'bar is true'
when quu then puts 'quu is true'
end
# 显示 "bar is true"

case将返回条件成立的when部分(或else部分)中最后被计算的表达式的结果。若所有条件都不成立的话,则返回nil。

循环

while

例:

ary = [0,2,4,8,16,32,64,128,256,512,1024]
i = 0
while i < ary.length
  print ary[i]
  i += 1
end

语法:

while 表达式 [do]
   ...
end

只要表达式的计算值为真,就循环执行while语句中的内容。while不返回值。

ruby 1.7 特性: while返回nil。另外,可以使用带参数的break,将while表达式的返回值设为那个参数的值。

while 修饰句

例:

sleep(60) while io_not_ready?

语法:

表达式 while 表达式

只要右边表达式的计算值为真,就循环执行左边部分。

若左边表达式是begin,且即不含rescue,又不含ensure的话,则只在开始时计算一次然后就执行循环。

ruby 1.7 特性: 在version 1.7中,即使出现rescue/ensure部分也会作相同处理。

例:

send_request(data)
begin
  res = get_response()
end while res == 'Continue'

while 修饰的表达式没有返回值。

ruby 1.7 特性: while修饰的表达式返回nil。另外,可以使用带参数的break,将while修饰的表达式的返回值设为那个参数的值。

until

例:

until f.eof?
  print f.gets
end

语法:

until 表达式 [do]
   ...
end

在表达式的计算值变为真之前,一直循环执行until中的内容。until不返回值。

ruby 1.7 特性: until 返回 nil。另外,可以使用带参数的break,将until表达式的返回值设定为那个参数的值。

until修饰句

例:

print(f.gets) until f.eof?

语法:

表达式 until 表达式

在右边表达式的计算值变为真之前,一直循环执行左边部分。

若左边表达式是begin,且即不含rescue,又不含ensure的话,则只在开始时计算一次然后就执行循环。

ruby 1.7 特性: 在version 1.7中,即使出现rescue/ensure部分也会作相同处理

例:

send_request(data)
begin
  res = get_response()
end until res == 'OK'

until修饰的表达式没有返回值。

ruby 1.7 特性: until修饰的表达式返回nil。另外,可以使用带参数的break,将until修饰的表达式的返回值设为那个参数的值。

for

例:

for i in [1, 2, 3]
  print i*2, "\n"
end

语法:

for lhs ...  in 表达式 [do]
  表达式..
end

先计算表达式得到一个对象,然后分别针对该对象中的每个要素,循环执行for的内容。这基本等价于

(表达式).each `{' `|' lhs..`|' 表达式.. `}'

之所以说“基本”是因为,do...end以及由{}构成的块中导入了新的局部变量的有效范围,而for语句对于局部变量的有效范围没有任何影响。

for将返回in所指对象的each方法的返回值。

若想使用多个循环变量的话,可以这样

for i,j in [[1,2], [3,4], [5,6]]
  p [i,j]
end
=> [1, 2]
   [3, 4]
   [5, 6]

使用for或each时,每次只能取一个数组元素进行循环,而不能一次取多个。

for i,j in [1, 2, 3]
  p [i,j]
end

=> [1, nil]
   [2, nil]
   [3, nil]

# 可能您希望这样[1,2] [3,nil],但实际上这是行不通的

您必须自己定义这样的方法(迭代器)。请参考 each

class Array
  def each2
    i = 0
    while i < self.size
      yield self[i], self[i+1]
      i += 2
    end
  end
end

break

例:

i = 0
while i < 3
  print i, "\n"
  break
end

语法:

break

break val             ruby 1.7 特性

break将退出最内层的循环。所谓循环是指,下列之一

与C语言不同,break只能从循环中退出,而不能从case中退出。

若使用break退出for或迭代循环后,该循环将返回nil。ruby 1.7 特性:但如果使用了参数的话,循环将返回那个参数的值。

例:

# 忽略空行的cat
ARGF.each_line do |line|
  next if line.strip.empty?
  print line
end

语法:

next

next val              ruby 1.7 特性

next将跳转到最内侧循环的头部。在迭代器中,它将跳离yield调用。

使用next跳离yield后,yield表达式将返回nil。ruby 1.7 特性:但如果使用了参数的话,yield表达式的返回值就是该参数的值。

redo

例:

redo

语法:

redo

不检查循环条件,重新开始当前循环。

retry

例:

retry

语法:

retry

在迭代、块或for语句中使用retry,意味着重启迭代器。同时迭代器的参数也将被重新计算。

for i in 1..5
  retry if some_condition # 从 i == 1 开始重新执行
end

# 用户定义的 "until循环"
def UNTIL(cond)
  return if cond
  yield
  retry
end

除了循环以外,还可以在rescue部分(后述)中使用retry。这时将从begin表达式开始重新执行。使用retry可以在某处理过程成功之前,一直循环该处理过程。

begin
  do_something # exception raised
rescue
  # handles error
  retry  # restart from beginning
end

若在rescue部分、迭代器块或for语句之外使用retry的话会引发LocalJumpError异常。

归纳一下,在迭代调用中使用break, next, redo, retry的作用如下。

def iter
 (a)
  :
 (b)
 yield
 (c)
  :
 (d)
end
iter { retry }   -> 跳到 (a)
iter { redo  }   -> 跳到 (b)
iter { next  }   -> 跳到 (c)
iter { break }   -> 跳到 (d)

严格地讲(a)是从计算参数开始的。(b)指的是即将开始执行块的时候(yield的参数不会被再次计算)。(d)指的是方法的终结。

def iter(var = p("(a)"))
  p " : "
  yield
  p "(c)"
  p " : "
ensure
  p "(d)"
end

iter { p "(b)"; retry }     # => (a) .. (b)(d)(a) .. (b)(d)(a) ...
iter { p "(b)"; redo  }     # => (a) .. (b)(b)(b)(b) ...
iter { p "(b)"; next  }     # => (a) .. (b)(c) .. (d)
iter { p "(b)"; break }     # => (a)..(b)(d)

异常处理

raise

例:

raise "you lose"  # 引发RuntimeError异常
# 下面两个将引发SyntaxError异常
raise SyntaxError, "invalid syntax"
raise SyntaxError.new("invalid syntax")
raise             # 再次引发上一个异常

语法:

raise
raise message或exception
raise error_type, message
raise error_type, message, traceback

引发异常。第一句将再次引发上一个异常。第二句中,若参数是字符串的话,就把它当作错误信息(message)再引发RuntimeError异常。若参数为异常对象则引发该异常。第三句中,将引发第一个参数所指的异常,并以第二个参数的内容作为错误信息。第四句中,第三参数装载的是源自于$@caller的堆栈信息,它指明发生异常的地点。

可以使用begin表达式的rescue部分来捕捉异常。这时使用rescue error_type => var就可以得到异常对象。您还可以从内部变量$!中获得这个对象。另外,变量$@中装载的是发生异常的源代码位置。

raise并不是Ruby的保留字,它是Kernel模块中定义的函数式的方法。

begin

例:

begin
  do_something
rescue
  recover
ensure
  must_to_do
end

语法:

begin
  表达式..
[rescue [error_type,..] [=> evar] [then]
  表达式..]..
[else
  表达式..]
[ensure
  表达式..]
end

若给出了rescue部分(可以有若干个)的话,就可以在发生异常时捕捉到它。若存在与异常类型一致的rescue部分的话,就执行rescue的内容。可以使用$!来查看异常的情况。另外,若事先设定了变量evar的话,它也可以像$!一样存储那些异常的信息。

begin
  raise "error message"
rescue => evar
  p $!
  p evar
end
# => #<RuntimeError: error message>
     #<RuntimeError: error message>

rescue部分使用Object#kind of?来判断刚才的异常的类是否就是自己期待的异常类,或者这二者是否处于父类/子类的关系。

error_type被省略,则将捕捉StandardError的子类中的所有异常。Ruby的内部异常(除了SystemExitInterrupt这些退出命令以外)是StandardError的子类。

请参考异常类来了解异常类的层次关系。

rescue部分中,error_type与普通的参数一样接受计算,若符合的话就执行相应部分的内容。若error_type的计算值既非类又非模块的话,则引发TypeError异常。

若运行过程中没发生异常,则开始计算可选的else部分。

若存在ensure部分的话,则在begin表达式结束之前一定会计算它。

begin表达式整体的计算值取决于,begin的内容部分/rescue部分/else部分中最后被计算的句子的值。若各部分中均无语句时,其值为nil。不管怎样,ensure部分的值始终会被忽略。

rescue修饰句

例:

open("nonexistent file") rescue STDERR.puts "Warning: #$!"

语法:

表达式1 rescue 表达式2

若表达式1中发生异常时就计算表达式2。这等同于下例。不能指定想捕捉的异常类。(也就是说,只能捕捉StandardError异常类的子类了)

begin
  表达式1
rescue
  表达式2
end

在包括rescue修饰句的表达式中,若没发生异常则返回表达式1的值,若发生异常则返回表达式2的值。但在大多数场合中,因为考虑到优先度的问题,所以需要使用括号将整个表达式括起来。

var = open("nonexistent file") rescue false
p var
=> nil      # 因为只定义了一个空变量var

var = (open("nonexistent file") rescue false)
p var
=> false

特别是传递给某方法的参数时,有必要使用双重括号。

p(open("nonexistent file") rescue false)
=> parse error

p((open("nonexistent file") rescue false))
=> false

ruby 1.7 特性: 在1.7中,rescue的优先度发生了变化,因此免去了这些烦恼。

var = open("nonexistent file") rescue false
p var
=> false

p(open("nonexistent file") rescue false)
=> false

其他

return

例:

return
return 12
return 1,2,3

语法:

return [表达式[`,' 表达式 ... ]]

结束方法的运行,且把表达式的值设定为方法的返回值。若给出了2个以上的表达式,则将把这些表达式化为一个数组,然后把该数组设定为方法的返回值。若省略表达式,将返回值设为nil。

BEGIN

例:

BEGIN {
   ...
}

语法:

BEGIN '{' 语句.. '}'

注册初始化例程(routine)。BEGIN块所指定的语句的执行顺序将先于该文件中任何语句。若有多个BEGIN块的话,将按照出现顺序依次执行。

BEGIN块在编译时被注册。也就是说,同一条语句只会被注册一次。

if false
  BEGIN { p "begin" }
end

# => "begin"

BEGIN块引入了独立的局部变量作用域,因此不能和外部共享局部变量。为了与块外交换信息,必须借助于常数或全局变量。

BEGIN { $foo, foo = true, true }
p $foo  # => true
p foo   # undefined local variable or method `foo' for main:Object (NameError)

BEGIN不能出现在方法定义表达式中,否则会引发 parse error。

def foo
  BEGIN { p "begin" }
end
# => -:2: BEGIN in method

END

例:

END {
   ... 
}

语法:

END '{' 语句.. '}'

注册“善后”例程。END块中指定的语句会在解释器结束前得到执行。关于Ruby退出程序时的相关处理问题,请参考结束时的相关处理

若注册了若干END块的话,则以与注册时相反的顺序依次执行这些块。

END { p 1 }
END { p 2 }
END { p 3 }

# => 3
     2
     1

END块中同一条语句只会执行一次。如下例,即使把END块置入循环中,也只会注册一次。若想实现复用,请使用 at_exit

5.times do |i|
  END { p i }
end
# => 0

若把END块置入方法定义表达式中会引起警告。若有意如此,请使用at_exit

def foo
  END { p "end" }
end
p foo

# => -:2: warning: END in method; use at_exit
     nil
     "end"

END与BEGIN不同的是,它在运行时进行注册。因此,下例中的END块将不会运行。

if false
  END { p "end" }
end

END和at_exit中注册的善后处理无法取消。

END块与BEGIN块不同的是,它同周围部分共享作用域。也就是说,它的作用域同迭代器一样。

若END块中发生了异常,将中断该块。但解释器并不结束,只是发出信息,并且试图处理完所有的善后例程。

例:

END { p "FOO" }
END { raise "bar"; p "BAR" }
END { raise "baz"; p "BAZ" }

=> baz (RuntimeError)
   bar (RuntimeError)
   "FOO"