「Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。
在公開演講或是校園推廣 Rails 的時候,我常會開 rails console 出來秀一下 Rails 快速開發的威力:
1 2 3 4 5 6 7 8 | |
這真的很驚人,不只程式碼 2.days.ago 本身看起來好寫,而且就算連不懂程式語言的人看了也大概猜得出來是什麼意思,台下觀眾看到這裡,有的就會開始發出「喔喔喔」的讚嘆聲了。
但打開標準的 irb 來試試:
1 2 3 4 5 6 7 8 | |
一樣的語法在 Ruby 竟然不會動了!
原來,這些看起來很厲害的語法是 Rails 幫 Ruby 做的擴充功能,讓開發者在開發網站應用程式的時候可以很快的把功能完成。
怎麼做的?
先來看一段範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
這裡我不小心定義了兩次 Cat 類別,如果這是你剛接觸 Ruby 不久,你也許會認為後面定義的類別會蓋掉前面的類別,所以執行 eat 方法沒問題,但執行 sleep 就會壞掉了。
但實際執行的時候發現其實兩個方法都可以正常運作! 所以,上面這段範例到底發生了什麼事?
事實上,當 Ruby 遇到兩個類別重複定義的時候,後面的定義並不會「覆蓋」掉前面的,反而比較像是「合併」。這個設計在 Ruby 裡面稱之「Open Class」,可以「打開」已經存在的類別,並再加料進去,甚至連內建的類別(像是數字、字串、陣列之類的)也可以。
我們來試試看內建的字串類別:
1 2 3 4 5 | |
在這個定義之後,所有的字串就都會 say_hello 了
1 2 3 | |
這在其它程式語言是很少見的設計,特別是連內建的類別也可以這樣玩。讓我們再來玩一個!
1
| |
1 加 2,不就等於 3 啊,這小學生都會!
但你知道其實這個簡單的加法可能跟你想的不太一樣,Ruby 的四則運算,事實上並不是一般的運算元(operator)。以上面這個 1 + 2 的例子來說,在 Ruby 裡實際上是:
數字物件 1 呼叫了 `+` 這個方法,並且把數字物件 2 傳進去當做參數
所以 1 + 2 事實上是長這樣:
1
| |
執行之後會得到一樣的結果。既然知道加法其實也是一個 Ruby 的 method 的話,那就可以來試著這樣玩:
1 2 3 4 5 6 7 | |
這樣一來,不管是 1 + 2 或是 3 + 4,得到的結果都會是 1000。
不過這樣做有點風險,因為這樣等於是改寫了加法的行為,如果有其它方法有用到這個方法的話也會跟著得到不正確的結果。上面這個例子可以再改寫成:
1 2 3 4 5 6 7 8 9 10 | |
執行上面這段範例可以發現,1 + 2 還是等於 3,但除此之外還會默默的輸出 hey hey hey 字樣,表示除了原來的加法之外,還可以做一些事。至於可以做什麼,這就靠大家自己發揮想像力了。
所以,Rails 是怎麼做的?
Rails 使用了 Open Class 的手法來幫 Ruby 加了不少功能,就以 2.days.ago 來說:
1 2 3 4 5 6 7 8 9 10 | |
這裡打開了 Numeric 類別(它是 Fixnum 的上層類別),幫它加上了 days 方法,而且還很貼心的幫 days 做了單數的 alias day,萬一只有 1 天也可以寫出 1.day.ago 這樣更像英文的語法。
實作的程式碼請看這裡。
安全嗎?
這種「猴子補丁 (Monkey Patching)」的手法,有些人覺得很不嚴謹 (但我自己個人覺得還好),在 Ruby 2.0 之後推出了一個叫做 Refinement 的設計,可以稍微控制一下 Open Class 影響範圍。以前面那個 "kitty".hello 的範例,重新用 Refinement 改寫會長像這樣:
1 2 3 4 5 6 7 | |
上面這段範例建立了一個叫 MyHelloString 的模組,在裡面使用 refine 方法在 String 類別上面進行「提煉」。但這並不會像之前 Open Class 的一樣寫完馬上有效果,要使用這個提煉過的方法,則是使用 using 方法:
1 2 3 | |
小結
各位看官會覺得 Ruby 的 Open Class 很隨便嗎? 我自己是還滿愛這樣的設計的 :)
Ruby 是一款非常有彈性的程式語言,但彈性本身也是雙面刃,你得知道學會怎麼運用這個彈性,而且你最好知道自己在做什麼。