控制表达式¶ ↑
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
上方的测试结果都为假时,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
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”语句,右侧是“测试”表达式。
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#===
。
以下是如何使用 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/ === "12345"
与 /^1/
进行比较,该调用返回 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
,因此打印“字符串以 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
同样,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
。
for
循环在现代ruby程序中很少使用。
修饰符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
的优先级低于=
。
翻转¶ ↑
翻转是一个稍微特殊的条件表达式。它的一种典型用法是处理来自与ruby -n
或ruby -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。
翻转必须在条件语句中使用,例如!
、? :
、not
、if
、while
、unless
、until
等,包括修饰符形式。
当使用包含范围(..
)时,“关闭”条件在“开启”条件改变时进行评估
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
用于在 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#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