Ruby on rails的常量自動加載

Ruby中常量的運行機制:

1.嵌套

類和模塊可以嵌套,來組成命名空間
命名空間在ruby中就是一個沙盒,在其中定義的常量名和方法名可以不用擔心名稱與其他常量名和方法名沖突

module XML
    class SAXParser
        # (1)
        puts Module.nesting 
    end
end

可以在代碼的任意位置調用Module.nesting來審查當前的嵌套結構,比如(1)處的嵌套就是[XML::SAXParser, XML]

借著上述的類XML::SAXParser來解釋幾個關鍵概念:

  • 該類的名字叫做XML::SAXParser
  • 該類內部的嵌套為 [XML::SAXParser, XML]
  • 該類內部的命名空間是 XML::SAXParser
  • 該類的嵌套是由類XML::SAXParser和模塊XML組成,外層是一個模塊,第二層一個類
這里還涉及一個官方特意點出的概念:

組成嵌套的是類和模塊“對象”,而不是訪問它們的常量,與它們的名稱也沒有關系。

解析:
  1. 組成嵌套的是類和模塊“對象”,首先這里的類和模塊對象指的是它們都是ClassModule的對象,在ruby中我們常用的類它們本身也是個對象,只不過它們是Class的對象,而模塊是Module的對象,從上面的Module.nesting結果也可以看到,嵌套中有個2個元素,一個類XML::SAXParser和一個模塊XML
  2. “而不是訪問它們的常量”,在上面的例子中我們是通過XML::SAXParser常量即可訪問到我們定義的類,但事實上同樣的常量名,是可以有完全不同的嵌套結構的,可以見下面的示例
  3. “與它們的名稱也沒有關系”,的確,在上面定義的類中,該類的名稱是"XML::SAXParser",但是對于另一種定義該類的方式,名稱一樣的情況下,嵌套也可以截然不同,見下面的示例
class XML::SAXParser
# (2)
    puts Module.nesting
end
puts XML::SAXParser.name

上面這個例子中,訪問的常量也是XML::SAXParser,常量的名稱也是"XML::SAXParser",所以說光看到一個常量的名稱或者訪問它的方式,是無法確定它的嵌套的。

為什么要弄清嵌套的含義呢?

因為嵌套影響著其內部的常量的查找的方式,這樣當在一個嵌套內使用一個未定義的常量的時候我們能夠知道這個常量是如何查找的

嵌套是解釋器維護的一個內部堆棧,根據(jù)以下規(guī)則修改:

  • 執(zhí)行 class 關鍵字后面的定義體時,類對象入棧;執(zhí)行完畢后出棧。

  • 執(zhí)行 module 關鍵字后面的定義體時,模塊對象入棧;執(zhí)行完畢后出棧。

  • 執(zhí)行 class << object 打開的單例類時,類對象入棧;執(zhí)行完畢后出棧。

  • 調用 instance_eval 時如果傳入字符串參數(shù),接收者的單例類入棧求值的代碼所在的嵌套層次。調用 class_eval 或 module_eval 時如果傳入字符串參數(shù),接收者入棧求值的代碼所在的嵌套層次.

  • 頂層代碼中由 Kernel#load 解釋嵌套是空的,除非調用 load 時把第二個參數(shù)設為真值;如果是這樣,Ruby 會創(chuàng)建一個匿名模塊,將其入棧。

這個入棧規(guī)則我們仍舊以上面的XML::Parser來做解釋:

module XML
    class  SAXParser
    end
end
  1. 先碰到module關鍵字,將XML模塊對象入棧


    image.png
  2. 解釋到第二行,發(fā)現(xiàn)還沒執(zhí)行完,繼續(xù)執(zhí)行,發(fā)現(xiàn)class關鍵字,將類對象XML::Parser入棧


    image.png
定義類和模塊是為常量賦值

前面說到通過class或者module定義類或者模塊的時候其實都是在新建類或者模塊對象

module Admin
end

等效于

Admin = Module.new
類和模塊都有維護一個常量表

通常我們所說的String類,是不準確的,實際上它是Object常量(至于為什么是Object,這是由于ruby中的常量解析算法決定的)存儲的類對象中所維護一個常量表中的一個常量指向的一個類對象,該類對象名叫"String"


image.png

2.解析算法

