跳到內容

控制流程

基本型別

Nil

最簡單的型別是 Nil。它只有一個值:nil,表示缺少實際值。

還記得上一課的 String#index 嗎? 如果子字串在搜尋字串中不存在,它會回傳 nil。它沒有索引,因此索引位置不存在。

p! "Crystal is awesome".index("aw"),
  "Crystal is awesome".index("xxxx")

Bool

Bool 型別只有兩個可能的值:truefalse,表示邏輯和布林代數的真值。

p! true, false

布林值對於管理程式中的控制流程特別有用。

布林代數

以下範例顯示了使用布林值實作布林代數的運算子

a = true
b = false

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

您可以嘗試翻轉 ab 的值,以查看不同輸入值的運算子行為。

真值性

布林代數不僅限於布林型別。所有值都具有隱含的真值性:nilfalse 和空指標(為完整起見,我們稍後會介紹)都是*偽值*。任何其他值(包括 0)都是*真值*。

讓我們在上面的範例中,將 truefalse 替換為其他值,例如 "foo"nil

a = "foo"
b = nil

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

ANDOR 運算子會回傳符合該運算子真值性的第一個運算元值。

p! "foo" && nil,
  "foo" && false,
  false || "foo",
  "bar" || "foo"

NOTXOR 和相等運算子始終回傳 Bool 值 (truefalse)。

控制流程

控制程式的流程是指根據條件採取不同的路徑。到目前為止,本教學中的每個程式都是一系列循序表達式。現在情況將會改變。

條件式

條件子句將一段程式碼分支放在只有在滿足條件時才會開啟的閘門之後。

在最基本的形式中,它由關鍵字 if 後跟作為條件的表達式組成。當表達式的回傳值為*真值*時,則滿足條件。所有後續表達式都是分支的一部分,直到它以關鍵字 end 關閉。

依照慣例,我們將巢狀分支縮排兩個空格。

以下範例僅在滿足以 Hello 開頭的條件時才會列印訊息。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

注意

從技術上講,此程式仍然以預定義的順序執行。固定的訊息始終匹配並使條件為真值。但假設我們不在原始碼中定義訊息的值。它也可以來自使用者輸入,例如聊天客戶端。

如果訊息的值不是以 Hello 開頭,則會跳過條件分支,並且程式不會列印任何內容。

條件表達式可以更複雜。使用布林代數,我們可以建構一個接受 HelloHi 的條件

message = "Hello World"

if message.starts_with?("Hello") || message.starts_with?("Hi")
  puts "Hey there!"
end

讓我們將條件反轉:僅在訊息*不*以 Hello 開頭時才列印訊息。這只是與先前範例的一個小偏差:我們可以使用否定運算子 (!) 將條件轉為相反的表達式。

message = "Hello World"

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

另一種選擇是將 if 替換為關鍵字 unless,它只會預期相反的真值性。unless x 等效於 if !x

message = "Hello World"

unless message.starts_with?("Hello")
  puts "I didn't understand that."
end

讓我們看看一個使用 String#index 尋找子字串並醒目提示其位置的範例。還記得如果找不到子字串,它會回傳 nil 嗎?在這種情況下,我們無法醒目提示任何內容。因此,我們需要一個 if 子句,其中包含一個檢查索引是否為 nil 的條件。.nil? 方法非常適合這個。

str = "Crystal is awesome"
index = str.index("aw")

if !index.nil?
  puts str
  puts "#{" " * index}^^"
end

編譯器會強制您處理 nil 的情況。嘗試移除條件式或將條件變更為 true:會出現一個型別錯誤,解釋說您無法在該表達式中使用 Nil 值。透過適當的條件,編譯器知道 index 在分支內部不能為 nil,並且可以將其用作數值輸入。

提示

if !index.nil? 的較短形式是 if index,它們大致相等。如果您想區分偽值是 nil 還是 false,這才會有所不同,因為前一個條件與 false 匹配,而後者則不會。

Else

讓我們改進程式,並在訊息符合條件或不符合條件的兩種情況下做出反應。

我們可以將其作為兩個具有否定條件的單獨條件式執行

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

這可行,但有兩個缺點:條件表達式 message.starts_with?("Hello") 會評估兩次,這很沒有效率。稍後,如果我們在一個地方變更條件(也許也允許 Hi),我們可能會忘記也變更另一個條件。

條件式可以有多個分支。另一個分支由關鍵字 else 指示。如果未滿足條件,則會執行它。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
else
  puts "I didn't understand that."
end

更多分支

我們的程式只對 Hello 做出反應,但我們想要更多互動。讓我們新增一個分支來回應 Bye。我們可以在同一個條件式中針對不同的條件使用分支。這就像另一個整合的 ifelse。因此關鍵字為 elsif

message = "Bye World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.starts_with?("Bye")
  puts "See you later!"
else
  puts "I didn't understand that."
end

