Ruby 使用手冊

繼續簡單示範

現在我們來分析一下之前的程式範例。

下列程式碼在簡單示範一節中出現過。

def fact(n)
  if n == 0
    1
  else
    n * fact(n-1)
  end
end
puts fact(ARGV[0].to_i)

因為這是我們第一次解釋,所以每一行都會仔細說明。

階乘

def fact(n)

第 1 行中,def 用來定義函數 (function) 的敘述(或者準確來說是方法 (method);我們會在之後的章節探討甚麼是方法)。這裡指出函數 fact 有一個引數 (argument),即是 n

if n == 0

if 是用來判斷條件 (condition)。若條件成立,便運行下一行程式碼,否則就運行 else 之後的程式碼。

1

若條件成立,if 的值就是 1。

else

若條件不成立,就會運行從 else 到 end 的程式碼。

n * fact(n-1)

若條件不成立,if 的值就是 n 乘以 fact(n-1)

end

第一個 end 結束 if 敘述。

end

第二個 end 結束 def 敘述。

puts fact(ARGV[0].to_i)

這樣便會呼叫 fact() 函數,並傳入從命令列傳進來的參數,並輸出結果。

ARGV 是包含命令列引數的陣列。由於 ARGV 的成員都是字串,因此要用 to_i 轉換為整數 (integral number)。Ruby 不會像 Perl 那樣,自動將字串轉換為整數。

如果這個程式中輸入負數,會怎麼樣呢?你發現問題了嗎?能夠修正嗎?

字串

接下來我們會分析字串一節的猜字程式。這個程式有點長,所以我們會編號方便指示。

01 words = ['foobar', 'baz', 'quux']
02 secret = words[rand(3)]
03
04 print "guess?"
05 while guess = STDIN.gets
06   guess.chop!
07   if guess == secret
08     puts "You win!"
09     break
10   else
11     puts "Sorry, you lose."
12   end
13   print "guess?"
14 end
15 puts "the word is "+ secret + " . "

程式中,使用了 while 這個新的控制結構。若指定的條件為真時,就會重複運行 while 與其對應的 end 之間的程式碼。這個範例中,guess=STDIN.gets 既是一個有動作的敘述(收集使用者輸入的一行內容,並儲存為 guess),也是一項判斷條件(若沒有輸入任何內容,guess 即等於整個 guess=STDIN.gets 表示式的值,而這個值是 nil,會讓 while 停止迴圈。)

STDIN 是標準輸入 (standard input) 物件。一般來說,guess=gets 的功能與 guess=STDIN.gets 一樣。

第 2 行的 rand(3) 會傳回 0 至 2 間的一個亂數 ( random number), 用來提取陣列 words 中的一個元素。

第 5 行中,我們利用方法 STDIN.gets,從標準輸入中提取一行內容。提取時若出現 EOF(檔案結尾),gets 會傳回 nil。因此 while 程式碼會不斷重複,直到遇到代表結束輸入的 ^D(DOS/Windows 中則是 ^ZF6)。

第 6 行中 guess.chop! 會刪除 guess 後的最後一個字元;在本例中這是一個換行 (newline) 字元,這是因為 gets 會包含使用者按下的 Return 鍵,這是我們不感興趣的。

在第 15 行中輸出謎底。我們將謎底寫為三個字串將之相加在一起;這與將 secret 寫為 #{secret} 的單一字串效果相同,清楚顯示這是將要計算的變數,而不只是逐字輸出而已:

puts "the word is #{secret}."

許多程式員都覺得這樣能夠比較簡潔地顯示輸出的內容。(編註:執行的效率也比較好,不需要呼叫相加兩次)

我們現在已經熟悉如何使用 puts 作為標準輸出,但這個腳本的第 4 及 13 行卻是用 print。它們是不太一樣的。print 會準確輸出所提供的內容;puts 則一定會以輸出行 (output line) 結尾。在第 4 及 13 行使用 print,會讓游標停留在顯示內容的後面,而不是下一行開頭。這可用作提示使用者輸入。一般來說,以下四個輸出呼叫 (output call) 的結果都是相同。

# 若內容沒有換行,則 puts 會隱含加上換行:
puts  "Darwin's wife, Esmerelda, died in a fit of penguins."

# 換行必須明確加入:
print "Darwin's wife, Esmerelda, died in a fit of penguins.\n"

# 可用 + 連接輸出內容:
print 'Darwin's wife, Esmerelda, died in a fit of penguins.'+"\n"

# 或提供多個字串以連接:
print 'Darwin's wife, Esmerelda, died in a fit of penguins.', "\n"

