控制表达式

Ruby 提供了多种控制执行流程的方式。这里描述的所有表达式都会返回值。

在这些控制表达式的测试中,nilfalse 被视为假值,而 true 和任何其他对象都被视为真值。本文档中“真”将表示“真值”,“假”将表示“假值”。

if 表达式

最简单的 if 表达式包含两个部分:“测试”表达式和“then”表达式。如果“测试”表达式的求值结果为真,则执行“then”表达式。

这是一个简单的 if 语句

if true then
  puts "the test resulted in a true-value"
end

这将打印“测试结果为真值”。

then 是可选的

if true
  puts "the test resulted in a true-value"
end

本文档将省略所有表达式中可选的 then,因为这是 if 最常见的用法。

您还可以添加一个 else 表达式。如果测试不求值为真,则执行 else 表达式

if false
  puts "the test resulted in a true-value"
else
  puts "the test resulted in a false-value"
end

这将打印“测试结果为假值”。

您可以使用 elsif 向 if 表达式添加任意数量的额外测试。当其上方所有测试都为假时,执行 elsif

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
else
  puts "a is some other value"
end

这将打印“a 等于一”,因为 1 不等于 0else 仅在没有匹配的条件时执行。

一旦某个条件匹配,无论是 if 条件还是任何 elsif 条件,if 表达式就完成,不会再进行进一步的测试。

if 类似,elsif 条件后面可以跟一个 then

在此示例中,仅打印“a 等于一”。

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
elsif a >= 1
  puts "a is greater than or equal to one"
else
  puts "a is some other value"
end

ifelsif 的测试可能带有副作用。副作用最常见的用法是将值缓存到局部变量中

if a = object.some_value
  # do something to a
end

if 表达式的结果值是在表达式中最后执行的值。

三元 if

您还可以使用 ?: 来编写 if-then-else 表达式。这个三元 if

input_type = gets =~ /hello/i ? "greeting" : "other"

等同于这个 if 表达式

input_type =
  if gets =~ /hello/i
    "greeting"
  else
    "other"
  end

虽然三元 if 的书写比冗长的形式更简短,但为了可读性,建议仅将三元 if 用于简单的条件判断。此外,避免在同一表达式中使用多个三元条件,因为这会造成混淆。

unless 表达式

unless 表达式是 if 表达式的对立面。如果值为假,则执行“then”表达式

unless true
  puts "the value is a false-value"
end

这不会打印任何内容,因为 true 不是假值。

if 一样,您也可以在 unless 中使用可选的 then

请注意,上面的 unless 表达式等同于

if not true
  puts "the value is a false-value"
end

if 表达式类似,您可以在 unless 中使用 else 条件

unless true
  puts "the value is false"
else
  puts "the value is true"
end

这会从 else 条件打印“值为真”。

您不能在 unless 表达式中使用 elsif

unless 表达式的结果值是在表达式中最后执行的值。

修饰符 ifunless

ifunless 也可以用来修饰表达式。当用作修饰符时,左侧是“then”语句,右侧是“test”表达式

a = 0

a += 1 if a.zero?

p a

这将打印 1。

a = 0

a += 1 unless a.zero?

p a

这将打印 0。

虽然修饰符版本和标准版本都有“test”表达式和“then”语句,但由于解析顺序的原因,它们并不完全等价。下面是一个展示差异的示例

p a if a = 0.zero?

这会引发 NameError “未定义的局部变量或方法 ‘a’”。

当 Ruby 解析此表达式时,它首先在“then”表达式中将 a 视为一个方法调用,然后才在“test”表达式中看到对 a 的赋值,并将其标记为局部变量。

运行此行时,它首先执行“test”表达式 a = 0.zero?

由于测试结果为真,它会执行“then”表达式 p a。由于 a 在方法体中被记录为一个不存在的方法,因此会引发 NameError

unless 也同样如此。

case 表达式

case 表达式有两种用法。

最常见的用法是将一个对象与多个模式进行比较。模式使用 === 方法进行匹配,该方法在 Object 上被别名为 ==。其他类必须重写它才能提供有意义的行为。请参阅 Module#===Regexp#=== 获取示例。

这是一个使用 caseString 与模式进行比较的示例

case "12345"
when /^1/
  puts "the string starts with one"
else
  puts "I don't know what the string starts with"
end

这里,字符串 "12345"/^1/ 通过调用 /^1/ === "12345" 进行比较,结果返回 true。与 if 表达式一样,第一个匹配的 when 会被执行,所有其他匹配都会被忽略。

如果没有找到匹配项,则执行 else

elsethen 是可选的,此 case 表达式与上面的表达式结果相同