相對常量解析算法

舉一個例子:

module A
    class B
        class C
            p Module.nesting # => [A::B::C, A::B, A]
            User
        end
    end
end

使用cref表示嵌套中第一個元素,比如前面的XML::SAXParser,如果沒有嵌套則表示Object
解析算法如下:

  1. 如果嵌套不為空,在嵌套中按元素順序查找常量。元素的祖先忽略不計。

按照嵌套所示順序,從A::B::C::User開始查找,一直到 A::User

  1. 如果未找到,算法向上,進入 cref 的祖先鏈。

如果查找到A::User都不存在的話,進入cref的祖先鏈


image.png

祖先鏈也就是cref的父類
這里還需要注意一點,所有的類在沒有顯示地繼承于哪個類的情況下,都是繼承自Object這個類,同時我們在頂層自定義的類或者模塊也是屬于Object中的,在查找頂層常量的時候大多到這一步就結束了,舉個例子

class User
end
module Admin
    class Man
        puts User
    end
end   

上面這段代碼,User能夠正確找到它是個頂層常量,因為Admin::Man的父類是Object

  1. 如果未找到,而且 cref 是個模塊,在 Object 中查找常量。
    這個比較好理解,父級命名空間是個模塊的話,最后也是會到頂層的Object中查找

  2. 如果未找到,在 cref 上調用 const_missing 方法。這個方法的默認行為是拋出 NameError 異常,不過可以覆蓋。

注意,結合2,3這兩點我們會疑惑,好像父級命名空間不論是類還是模塊最終都會去Object中也就是頂層中去查找,事實上如果父級命名空間是類并且繼承自BasicObject(BasicObject是Object的父類)的話,那么在該命名空間下就會連頂層的常量都找不到了

class User
end
module Admin
    class Man < BasicObject
        # 同時測試下第4點提到的const_missing方法,應該是定義在cref中,也就是這里的Admin::Man
        def self.const_missing(*args)
        puts "Ah oh! 找不到了#{args}"
        end
        puts User
    end
end

但是把上面的BasicObject換成Object或者去掉 < BasicObject就不會有這樣的問題

限定常量的解析算法

所謂限定常量指的就是這種形式的常量:

Billing::Invoice

這種形式限定了Invoice常量最起碼都是Billing這個命名空間之下的,這個時候先將左邊的命名空間作為相對常量查找,找到之后再在該常量下或者該常量的祖先類中查找常量

詞匯表

父級命名空間

僅僅指代常量的名稱中去除最右邊的那一部分后剩余的部分

加載機制

Rails中的通過config.cache_classes設置是否需要緩存已加載的常量。如果是true則在程序中使用Kernel#require來加載,否則使用Kernel#load

自動加載可用性

自動加載,就是執(zhí)行代碼時碰到一個暫時未定義的常量(可以是類或者模塊或者一個其他的常量)的時候,Rails會按照該常量的嵌套去嘗試自動加載,比如:

class BeachHouse < House
end

如果加載這個文件時發(fā)現(xiàn)House還未定義,就會去自動加載該類
在Rails的生產環(huán)境中,會及早地加載應用中的文件

autoload_paths

在ruby中,如果使用require引入一個相對文件名時,那么ruby將會在LOAD_PATH中列出的目錄中尋找文件。 而在Rails中,與LOAD_PATH類似,Rails會在autoloads中尋找對應的文件,默認情況下這些目錄包含:

  • 引擎(rails應用和用到Railsengine的gem)中的app目錄下所有子目錄


    image.png

