控制表达式
Ruby 提供了多种控制执行流程的方式。这里描述的所有表达式都会返回值。
在这些控制表达式的测试中,nil 和 false 被视为假值,而 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 不等于 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
if 和 elsif 的测试可能带有副作用。副作用最常见的用法是将值缓存到局部变量中
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 表达式的结果值是在表达式中最后执行的值。
修饰符 if 和 unless
if 和 unless 也可以用来修饰表达式。当用作修饰符时,左侧是“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#=== 获取示例。
这是一个使用 case 将 String 与模式进行比较的示例
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。
else 和 then 是可选的,此 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
同样,then 和 else 是可选的。
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。
与 while 和 until 一样,do 是可选的。
for 循环类似于使用 each,但它不会创建新的变量作用域。
for 循环的结果值是迭代的值,除非使用 break。
在现代 Ruby 程序中很少使用 for 循环。
修饰符 while 和 until
与 if 和 unless 一样,while 和 until 也可以用作修饰符
a = 0 a += 1 while a < 10 p a # prints 10
until 用作修饰符
a = 0 a += 1 until a > 10 p a # prints 11
您可以使用 begin 和 end 来创建一个 while 循环,该循环在条件检查之前先执行一次循环体
a = 0 begin a += 1 end while a < 10 p a # prints 10
如果您不使用 rescue 或 ensure,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 中,您也可以在 redo 的地方使用 retry。现在已不再是这样,当您在 rescue 块外使用 retry 时,您将收到一个 SyntaxError。请参阅 异常 获取 retry 的正确用法。
修饰符语句
Ruby 的语法区分语句和表达式。所有表达式都是语句(表达式是一种语句),但并非所有语句都是表达式。有些语法部分接受表达式而不接受其他类型的语句,这会导致看起来相似的代码被解析得不同。
例如,当不用作修饰符时,if、else、while、until 和 begin 是表达式(也是语句)。然而,当用作修饰符时,if、else、while、until 和 rescue 是语句但不是表达式。
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 -n 或 ruby -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 必须用在条件语句中,例如 !、? :、not、if、while、unless、until 等,包括修饰符形式。
当您使用包含范围(..)时,当“开启”条件改变时,“关闭”条件会被评估
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
throw 和 catch 用于在 Ruby 中实现非局部控制流。它们的操作方式类似于异常,允许控制直接从调用 throw 的地方传递到调用匹配的 catch 的地方。throw/catch 与异常处理的主要区别在于 throw/catch 是为预期的非局部控制流设计的,而异常是为异常控制流情况设计的,例如处理意外错误。
使用 throw 时,您需要提供 1-2 个参数。第一个参数是匹配的 catch 的值。第二个参数是可选的(默认为 nil),如果是匹配的 throw 在 catch 块内被调用,它将是 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#throw 和 Kernel#catch),而不是关键字。因此,如果您处于 BasicObject 上下文,它们不能直接使用。在这种情况下,您可以使用 Kernel.throw 和 Kernel.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