如果先前沒有滿足任何條件,else 分支仍然只會執行。不過,它始終可以省略。

請注意,不同的分支是互斥的,並且條件從上到下評估。在上面的範例中,這無關緊要,因為兩個條件不能同時為真值(訊息不能同時以 HelloBye 開頭)。不過,我們可以新增一個非互斥的替代條件來示範這一點

message = "Hello Crystal"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.includes?("Crystal")
  puts "Shine bright like a crystal."
end

if message.includes?("Crystal")
  puts "Shine bright like a crystal."
elsif message.starts_with?("Hello")
  puts "Hello to you, too!"
end

兩個子句都有相同條件的分支,但順序不同,並且行為不同。第一個匹配條件會選取執行哪個分支。

迴圈

本節介紹程式碼重複執行的基礎概念。

最基本的功能是 while 子句。它的結構與 if 子句非常相似:關鍵字 while 表示開始,後面跟著一個作為迴圈條件的表達式。所有後續的表達式都是迴圈的一部分,直到結束關鍵字 end。只要條件的回傳值為真值,迴圈就會持續重複執行。

讓我們試著寫一個簡單的程式來從 1 數到 10

counter = 0

while counter < 10
  counter += 1

  puts "Counter: #{counter}"
end

whileend 之間的程式碼會執行 10 次。它會印出目前的計數器值並將其加一。在第 10 次迭代後,counter 的值為 10,因此 counter < 10 失敗,迴圈終止。

另一種方法是將 while 替換為關鍵字 until,它期望的是相反的真值。until x 等同於 while !x

counter = 0

until counter >= 10
  counter += 1

  puts "Counter: #{counter}"
end

提示

您可以在語言規範中找到有關這些表達式的更多詳細資訊:whileuntil

無窮迴圈

使用迴圈時,重要的是要注意迴圈條件在某個時間點變為假值。否則,它將永遠繼續執行,或者直到您從外部停止程式(例如 Ctrl+Ckill、拔掉插頭,或是世界末日來臨)。

在這個範例中,如果不增加計數器,它會等同於寫成

while true
  puts "Counter: #{counter}"
end

或者,如果條件是 counter > 0,它會匹配所有值:它們只會從 1 開始遞增。這在技術上不會是無限的,因為當計數器達到 32 位元整數的最大值時,它會因數學錯誤而失敗。但在概念上,它類似於一個無限迴圈。這種邏輯錯誤很容易被忽略,因此在編寫迴圈條件時,以及注意滿足中斷條件時,務必非常小心。對於索引變數(例如我們範例中的 counter),一個好的做法是在迴圈開始時遞增它們。這樣可以減少忘記更新它們的可能性。

提示

幸運的是,語言中有許多功能可以減輕手動編寫迴圈的負擔,並且還可以確保有效的跳脫條件。其中一些將在後續的課程中介紹。

在某些情況下,目的是真的要建立一個無限迴圈。一個例子是伺服器總是重複等待連線,或是命令處理器等待使用者輸入。那麼,這應該是很明顯的,而不是隱藏在一個複雜的、永遠不會失敗的迴圈條件中。表達它的最簡單方法是 while true。條件 true 永遠是真值,因此迴圈會無限重複。

while true
  puts "Hi, what's your name? (hit Enter when done)"

  # `gets` returns input from the console
  name = gets

  puts "Nice to meet you, #{name}."
  puts "Now, let's repeat."
end

注意

此範例並非刻意設計為互動式遊樂場,因為遊樂場無法處理非自行終止的程式和處理使用者輸入。它只會超時並印出錯誤訊息。不過,您可以使用本機編譯器編譯並執行此程式碼。

要停止程式,請按下 Ctrl+C。這會傳送一個訊號給程序,要求它退出。

跳過和中斷

在某些條件下,跳過某些迭代或完全停止迭代可能會很有用。

迴圈主體內的關鍵字 next 會跳到下一次迭代,忽略目前迭代中剩餘的任何表達式。如果迴圈條件不滿足,迴圈會結束,並且主體不會再執行一次。

counter = 0

while counter < 10
  counter += 1

  if counter % 3 == 0
    next
  end

  puts "Counter: #{counter}"
end

這個範例可以很容易地在沒有 next 的情況下寫出來,方法是將 puts 表達式放在條件式中。當方法主體中有更多表達式需要跳過時,next 的價值就會顯現出來。

迴圈條件可能很難計算,例如,因為它們需要多個步驟或依賴於需要判斷的輸入。在這種情況下,將所有邏輯寫在迴圈條件中不是很實用。關鍵字 break 可以用在迴圈主體中的任何位置,並且可以作為額外選項,無論其迴圈條件如何,都可以中斷迴圈。控制流程會立即繼續到迴圈結束之後。

counter = 0

while true
  counter += 1

  puts "Counter: #{counter}"

  if counter >= 10
    break
  end
end

puts "done"