控制表达式

Ruby 有多种控制执行的方式。这里描述的所有表达式都会返回一个值。

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

if 表达式

最简单的 if 表达式有两个部分:一个“测试”表达式和一个“then”表达式。如果“测试”表达式的计算结果为真,则计算“then”表达式。

这是一个简单的 if 语句

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

这将打印 “the test resulted in a true-value”(测试结果为真值)。

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

这将打印 “the test resulted in a false-value”(测试结果为假值)。

您可以使用 elsif 向 if 表达式添加任意数量的额外测试。当 elsif 上面的所有测试都为假时,将执行 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 is one”(a 是 1),因为 1 不等于 0。由于 else 仅在没有匹配条件时执行。

一旦条件匹配(if 条件或任何 elsif 条件),if 表达式就完成,并且不再执行其他测试。

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

在此示例中,仅打印 “a is one”。

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 条件打印 “the value is true”(该值为真)。

您不能在 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。

虽然修饰符版本和标准版本都有“测试”表达式和“then”语句,但由于解析顺序,它们并不是彼此的精确转换。以下示例显示了差异

p a if a = 0.zero?

这将引发 NameError “undefined local variable or method ‘a’”(未定义的局部变量或方法 'a')。

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

运行此行时,它首先执行“测试”表达式,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/ === "12345"/^1/ 进行比较,该调用返回 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,因此打印 “the string starts with one or is ‘2’”(字符串以 1 开头或为 '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

除非使用 break 提供一个值,否则 while 循环的结果为 nil

until 循环

当条件为假时,将执行 until 循环

a = 0

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

p a

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

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

while 循环一样,除非使用 break,否则 until 循环的结果为 nil。

for 循环

for 循环由 for 后跟一个包含迭代参数的变量,后跟 in 和使用 each 迭代的值组成。do 是可选的

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

打印 1、2 和 3。

whileuntil 一样,do 是可选的。

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

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

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

修饰符 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

你也可以使用 break 来终止 while 循环。

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 中,你还可以使用 retry 来替代 redo。但现在情况并非如此,现在你在 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)

触发器是一种稍微特殊的条件表达式。它的一个典型用途是处理来自与 ruby -nruby -p 一起使用的 ruby 单行程序的文本。

触发器的形式是一个表达式,指示触发器何时打开,..(或 ...),然后是一个指示触发器何时关闭的表达式。当触发器打开时,它将继续计算为 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,触发器最初是“关闭”的(false),但对于 2 变为“打开”的(true),并且在 8 之前保持“打开”状态。在 8 之后,它关闭,并且对于 9 和 10 保持“关闭”状态。

触发器必须在条件语句内部使用,例如 !? :notifwhileunlessuntil 等,包括修饰符形式。

当你使用包含范围 (..) 时,“关闭”条件会在“打开”条件更改时进行评估。

selected = []

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

p selected # prints [2]

在这里,触发器的两侧都会被评估,因此触发器仅在 value 等于 2 时打开和关闭。由于触发器在迭代中打开,它返回 true。

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

selected = []

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

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

在这里,触发器在 value 等于 2 时打开,但在同一迭代中不会关闭。“关闭”条件直到下一次迭代才会评估,并且 value 将永远不会再次等于 2。

throw/catch

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

当使用 throw 时,你提供 1-2 个参数。第一个参数是匹配的 catch 的值。第二个参数是可选的(默认为 nil),如果 catch 代码块内有匹配的 throw,则该参数将是 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