控制表达式

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 上方的所有测试都为假时,将执行 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”,因为 1 不等于 0。由于 else 仅在没有匹配条件时执行。

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

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

在此示例中,仅打印“a 是 1”

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 一样,将可选的 thenunless 一起使用。

请注意,上面的 unless 表达式与

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

if 表达式类似,您可以将 else 条件与 unless 一起使用

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

这将从 else 条件打印“该值为 true”。

您不能将 elsifunless 表达式一起使用。

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

修饰符 ifunless

ifunless 也可用于修改表达式。当用作修饰符时,左侧是“then”语句,右侧是“测试”表达式

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“未定义的局部变量或方法 ‘a’”。

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

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

由于测试为 true,因此它执行“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

这里,通过调用 /^1/ === "12345" 将字符串 "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,因此打印“字符串以 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

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 循环一样,除非使用 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 循环的结果值是迭代的值。

在现代 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 的优先级低于 =

触发器

触发器是一种略微特殊的条件表达式。它的典型用途之一是处理 Ruby 一行程序中使用 ruby -nruby -p 的文本。

触发器的形式是一个表达式,指示触发器何时打开,..(或 ...),然后是一个表达式,指示触发器何时关闭。当触发器打开时,它将持续计算为 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