case "12345"
when /^1/
  puts "the string starts with one"
end

您可以在同一个 when 上放置多个条件

case "2"
when /^1/, "2"
  puts "the string starts with one or is '2'"
end

Ruby 将依次尝试每个条件,因此首先 /^1/ === "2" 返回 false,然后 "2" === "2" 返回 true,因此会打印“字符串以一开头或等于‘2’”.

您可以在 when 条件后使用 then。这通常用于将 when 的主体放在单行上。

case a
when 1, 2 then puts "a is one or two"
when 3    then puts "a is three"
else           puts "I don't know what a is"
end

case 表达式的另一种用法类似于 if-elsif 表达式

a = 2

case
when a == 1, a == 2
  puts "a is one or two"
when a == 3
  puts "a is three"
else
  puts "I don't know what a is"
end

同样,thenelse 是可选的。

case 表达式的结果值是在表达式中最后执行的值。

自 Ruby 2.7 起,case 表达式还通过 in 关键字提供更强大的模式匹配功能

case {a: 1, b: 2, c: 3}
in a: Integer => m
  "matched: #{m}"
else
  "not matched"
end
# => "matched: 1"

模式匹配语法在其 单独的页面 上有描述。

while 循环

while 循环在条件为真时执行

a = 0

while a < 10 do
  p a
  a += 1
end

p a

打印数字 0 到 10。在进入循环之前检查条件 a < 10,然后执行循环体,然后再次检查条件。当条件结果为假时,循环终止。

do 关键字是可选的。下面的循环等同于上面的循环

while a < 10
  p a
  a += 1
end

while 循环的结果是 nil,除非使用 break 来提供一个值。

until 循环

until 循环在条件为假时执行

a = 0

until a > 10 do
  p a
  a += 1
end

p a

这将打印数字 0 到 11。与 while 循环一样,在进入循环时和每次执行循环体时都会检查条件 a > 10。如果条件为假,循环将继续执行。

while 循环一样,do 是可选的。

while 循环一样,until 循环的结果是 nil,除非使用 break

for 循环

for 循环由 for、一个用于存放迭代参数的变量、in 以及用于迭代的 each 值组成。do 是可选的

for value in [1, 2, 3] do
  puts value
end

打印 1、2 和 3。

whileuntil 一样,do 是可选的。

for 循环类似于使用 each,但它不会创建新的变量作用域。

for 循环的结果值是迭代的值,除非使用 break

在现代 Ruby 程序中很少使用 for 循环。

修饰符 whileuntil

ifunless 一样,whileuntil 也可以用作修饰符

a = 0

a += 1 while a < 10

p a # prints 10

until 用作修饰符

a = 0

a += 1 until a > 10

p a # prints 11

您可以使用 beginend 来创建一个 while 循环,该循环在条件检查之前先执行一次循环体

a = 0

begin
  a += 1
end while a < 10

p a # prints 10

如果您不使用 rescueensure,Ruby 会优化掉任何异常处理开销。

break 语句

使用 break 提前退出块。如果 values 中的某个项是偶数,这将停止迭代

values.each do |value|
  break if value.even?

  # ...
end

您也可以使用 breakwhile 循环中终止

a = 0

while true do
  p a
  a += 1

  break if a < 10
end

p a

这将打印数字 0 和 1。

break 接受一个值,该值将作为其“中断”出的表达式的结果

result = [1, 2, 3].each do |value|
  break value * 2 if value.even?
end

p result # prints 4

next 语句

使用 next 跳过当前迭代的剩余部分

result = [1, 2, 3].map do |value|
  next if value.even?

  value * 2
end

p result # prints [2, nil, 6]

next 接受一个参数,该参数可用作当前块迭代的结果

result = [1, 2, 3].map do |value|
  next value if value.even?

  value * 2
end

p result # prints [2, 2, 6]

redo 语句

使用 redo 重做当前迭代

result = []

while result.length < 10 do
  result << result.length

  redo if result.last.even?

  result << result.length + 1
end

p result

这将打印 [0, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11]

在 Ruby 1.8 中,您也可以在 redo 的地方使用 retry。现在已不再是这样,当您在 rescue 块外使用 retry 时,您将收到一个 SyntaxError。请参阅 异常 获取 retry 的正确用法。

修饰符语句

Ruby 的语法区分语句和表达式。所有表达式都是语句(表达式是一种语句),但并非所有语句都是表达式。有些语法部分接受表达式而不接受其他类型的语句,这会导致看起来相似的代码被解析得不同。

例如,当不用作修饰符时,ifelsewhileuntilbegin 是表达式(也是语句)。然而,当用作修饰符时,ifelsewhileuntilrescue 是语句但不是表达式。