如圖是foreman項目的autoload_paths目錄,因為katello、foreman_remote_execution和foreman-tasks都是用到了RailsEngine所以它們的app目錄也會加在當前的autoload_paths目錄下

  • 應用和引擎中的名為app/*/concerns的二級目錄
  • test/mailers/previews目錄

此外,這些目錄可以使用config.autoload_paths配置。例如,以前這些lib在這一系列目錄中,但是現(xiàn)在不在了,可以通過config/application.rb文件添加下述配置,將其納入其中:

config.autoload_paths << "#{Rails.root}/lib"

自動加載算法

相對引用

以一個例子來講解:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

在這里,PostsController,ApplicationController,Post三者都是屬于相對引用,假設代碼執(zhí)行到這個位置的時候,這個三個常量全都是未定義的情況下,該如何處理,分別對應了3種情況:

  1. class,module后面的關鍵后的常量:即便是一個未定義的常量也不會觸發(fā)自動加載,而是由ruby直接定義這個控制器類
  2. 頂層常量:ApplicationController就是一個頂層常量,因為從當前的代碼來看,外部沒有任何嵌套了。然后rails會開始檢查所有autoload_paths中的目錄,是否有application_controller.rb文件。
  3. 命名空間:不同于ApplicationController是直接去autoload_paths中尋找對應文件,這是因為它沒有嵌套。而Post的就不同了,它的嵌套是PostsController,此時就需要涉及命名空間的算法

那么在上面的代碼中,就會按照:

  1. PostsController::Post
  2. Post的順序來查找

這樣的順序來查找,上面這兩步在rails中有些許的不同:
1.1 查找PostsController::Post時,首先會利用underscore方法,將常量名轉換為文件名,以此來查找是否有對應的posts_controller/post.rb文件


image.png

1.2 如果沒有找到的話,那么會去尋找名叫post的目錄,也就是:

app/assets/posts_controller/post
app/controllers/posts_controller/post
app/helpers/posts_controller/post
...
test/mailers/previews/posts_controller/post

因為,在rails中,只要在autoload_paths目錄中新建了目錄,那么我們即便不顯示地去用module來定義這個模塊,rails也會自動定義一個模塊賦值給該常量


image.png

限定引用

所謂限定引用,前面已經(jīng)提過就是類似:

Billing::Invoice

這樣的形式的調用
如果缺失限定常量,Rails不會在父級命名空間中查找,例如:

#module Man
#    class User
#    end
#end

#class User
#end
module Admin
    # 在Admin中調用User
    User
end

或者

Admin::User

在Admin模塊中調用了User常量,假如User不存在的話,那么Rails僅僅知道當前缺的是Admin模塊下的一個名叫User的常量(而不會知道說這個User應該是在頂級命名空間或者在Man這個模塊下的一個常量)

如果存在一個頂級User常量,那么在前面的例子中ruby將會解析,但是在后者的情況下不會解析(因為已經(jīng)限定了必須是在Admin模塊的命名空間下)。通常來說,Rails的解析算法和Ruby不太一樣,Rails會嘗試以下方式來解析:

如果父級命名空間中的類或者模塊沒有缺失的常量,那么Rails就認為其是一個相對常量。否則是限定常量

比如上面的例子中,父級命名空間就是Admin,并且在此之前我們并沒有定義過Admin::User常量,那么這個時候,就當作相對引用來處理(也就是先查找/admin/user.rb etc.,在查找/user.rb etc.),否則Admin中也存在User常量,那么就當限定引用來處理,也就是當作Admin::User來處理

image.png

再來舉個例子:
假設User是一個已經(jīng)定義了的頂層常量,并且現(xiàn)在有如下調用:

module Admin
  User
end

如果和ruby的解析算法一樣的話,那么很明顯,這里的User就會解析為頂層的那個User,但是對于Rails而言,這樣就不會觸發(fā)自動加載了,所以正如官方提到的,Rails會認為其是一個限定常量,會去查找Admin::User這個常量

總結:

ruby的常量解析算法指示了在ruby中碰到一個常量時會如何去解析它,前提是在所有文件常量都已經(jīng)加載完成的情況下;而Rails還含有自動加載,也就是當常量不存在的時候還會去尋找對應的文件去自動加載,查找順序還是和ruby中的相對引用和限定引用的查找順序一樣,只是在每一層嵌套中查找時,如果未定義的話會先去嘗試自動加載,自動加載仍然沒找到后再去找下一層的嵌套

碰到常量未加載如何解決?

  1. 如果打開控制臺或者模擬的運行環(huán)境(比如rails runner或者直接在項目中打印日志),那么就打印缺失常量所在的命名空間關聯(lián)的模塊/類是否已加載,沒有加載的話,檢查autoload_paths,再檢查這些路徑中是否已經(jīng)有對應相同層級結構的文件或者目錄,有個時候常量找不到可能是因為其父級命名空間的嵌套已經(jīng)被其他地方的同名結構的模塊覆蓋,這個時候也可以找到那個模塊/類,在其中打印日志檢查是否生效
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容