你可能需要留意:文字視窗有時會為了速度而緩衝輸出內容,只有遇到換行字元時,才會收集個別字元並加以顯示。所以如果使用者猜字後,猜字程式無法如預期般顯示提示行,大概就是因為緩衝引起的。輸出提示後,你可以 flush 輸出內容,避免上述情況發生。它可通知標準輸出裝置(該物件稱為 STDOUT):「別等了,馬上顯示緩衝區的內容。」

04 print "guess?"; STDOUT.flush
  ...
13 print "guess?"; STDOUT.flush

事實上,我們在下一個腳本會更小心使用這個指令。

正規表示式

最後,我們會討論正規表示式一節的程式。

01 st = "\033[7m"
02 en = "\033[m"
03
04 puts "Enter an empty string at any time to exit."
05
06 while true
07   print "str> "; STDOUT.flush; str=gets.chop
08   break if str.empty?
09   print "pat> "; STDOUT.flush; pat=gets.chop
10   break if pat.empty?
11   re = Regexp.new(pat)
12   puts str.gsub(re, "#{st}\\&#{en}")
13 end

第 6 行中,while 的條件硬性指定為 true,所以形成無窮迴圈。但是,我們在第 8 及 10 行輸入 break 敘述來逃出迴圈。這兩個 break 都用了「if 修飾詞 (modifier)」,if 修飾詞僅在指定條件符合時,才會執行其左邊的敘述。這種由右到左的演算邏輯並不常見,但卻貼近日常用語的表達形式。由於不需要用 end 敘述來通知直譯器之後的程式碼有多少需要符合條件,所以也顯得簡潔。一般來說,只有在敘述與條件能夠簡短地寫在一行的情況下,才會使用 if 修飾詞。

請注意這個程式與猜字程式在使用者介面上的差別。使用者可在這個程式的空白行上按下 Return 鍵離開程式。因此我們測試輸入字串是否為空,而非是否不存在。

第 7 及 9 行中有一個「非破壞性的」(non-destructive) 的 chop(去除尾部空白);同樣,我們用 gets 去掉不要的換行字元。加入驚嘆號就是「破壞性的」(destructive) chop, 那兩者有甚麼差別呢?在 Ruby 中,我們一般在方法名稱後加上 '!' 或 '?'。驚嘆號(! 有時讀作 "bang!")代表具有破壞性,即會改變所接觸物件的值。chop! 會直接影響字串,但 chop 則會提供一個刪減後的版本,而不影響原本的物件。以下將闡釋兩者的差異。

ruby> s1 = "forth"
  "forth"
ruby> s1.chop!       # 這改變了 s1。
  "fort"
ruby> s2 = s1.chop   # 這將變更後的版本置於 s2,
  "for"
ruby> s1             # ⋯⋯而不影響 s1。
  "fort"

你也會看見有 chompchomp!。提供更多選擇性:字串結尾是一個換行字元時,結尾才會刪掉。例如:"XYZ".chomp!,並不會有任何效果。記住兩者差別的技巧就是,想像人或動物吃東西前,總會先嘗嘗味道,才會一口咬下去,而不像斧頭那樣隨便就砍下去。

第 8 及 10 行也出現另一種命名方式。問號(? 有時讀作 "huh?")的「述語」(predicate) 方法,表示只會傳回 truefalse。(編註:! 和 ? 結尾的命名方式,只是一個慣例。前者暗示有某種副作用,後者暗示回傳值是 Boolean 布林值)

第 11 行利用使用者提供的字串,建立一個正規表示式物件。第 12 行則完成真正的工作,使用 gsub 將 ANSI 標記替換 (globally substitute) 到表示式中每個匹配的內容;第 12 行也會輸出結果。

我們可以將第 12 行拆成:

highlighted = str.gsub(re,"#{st}\\&#{en}")
puts highlighted

或是「破壞性」的形式:

str.gsub!(re,"#{st}\\&#{en}")
puts str

再看看第 12 行的最後部分, sten 在第 1 至 2 行中定義為 ANSI 碼改變文字顏色。並在第 12 行以 #{} 包含其內,確保解釋正確(如此我們並不會看到輸出變數的名稱 (names)), 兩者之間看到的是 \\&。這有點困難, 因為這個替代字串以雙引號括起,所以其中的一對斜線會解釋成為單一斜線,造成 gsub 實際看到的是 \&,而這剛好是指示一開始符合樣式的特別程式碼。所以新字串顯示時,除了符合指定樣式部分會反白顯示以外,其餘都跟舊字串一樣。

Copyright (c) 2005-2008 Mark Slagell,中文翻譯 Ruby Taiwan 社群

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.

A copy of the license is included in the section entitled "GNU Free Documentation License."