if true; 1 end # expression (and therefore statement)
1 if true      # statement (not expression)

不是表达式的语句不能用在需要表达式的上下文中,例如方法参数。

puts( 1 if true )      #=> SyntaxError

您可以将一个语句包装在括号中来创建一个表达式。

puts((1 if true))      #=> 1

如果您在方法名称和左括号之间留一个空格,则不需要两个括号。

puts (1 if true)       #=> 1, because of optional parentheses for method

这是因为这被解析为类似于不带括号的方法调用。它等同于下面的代码,而不创建局部变量

x = (1 if true)
p x

在修饰符语句中,左侧必须是语句,右侧必须是表达式。

因此,在 a if b rescue c 中,因为 b rescue c 是一个不是表达式的语句,因此不允许作为 if 修饰符语句的右侧,代码必然被解析为 (a if b) rescue c

这与运算符优先级相互作用,以至于

stmt if v = expr rescue x
stmt if v = expr unless x

被解析为

stmt if v = (expr rescue x)
(stmt if v = expr) unless x

这是因为修饰符 rescue 的优先级高于 =,而修饰符 if 的优先级低于 =

Flip-Flop

flip-flop 是一个稍微特殊的条件表达式。它的一个典型用途是处理 ruby 的单行程序(通过 ruby -nruby -p 使用)的文本。

flip-flop 的形式是:一个指示 flip-flop 何时开启的表达式,..(或 ...),然后是一个指示 flip-flop 何时关闭的表达式。当 flip-flop 开启时,它将继续求值为 true,关闭时则为 false

这是一个例子

selected = []

0.upto 10 do |value|
  selected << value if value==2..value==8
end

p selected # prints [2, 3, 4, 5, 6, 7, 8]

在上面的例子中,“开启”条件是 n==2。对于 0 和 1,flip-flop 最初是“关闭”(false)的,但在 2 时变为“开启”(true)并一直保持“开启”到 8。在 8 之后,它会关闭并对 9 和 10 保持“关闭”。

flip-flop 必须用在条件语句中,例如 !? :notifwhileunlessuntil 等,包括修饰符形式。

当您使用包含范围(..)时,当“开启”条件改变时,“关闭”条件会被评估

selected = []

0.upto 5 do |value|
  selected << value if value==2..value==2
end

p selected # prints [2]

这里,flip-flop 的两侧都会被评估,因此只有当 value 等于 2 时,flip-flop 才会开启和关闭。由于 flip-flop 在迭代中开启,它返回 true。

当您使用排除范围(...)时,“关闭”条件会在下一次迭代中进行评估

selected = []

0.upto 5 do |value|
  selected << value if value==2...value==2
end

p selected # prints [2, 3, 4, 5]

这里,当 value 等于 2 时,flip-flop 会开启,但不会在同一迭代中关闭。“关闭”条件直到下一次迭代才会被评估,而 value 永远不会再次等于 2。

throw/catch

throwcatch 用于在 Ruby 中实现非局部控制流。它们的操作方式类似于异常,允许控制直接从调用 throw 的地方传递到调用匹配的 catch 的地方。throw/catch 与异常处理的主要区别在于 throw/catch 是为预期的非局部控制流设计的,而异常是为异常控制流情况设计的,例如处理意外错误。

使用 throw 时,您需要提供 1-2 个参数。第一个参数是匹配的 catch 的值。第二个参数是可选的(默认为 nil),如果是匹配的 throwcatch 块内被调用,它将是 catch 返回的值。如果在 catch 块内没有调用匹配的 throw 方法,catch 方法将返回传递给它的块的返回值。

def a(n)
  throw :d, :a if n == 0
  b(n)
end

def b(n)
  throw :d, :b if n == 1
  c(n)
end

def c(n)
  throw :d if n == 2
end

4.times.map do |i|
  catch(:d) do
    a(i)
    :default
  end
end
# => [:a, :b, nil, :default]

如果您传递给 throw 的第一个参数没有被匹配的 catch 处理,将会引发一个 UncaughtThrowError 异常。这是因为 throw/catch 应该仅用于预期的控制流更改,因此使用一个尚未预期的值是一种错误。

throw/catch 是作为 Kernel 方法实现的(Kernel#throwKernel#catch),而不是关键字。因此,如果您处于 BasicObject 上下文,它们不能直接使用。在这种情况下,您可以使用 Kernel.throwKernel.catch

BasicObject.new.instance_exec do
  def a
    b
  end

  def b
    c
  end

  def c
    ::Kernel.throw :d, :e
  end

  result = ::Kernel.catch(:d) do
    a
  end
  result # => :e
end