控制表达式

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 一样在 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”语句,右侧是“测试”表达式。

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?

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

您也可以使用 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 -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。

抛出/捕获

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