控制表達式

Ruby 有多種方式來控制執行。這裡描述的所有表達式都會傳回一個值。

對於這些控制表達式中的測試,nilfalse 是 false 值,而 true 和任何其他物件都是 true 值。在此文件中,「true」表示「true 值」,而「false」表示「false 值」。

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

這不會印出任何東西,因為 true 並非 false 值。

你可以使用一個選用的 thenunless,就像 if 一樣。

請注意,以上的 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」。

你不能在 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?

由於測試為 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

在此,字串 "12345"/^1/ 進行比較,方法是呼叫 /^1/ === "12345",它會傳回 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。

如同 whileuntildo 是選用的。

for 迴圈類似於使用 each,但不會建立新的變數範圍。

for 迴圈的結果值是反覆運算的值,除非使用 break

for 迴圈很少用於現代的 ruby 程式。

修改器 whileuntil

如同 ifunlesswhileuntil 可用作修改器

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 中,您也可以在使用 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。「開啟」後,它會關閉並對 9 和 10 保持「關閉」狀態。

反轉必須用在條件式中,例如 !? :notifwhileunlessuntil 等,包括修改器形式。

當你使用包含範圍(..)時,「關閉」條件會在「開啟」條件改變時評估

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

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