從底層去認識 ruby 的load,require,gems,bundler,以及rails中的autoloading

zyhmz發表於2018-06-24

在rails中,我一直對require和autoloading感到很疑惑,嚴重阻塞了我學習的進度,所以我覺得搞清楚這些概念是很有必要的, 在這裡翻譯一篇國外的博文,並寫下自己的一些理解。

load 和 require 到底做了些什麼?

第一次看到require語句的時候,立刻有一種似曾相識的感覺。對於熟悉的c語言,我看到了include的影子。從開發者的角度來說,這兩條指令完成了相似的工作:告訴編譯器我們想要引用其它地方的程式碼。但是,仔細思考和實驗過後,卻不難發覺,這兩條指令在實現層面上乃是千差萬別。

當我們include一個標頭檔案的時候發生了什麼?答案是編譯器找到對應的標頭檔案,並將其原地展開。通過對標頭檔案中程式碼的解析,編譯器知道了我們要引用的程式碼的宣告(Declaration)。所謂宣告,即是指編譯器知道將要引用的程式碼到底長成什麼樣,而並不知道程式碼具體是什麼(Definition)。而為了讓我們的程式碼真正地用上所引用的程式碼,我們需要在連結的時候,向linker指明引用庫的地址。而這個所謂的引用庫,才包含了我們欲引用程式碼的具體定義。

反觀require呢?當我們require某個外部程式碼的時候,我們實際上是告訴編譯器尋找對應的rb檔案。這個rb檔案中有什麼?答案是,引用程式碼的具體定義。這時候require和include有何區別的答案便呼之欲出了:由於Ruby沒有傳統意義上的連結過程,我們require實際上是載入程式碼的定義和定義;而對於C編譯器來說,include只不過是告訴編譯器程式碼長啥樣,這使得編譯器能夠生成"呼叫程式碼",而"實現程式碼"則是在連結階段予以提供的。

load
首先來看看rubu中load的用法:

puts("foo.rb loaded!")
$FOO = 2

我們開啟irb:

> load('/Users/zhang/foo.rb')
foo.rb loaded!
 => true
> $FOO
 => 2

load這個方法是是定義在Kernel模組裡面。當我們給load方法傳遞一個絕對路徑,這個方法會執行絕對路徑所對應的程式碼。load方法總是會返回true,除非這個路徑的檔案無法載入才會返回LoadError。除了區域性變數,全域性變數,常量,類都會載入進來:

# foo.rb
$FOO_GLOBAL_VARIABLE = 2
class FooClass; end
FOO_CONSTANT = 3
def foo_method; end
foo_local_variable = 4

在irb中:

> load('/Users/zhang/foo.rb')
 => true
> $FOO_GLOBAL_VARIABLE
 => 2
> FooClass
 => FooClass
> FOO_CONSTANT
 => 3
> foo_method
 => nil
> foo_local_variable
NameError: undefined local variable or method `foo_local_variable' 

如果我們載入對於同一個路徑載入兩次,那麼在這個檔案裡面就會載入這段程式碼兩次。現在在foo.rb檔案中,我們定義了一個常量。當我們載入這個rb檔案的時候,因為定義了兩次,就會產生warning。

在irb中執行:

> load('/Users/zhang/foo.rb')
foo.rb loaded!
 => true
> load('/Users/zhang/foo.rb')
foo.rb loaded!
/Users/cstack/foo.rb:2: warning: already initialized constant FOO_CONSTANT
 => true

我們還可以使用相對路徑來使用這個load方法,只要這個檔案在同一個目錄下面:

> load('./foo.rb')
foo.rb loaded!
 => true

$LOAD_PATH
這是一個收集了全部絕對路徑的全域性的陣列,如果我們僅僅load了一個檔名,他就會遍歷整個陣列以及在每一個資料夾裡面去查詢檔案:

> $LOAD_PATH.push("/Users/zhang")
> load('foo.rb')
foo.rb loaded!
 => true

load 也可以在當前資料夾查詢:

> Dir.chdir("/Users/zhang")
 => 0
> load('foo.rb')
foo.rb loaded!
 => true

require
require和load是很像的,但是他們還是有一些不一樣的地方。在一個檔案中呼叫require兩次,所對應的程式碼只會執行一次。require返回true,如果這個路徑所對應的檔案還沒有被載入。

> $LOAD_PATH.push('/Users/zhang')
 => ["/Users/zhang"]
> require('foo.rb')
foo.rb loaded!
 => true
> require('foo.rb')
 => false

有時候我們search另一個檔案的時候,我們不一定需要包括這個這個檔案的字尾,比如說:

> $LOAD_PATH.push('/Users/zhang')
 => ["/Users/zhang"]
> require('foo')
foo.rb loaded!
 => true

在這段程式碼裡面,require不僅僅會找尋foo.rb的資源,還會search一些動態連結庫的資源,比如說foo.so、foo.o、foo.dll,這就是為什麼我們可以在rails中呼叫c程式碼的原因。

gems
一個gem實際上是一個有RubyGems代為管理的ruby包。比如說,json就是一個gem包,包含了一些解析和產生json的程式碼。讓我來看看這個gem包究竟儲存在我們電腦中的什麼地方:

~ gem which json
/Users/paul.zhang/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/json.rb

那我們究竟應該怎麼載入這個gem包呢?如果我們知道了這個gem包的絕對路徑,我們可以使用load和require來載入:

> load('/Users/zhang/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/json.rb')
 => true
> JSON
 => JSON

還有一個點,在我們的Gemfile裡面,會看到一些:require => false的選項。一般來說,rails框架還是保證這個gem被安裝了,但是這個gem不會被require。我們需要用到時才require這個gem。

bundle exec 的作用
我們可以知道,當我們使用bundle install的時候,會生成一個Gemfile.lock的檔案。在我們執行一些檔案之前,時常會執行bundle exec這個指令,以保證我們require gem絕對路徑 的時候會載入我們寫在Gemfile.lock的gem。

相關文章