當我們開始寫這本書時,我們有一個宏大的計劃(那時我們還比較年輕)。我們希望將 Ruby 這門語言從頭至尾地描述一遍,從類和物件開始,直到語法中的種種細節爲止。那時這的確是個好主意。因爲在 Ruby 看來萬物皆物件,因此我們打算從物件開始談起。
或者,這只是我們的一廂情願。
不幸的是,以這種方式描述一門語言是困難的。如果你不曾瞭解字串,if 宣告,賦值以及其他細節,要寫出一個類的例子是難以成立的。儘管我們要從頭至尾地描述這門語言,但我們也需要從低級別的細節開始瞭解,這樣才能 纔能知曉樣例程式碼的含義。
因此,我們制定了新的宏大計劃(這樣我們就不會再被說一點也不務實了)。儘管我們想要從頭開始講述 Ruby,但在此之前,我們會新增一個簡短的章節,通過 Ruby 的專有關鍵字描述一些語言特性,這些語言特性會在後面樣例中使用到,算是一種爲書籍正式內容製作的一個迷你指引。
讓我們開始討論 Ruby 吧。Ruby 是一門真正的物件導向語言。你操作的每個事物都是物件,這些事物操作的結果也是物件。然而,許多語言都自稱是物件導向的,並且對於物件導向也都有着不同的解釋,以及對自己所採用思想的不同術語。
因此,在我們更多地瞭解細節之前,讓我們簡單地認識一下即將用到的對術語和符號。
當你寫物件導向程式碼時,通常你會從真實世界提取模型到並表現在你的程式碼中。一般情況下,在你抽象模型的過程,你會發現你需要表現到程式碼中事物的型別。例如一個自動點唱機,「歌曲」這個概念一定會是一種事物型別。在 Ruby 中,你需要定義一個 class(類) 來表現這個實體。一個類是狀態(例如這個歌曲的名字)和使用狀態的方法(或許是一個播放歌曲的方法)的結合體。
一旦你擁有了一些類,通常就會想爲每個類建立許多範例。在點唱機系統中就包含一個叫做「Song」的類,你需要爲其的一般使用區分出不同的範例,比如 Ruby Tuesday, Enveloped in Python, String of Pearls, Small talk 等等。object(物件) 這個詞和類範例是相同的含義(作爲一個懶惰的打字員,我使用物件這個詞會更加頻繁)。
在 Ruby 中,這些物件被構造器建立,它是類中的一個特殊方法。一個標準的構造被叫做 new(建立)。
song1 = Song.new("Ruby Tuesday")
song2 = Song.new("Enveloped in Python")
# and so on
這些範例都是從相同的類派生出來的,但它們都有自己唯一的特質。首先,每個物件都有唯一的 object identifier(物件標識) (簡稱 object id)。其次,你可以定義範例變數,每個範例的變數值都是不同的。這些範例變數儲存着物件的狀態。例如,每個 Song 中,都會有範例變數儲存歌曲標題的值。
在每個類中,我們可以定義實體方法。每個方法就是一塊函數定義,這些函數可以在類中呼叫,也可以在外部呼叫(在外部呼叫時依賴存取限制)。這些方法可以存取物件範例的變數,從而存取到物件的狀態。
方法通過發送資訊給物件的方式呼叫。這些資訊包括方法名字,也包含方法需要的參數。當物件獲取到資訊後,它會查詢類中相應的方法。如果查詢到就會執行此方法,如果沒有找到,這種情況我們後面再討論。
方法的業務和資訊聽起來可能比較複雜,但是在練習中方法的表現會非常自然。讓我們看看一些方法的呼叫(請記住,下面 下麪程式碼中的箭頭符號表示對應表達式返回的結果值)。
"gin joint".length » 9
"Rick".index("c") » 2
-1942.abs » 1942
sam.play(aSong) » "duh dum, da dum de dum ..."
上面程式碼中,在句點符號之前的部分被 receiver(接收器),在句點後的是方法的呼叫。第一個例子詢問了一個字串的長度,第二個例子在詢問另一個字串中字元 c
的下標。第三行程式碼是計算一個數字的絕對值。最後,我們讓 Sam 給我們播放了一首歌。
這裏值得注意的是,在許多其他語言和 Ruby 之間有一個主要的差異。就以 Java 而言,你會發現對過呼叫一個分離的方法和傳遞數位的方式計算此數位的絕對值。你可能會這樣寫
number = Math.abs(number) // Java code
在 Ruby 中,計算一個絕對值的能力建立在數位的基礎上——它們在方法內部關心細節。你發送一個 abs
的資訊給數位物件,並且讓它做相應的工作。
number = number.abs
Ruby 的物件都是類似的應用: 在 C 語言中你需要寫 strlen(name)
,而在 Ruby 中可以寫爲 name.length
等等。這就是我們說 Ruby 是一個真正的物件導向語言的原因。
當開始學習一門新的語言時,不是所有人都喜歡閱讀一堆無聊的語法規則。因此我們將走一下捷徑。在這個部分我們會新增一些高亮,用以提醒這些知識是寫 Ruby 程式碼必須知道的。稍後,在 18 章我們會着重介紹這些細節。
讓我們開始一段基本的 Ruby 程式。我們將會寫一個方法用以返回一個字串,這個字串會被新增上一個人的名字。稍後我們會呼叫這個方法好幾次。
def sayGoodnight(name)
result = "Goodnight, " + name
return result
end
# Time for bed...
puts sayGoodnight("John-Boy")
puts sayGoodnight("Mary-Ellen")
首先,進行一些基本的觀察。Ruby 方法是比較清晰的。你不需要在語句末尾使用分號,只需要換行就可以。Ruby 的註釋是以一個 #
號開頭,直到此行的結尾都是處於註釋的狀態。程式碼的佈局取決於你,縮排也是不重要的。
方法是由關鍵字 def 定義,在關鍵字之後跟方法名(在這個例子中是 「sayGoodnight」),而方法參數是在小括號之間。Ruby 不會使用花括號劃定複合語句和定義體的邊界。取而代之的是,你可以使用關鍵字 end 完成一段內容的結束。我們的方法體很簡單。第一行我們將「Goodnight, ]與參數 name
進行了拼接,並將結果賦值給臨時變數 result
。下一行將結果返回給了呼叫者。需要注意的是,我們並還是必須宣告 result
變數,在我們賦值的時候它已經存在準備被返回給方法呼叫的。
定義完方法後我們呼叫了此方法兩次。在所有的例子中,我們把呼叫方法返回的結果傳值給了 puts
方法,puts
方法會將其入參輸出並在末尾新增換行。
Goodnight, John-Boy
Goodnight, Mary-Ellen
puts sayGoodnight("John-Boy")
這行包含了兩個方法的呼叫,一個是 sayGoodnight
,另一個是 puts
。爲什麼一個方法的呼叫入參需要放在小括號內,而另一個方法的呼叫入參不需要如此?在這個案例中,完全是個人的習慣問題。下面 下麪幾行程式碼是完全等價的。
puts sayGoodnight "John-Boy"
puts sayGoodnight("John-Boy")
puts(sayGoodnight "John-Boy")
puts(sayGoodnight("John-Boy"))
然而生活並不是如此簡單,很多時候優先順序問題會使清晰知道哪個參數是傳入哪個方法呼叫變得困難,因此我們建議,除了最簡單的情況外都使用小括號。
這些例子也展示了一些 Ruby 的字串物件。其實有許多方式可以建立字串物件,但是使用字串建立字串物件是較常用的方式,也就是由單引號或者雙引號包含的字元序列。這兩種方式的區別在於 Ruby 對於字串的處理爲字串物件的工作量上。如果使用單引號,Ruby 的工作量非常少。除了少數情況,你輸入的字串都會轉換成字串物件值。
如果使用雙引號,Ruby 需要做更多的工作。首先,需要查詢替換點,如字串中以反斜槓開關的字元,會被一些二進制值替換。最常見的有 \n
,會被替換爲換行字元。當一個包含換行符的字串被輸出時,\n
會強制換行。
puts "And Goodnight,\nGrandma"
結果是:
And Goodnight,
Grandma
Ruby 中使用雙引號建立字串物件第二個功能是可以插入表達式。在字串中,字串 #{ expression }
會被表達式的值替換。我們可以使用這個功能重寫之前的方法。
def sayGoodnight(name)
result = "Goodnight, #{name}"
return result
end
當 Ruby 構造字串物件時,它找到當前 name
的值在字串中替代他。任何複雜的表達式都允許通過 #{...}
的方式構造。其實還可以使用便捷的寫法,當表達式是全域性變數,範例或者類變數時可以不使用花括號。字串作爲 Ruby 中的基本型別,關於它的更多知識我們會在第 5 章講述。
最後,我們還可以將剛纔的方法更加簡化。一個 Ruby 方法返回值是最後一個表達式的結果,因此我們可以去除 return
表達式。
def sayGoodnight(name)
"Goodnight, #{name}"
end
我們保證這個部分會十分簡短。我們還有個主題需要講:Ruby 命名。爲了簡潔,我們將會使用一些使用還沒有講過的知識點(例如 class varable(類變數))。然而在我們開始真正介紹 instance variables(範例變數) 及稍後討論的知識點之前,我們需要開始談論這個規則。
Ruby 用約定對命名的使用進行區分:名字的首字母表示這個命名是應該怎樣使用的。臨時變數,方法參數和方法名稱應該以小寫字母或者下劃線開頭。全域性變數需要以 $
作爲字首,而使用範例變數時需要以 @
符號開頭。類變數需要以 @@
作爲開頭。最後,類名,模組名和常數應該以一個大寫字母開頭。命名的不同樣例在下表中給出。
下面 下麪的這些初始化字元可以與字元,數位和下劃線拼接(另外,@
符號後的字元不能是數位)。
Variables Local | Global | Instance | Class | Constants and Class Names |
---|---|---|---|---|
name | $debug | @name | @@total | PI |
fishAndChips | $CUSTOMER | @point_1 | @@symtab | FeetPerMile |
x_axis | $_ | @X | @@N | String |
thx1138 | $plan9 | @_ | @@x_pos | MyClass |
_26 | $Global | @plan9 | @@SINGLE | Jazz_Song |
Ruby 的陣列和雜湊表是基於下標的收集器。都可以儲存物件,並且通過鍵存取儲存的物件。對於陣列來說,鍵是一個整型,但是雜湊表支援任何物件作爲鍵。陣列和雜湊表都可以根據元素的增加而變長。相比而言,陣列的元素存取效率會更高,而雜湊表的元素存取更具有靈活性。任何陣列和雜湊表都可以包含不同類型的物件,你可以在一個數組中同一時間包含整型,字串,以及一個浮點數。
你可以用陣列字元定義和初始化一個數組,只需要將陣列元素置於方括號之間。當有一個數組時,你可以通過將下標放置在方括號之間存取指定元素,就如同下面 下麪的例子一樣。
a = [ 1, 'cat', 3.14 ] # array with three elements
# access the first element
a[0] » 1
# set the third element
a[2] = nil
# dump out the array
a » [1, "cat", nil]
你可以通過不在方括號之間放置任何元素的方式建立空陣列,或者通過陣列物件的構造器 Array.new
也可以達到一樣的效果。
empty1 = []
empty2 = Array.new
有時,建立一個單詞陣列是比較麻煩的,你需要在元素間新增引號和逗號。幸運地是,%w
這個簡寫可以幫助我們方便地達到上述效果。
a = %w{ ant bee cat dog elk }
a[0] » "ant"
a[3] » "dog"
Ruby 的雜湊表和陣列相似。不過雜湊表是使用花括號而不是使用方括號。其中的每個條目必須使用兩個物件,一個是鍵,另一個是值。
例如,你可能想將樂器分配至管絃樂器模組。你可以用雜湊表完成此事。
instSection = {
'cello' => 'string',
'clarinet' => 'woodwind',
'drum' => 'percussion',
'oboe' => 'woodwind',
'trumpet' => 'brass',
'violin' => 'string'
}
雜湊表與陣列一樣使用方括號取值。
instSection['oboe'] » "woodwind"
instSection['cello'] » "string"
instSection['bassoon'] » nil
最後一個範例表明瞭,在雜湊表中當鍵的取值不存在時,預設返回 nil
。一般情況下沒有不便,畢竟當作爲條件表達式使用時,nil
和 false
是一個意思。但有時你會想修改預設值。例如,如果你用雜湊表統計每個鍵出現的次數時,爲了方便,預設值應該修改爲零。這時當你建立一個新的空雜湊表時,可以通過指定預設值的方式輕易做到上述需求。
histogram = Hash.new(0)
histogram['key1'] » 0
histogram['key1'] = histogram['key1'] + 1
histogram['key1'] » 1
陣列和雜湊表物件中有許多有用的方法,這些會從 33 頁開始的部分進行討論,關於其中的更多詳細內容位於 278 至 317 頁的部分。
Ruby 有所有常用的邏輯控制結構,比如 if
判斷和 while
回圈。在 Java,C 和 Perl 中,程式設計師可能會在這些邏輯結構體周圍不使用花括號。但是,Ruby 使用 end
關鍵字結束邏輯結構體。
if count > 10
puts "Try again"
elsif tries == 3
puts "You lose"
else
puts "Enter a number"
end
類似地,while
結構也是以 end
結束。
while weight < 100 and numPallets <= 30
pallet = nextPallet()
weight += pallet.weight
numPallets += 1
end
在你使用 if
或者 while
結構並且只有單一表達時,Ruby 結構修改器是一個有用的快捷方式。可以簡潔地書寫表達式,然後再跟 if
或者 while
條件。例如,下面 下麪是一個簡單的 if
結構。
if radiation > 3000
puts "Danger, Will Robinson"
end
這裏可以用結構修改器重新書寫如下。
puts "Danger, Will Robinson" if radiation > 3000
類似地,while
回圈也可以這樣修改
while square < 1000
square = square*square
end
可以簡寫如下
square = square*square while square < 1000
這些結構修改器在 Perl 程式設計師看來應該比較熟悉。
許多 Ruby 的內建型別對於所有不同語言的程式設計師來說都是相似的。主流的語言中都會包含字串,整型,浮點型,陣列等等。然而,在 Ruby 出現之前,正則表達式作爲內建型別只會出現在所謂的指令碼語言中,例如 Perl,Python 和 awk。這是一種羞恥,儘管正則表達式比較神祕,但不失爲一種強有力的文字處理工具。
已經有書籍完整地講述了關於正則表達式的種種(比如 Mastering Regular Expressions),因此我們不打算在這個簡短的章節裏面將這些知識複述一遍。取而代之的是,我們會實際展示幾個關於正則表達式的小例子。你也可以從 56 頁開始完全瞭解正則表達式。
正則表達式通過簡單的方式描述字元模式,以完成在字串中匹配目標的工作。在 Ruby 中,你通常可以通過在斜槓之間編寫模式的方式建立一個正則表達式(/pattern/)。而且,對於 Ruby 來說,正則表達式也是物件,本身也可以進行操作。
例如,你可以像下面 下麪的例子一樣編寫一個模式,用來匹配字串中的「Perl」或者「Python」文字。
/Perl|Python/
斜槓標識出了模式,模式中包含了我們需要匹配的兩個文字,使用管道符號「|」分隔。你也可以在表達式中使用小括號,正如在算術表達式中那樣使用,於是上面的表達式也可以寫成如下這樣。
/P(erl|ython)/
你也可以在模式中指定重複的方式。/ab+c/
匹配一個包含 a
並跟隨一個或多個 b
再緊接着一個 c
的字串模式。如果把加號替換成星號,/ab*c/
就匹配一個包含 a
並跟隨零個或多個 b
再緊接着一個 c
的字串模式。
你也可以通過一個模式匹配一組字元。一些公共的例子是字元類,比如 \s
,會匹配一個空白字元(空格,tab,換行等等),\d
可以匹配任何數位,以及 \w
可以匹配一般單詞中的任意字元。一個句點字元 .
可以匹配任意字元。
我們可以把這些知識組合成一些有用的正則表達式。
/\d\d:\d\d:\d\d/ # 匹配時間,例如 12:34:56
/Perl.*Python/ # Perl,跟零個或多個其他字元,再是 Python
/Perl\s+Python/ # Perl,跟一個或多個空白字元,再是 Python
/Ruby (Perl|Python)/ # Ruby,然後一個空格,再是 Perl 或者 Python
一旦你已經建立一個模式,如果不使用它顯示很不夠意思。匹配操作符 =~
可以通過正則表達式匹配一個字串。如果這個模式在字串中匹配上,=~
會返回其起始位置,否則會返回 nil
。這意味着你可以使用正則表達式作爲 if
和 while
語句的條件。如下,如果字串包含 ‘Perl’ 或者 ‘Python’ 就會將這段字串輸出。
if line =~ /Perl|Python/
puts "Scripting language mentioned: #{line}"
end
如果想通過正則表達式替換字串中的目標字元,可以使用 Ruby 的替換方法。
line.sub(/Perl/, 'Ruby') # 替換第一個 'Perl' 爲 'Ruby'
line.gsub(/Python/, 'Ruby') # 替換所有的 'Python' 爲 'Ruby'
整本書我們將會在許多地方談論到正則表達式。
這一章節會簡短介紹 Ruby 中的一個特點,也是它的優點。我們先看看程式碼塊,你能夠結合方法的呼叫使用程式碼塊,此時程式碼塊就像參數一樣。這是一個非常有用的特性。你可以使用程式碼塊實現回撥函數(但這種實現會比 Java 中的匿名內部類更加簡單),實現回撥函數的方式可以是傳遞程式碼塊(同樣它也比 C 語言的指針函數更加靈活),並且也可以實現迭代器。
程式碼塊就是在花括號或者 do...end
間的一塊程式碼。
{ puts "Hello" } # 這是一個程式碼塊
do #
club.enroll(person) # 這也是一個程式碼塊
person.socialize #
end #
一旦你已經建立了一個程式碼塊,你就可以通過呼叫方法的方式存取程式碼塊。然後你可以使用 Ruby 的 yield
語句呼叫方法塊一次或多次。下面 下麪的例子展示了這種方式。我們定義一個方法呼叫兩次 yield
。然後我們呼叫它,把一個程式碼塊放在方法呼叫之後,並且同一行的位置(並且是在呼叫方法的所有參數之後)。(一些人喜歡認爲通過一個方法的呼叫使用程式碼塊,就像把程式碼塊作爲一個參數傳遞。這只是其中的一方面,但並不完整。更好的認知應該是在程式碼塊與方法之間有一個協同程式,它可以在程式碼塊和方法之間來回穿梭)。
def callBlock
yield
yield
end
callBlock { puts "In the block" }
結果是:
In the block
In the block
看看程式碼塊是怎樣被執行了兩次的,其實每一次都是呼叫的 yield
。
你可能會提供參數來呼叫 yield
,希望這些參數能夠通過程式碼塊使用。在程式碼塊中,你通過在兩個豎線「|」之間列舉參數名字來獲取這些參數。
def callBlock
yield ,
end
callBlock { |, | ... }
程式碼塊也在 Ruby 庫中用來實現迭代器,比如一些從容器(例如陣列)中返回連續元素的方法。
a = %w( ant bee cat dog elk ) # 建立陣列
a.each { |animal| puts animal } # 迭代所有內容
結果是:
ant
bee
cat
dog
elk
讓我們看看如何實現一個像前一個例子中 Array
類的 each
方法。each
方法通過在回圈到每個元素時都使用 yield
呼叫。在虛擬碼中看起來可能像下面 下麪這樣:
# within class Array
def each
for each element
yield(element)
end
end
你可以呼叫 each
方法遍歷陣列中的元素同時也將程式碼塊應用上。程式碼塊在每個元素被遍歷到時都會呼叫。
[ 'cat', 'dog', 'horse' ].each do |animal|
print animal, " -- "
end
結果是:
cat -- dog -- horse --
類似的,許多像 C 和 Java 中的回圈結構在 Ruby 中被簡化爲了方法呼叫,然後通過程式碼塊零次或多次的呼叫實現。
5.times { print "*" }
3.upto(6) {|i| print i }
('a'..'e').each {|char| print char }
結果是:
*****3456abcde
我們使用了數位 5 呼叫了程式碼塊 5 次,然後通過數位 3 呼叫程式碼塊,傳入程式碼塊中連續值,一直到 6 爲止。最後,通過 each
方法將 ‘a’ 至 ‘e’ 之間的字元呼叫了程式碼塊。
Ruby 有一個全面的 I/O 庫。在本書的許多例子中都會和 I/O 的簡單方法有所關聯。我們在輸出時已經使用過兩個方法。puts
輸出它的每個參數,並且在結尾新增一個換行。print
也會輸出它的參數,不過沒有換行。這些都可以用來輸出任何 I/O 物件,但在預設情況下是輸出到控制檯。
其它我們使用比較多的輸出方法是 printf
,它會根據格式定義輸出它的參數(就像 C 或 Perl 中的 printf
方法一樣)。
printf "Number: %5.2f, String: %s", 1.23, "hello"
結果是:
Number: 1.23, String: hello
在這個例子中,格式化字串 「Number: %5.2f, String: %s」 使用一個浮點數(允許總共 5 個字元,其中小數位有 2 個字元)和字串替換。
當然也有很多方式可以讀取程式中的輸入。一般大多數傳統方式是使用 gets
例程,它會從你的程式標準輸入流中返回下一行。
line = gets
print line
不過 gets
例程有一個副作用。儘管可以返回讀取到的行,但它也會把讀取的行儲存到全域性變數 $_
中。這是一個特殊變數,在許多情況下會被作爲預設參數使用。如果你呼叫 print
方法而不傳入參數,它就會列印 $_
中的內容。如果使用一個正則表達式作爲 if
或者 while
語句的條件,正則表達式會對 $_
進行匹配。從一些純粹主義者的角度來說, 以上這些簡寫都可以幫助你簡化程式碼。例如,下面 下麪的程式會輸出所有包含 ‘Ruby’ 的輸入的行。
while gets # 讀取行並賦值給 $_
if /Ruby/ # 與 $_ 進行匹配
print # 輸出 $_
end
end
不過以 Ruby 的方式一般會使用遍歷器。
ARGF.each { |line| print line if line =~ /Ruby/ }
這裏使用了一個預定義物件 ARGF
,它代表程式能夠讀取的標準輸入。
這章就到這裏。我們完成了 Ruby 基礎部分的快速學習。我們簡單地介紹了物件,方法,字串,容器和正則表達式,也瞭解了一些基本的邏輯控制結構,以及一些優雅的迭代器。希望這章能夠給予你足夠的知識儲備以完成本書接下來的內容。
時間要繼續,我們也要繼續到下一個更高的層級進行學習。接下來,我們要學習一下類和物件,同時也會了解 Ruby 中最高階的結構以及這門完整語言的重要基礎。
本文翻譯自《Programming Ruby》,主要目的是自己學習使用,文中翻譯不到位之處煩請指正,如需轉載請註明出處
本章原文爲 Ruby.new