「Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。
也許你曾在 Ruby 或 Rails 專案中寫過這行語法:
1 2 | |
上面這段語法的大意是「引用 “digest” 模組,然後使用那個模組裡的某個方法產生 MD5 編碼字串」。但你知道這個 require 到底做了什麼事嗎? 以下將用我自己寫的一個名為 takami 的套件為例,它是一個完全沒功能的空包彈套件,純粹練功用途。
require 是怎麼運作的?
讓我們先打開終端機,直接試著印出 $PATH 這個變數來看看:
$ echo $PATH
/Users/user/.rvm/gems/ruby-2.3.1/bin:/Users/user/.rvm/gems/ruby-2.3.1@global/bin ...[略]...
在你電腦上的輸出結果可能跟我的不太一樣。當你想執行某個程式,例如 git log,系統會依照 $PATH 列出來的順序,一個一個的問「請問在這個路徑是不是有 git 這個程式?」,如果有,就執行它;如果都沒有,就會出現 command not found 的訊息。
在 Ruby 裡有個叫做 $LOAD_PATH 的全域變數(可簡寫成 $:)的功用跟 $PATH 類似,讓我們開 irb 來試試:
$ irb
>> $LOAD_PATH
=> ["/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib", ...[略]...
>> $:
=> ["/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib", ...[略]...
當你執行 require "takami" 之後,它會把這個 gem 加到 $LOAD_PATH 裡,並且把它加到另一個全域變數 $LOADED_FEATURES(可簡寫成 $") 裡:
$ irb
>> $LOAD_PATH.count
=> 9
>> $LOADED_FEATURES.count
=> 59
>> require "takami"
=> true
>> $LOAD_PATH.count
=> 10
>> $LOAD_PATH
=> ["/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib", "/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib", ...[略]...
>> $LOADED_FEATURES
=> ["enumerator.so", "thread.rb", ...[略] ... "/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib/takami.rb"]
>> $LOADED_FEATURES.count
=> 61
在 require 之前 $LOAD_PATH 只有 9 個,require 之後變 10 個了,而 $LOADED_FEATURES 的數量也從原本的 59 變成 61 了。除了可以在 $LOADED_FEATURES 看到新增加跟 takami 套件有關的東西之外,也可以看的出來 takami 套件的確在 $LOAD_PATH 裡了,這樣一來就可以直接使用我那個空包彈的 Takami 套件了。
原始碼挖挖挖!
首先,require 並不是關鍵字,它在 Ruby 裡只是個一般的方法(定義在 Kernel 模組裡),所以就讓我們試著挖 Ruby 的原始碼出來看看它到底做了什麼事。
但在開挖之前,要先講一下歷史故事,免得挖錯地方。在 Ruby 1.8 時代,Ruby 的 require 就是普通的 require,在 Ruby 1.9 之後,require 的事情則是交給 rubygems 套件來管理,所以我們現在應該要去挖 rubygems 套件的原始碼才能挖得到 require 真正的行為。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
上面這段我把部份的註解拿掉省一點空間。當 rubygems 被 require 進來之後,原本 Kernel 模組的 require 方法就被換成 rubygems 裡的 require 方法,並且把原本 Kernel 模組定義的 require 方法 alias 成 gem_original_require。
如果該套件本來就已經在 $LOAD_PATH 上的話,它會直接呼叫原本 Kernel 模組定義的 require 方法來載入套件;但如果在 $LOAD_PATH 上沒有找不到的話,它就會試著開始去找看看是不是在系統上有安裝這個套件,然後執行 activate 方法,將它加到 $LOAD_PATH 裡。
讓我們來玩一下分解動作:
$ irb
>> $LOAD_PATH
=> ["/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib", ...[略]...
# 原本只有 9 個
>> $LOAD_PATH.count
=> 9
# 找到我的套件
>> my_gem = Gem::Specification.find_by_name("takami")
=> #<Gem::Specification:0x3fdcf54346c8 takami-0.0.1>
# activate 方法會把這個 gem 加到 $LOAD_PATH 裡
>> my_gem.activate
=> true
# 套件現在加到 $LOAD_PATH 裡了
>> $LOAD_PATH
=> ["/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib", "/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib", ...[略]...
# 數量也變 10 個
>> $LOAD_PATH.count
=> 10
# 咦? 不是在 $LOAD_PATH 了嗎? 怎麼不行
>> Takimi
NameError: uninitialized constant Takimi
from (irb):8
from /Users/user/.rvm/rubies/ruby-2.3.1/bin/irb:11:in `<main>'
# 呼叫原本 Kernel 的 require 方法
>> gem_original_require 'takami'
=> true
# 可正常執行!
>> Takami
=> Takami
並不是有在 $LOAD_PATH 裡就可以直接使用,事實上它還是得靠原本 Kernel 模組的 require 方法把它加到 $LOADED_FEATURES 之後才能使用。所以說穿了,rubygems 其實只算是在幫你管理 $LOAD_PATH 的路徑而已(當然也沒這麼單純啦),真正 require 的行為還是在 Kernel 模組裡。
順帶一提,如果 require 是 Ruby 內建的標準函式庫(StdLib),例如本文一開始範例裡提到的 digest,$LOAD_PATH 本身並不會有任何變化(因為這些內建的本來就找得到了啊),但還是會把該模組加到 $LOADED_FEATURES 裡。
原本 Kernel 模組的 require
繼續挖 Ruby 原始碼來看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
這段是在 Kernel 模組裡 require 方法的實作,真正載入的行為是在 rb_require_internal 這個 function 裡,有興趣的朋友可以繼續往下追。Ruby 會檢查那個檔案是不是已經在 $LOADED_FEATURES 裡,如果不在就會把它加進來。
另外,rb_require_internal 這個 function 的回傳結果可能有以下 4 種:
1 2 3 4 5 6 7 | |
所以,如果 require 成功,rb_require_safe 這個 function 會回傳 true,但如果之前就已經 require 過的話則會回傳 false,如果什麼都找不到,則是直接產生 LoadError 的錯誤訊息:
$ irb
# 第一次 require,回傳 true
>> require "takami"
=> true
# 第二次 require 相同的套件,回傳 false
>> require "takami"
=> false
# require 一個找不到的 gem
>> require "the_gem_not_install"
LoadError: cannot load such file -- the_gem_not_install
小結
也許這個簡單的 require 指令一下子就跑完了,但當你知道它背後是怎麼運作之後,你對 Ruby 就可以有更深一層的了解。在挖原始碼的過程有點辛苦但也很有趣,除了可以看看大師們是怎麼設計的,還可以看到一些平常看不到的東西 :)