控制表达式

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 不等于 0。由于只有在没有匹配条件时才执行 else

一旦条件匹配(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

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

您可以像 if 一样使用可选的 thenunless

请注意,上面的 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 解析此表达式时,它首先将 a 视为“then”表达式中的方法调用,然后它在“test”表达式中看到对 a 的赋值,并将 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/ === "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 循环一样,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

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。现在不再是这样了,现在当你使用 retry 时,你会收到一个 SyntaxError,而不在 rescue 块之外。有关 retry 的正确用法,请参阅 Exceptions

修饰符语句

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 -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