前言
從Lua5.1版本開始,就對模塊和包添加了新的支持,可是使用require和module來定義和使用模塊和包。require用于使用模塊,module用于創建模塊。簡單的說,一個模塊就是一個程序庫,可以通過require來加載。然后便得到了一個全局變量,表示一個table。這個table就像是一個命名空間,其內容就是模塊中導出的所有東西,比如函數和常量,一個符合規范的模塊還應使require返回這個table。現在就來具體的總結一下require和module這兩個函數。
require函數
Lua提供了一個名為require的函數用來加載模塊。要加載一個模塊,只需要簡單地調用require “<模塊名>”就可以了。這個調用會返回一個由模塊函數組成的table,并且還會定義一個包含該table的全局變量。但是,這些行為都是由模塊完成的,而非require。所以,有些模塊會選擇返回其它值,或者具有其它的效果。那么require到底是如何加載模塊的呢?
首先,要加載一個模塊,就必須的知道這個模塊在哪里。知道了這個模塊在哪里以后,才能進行正確的加載。當我們寫下require “mod”這樣的代碼以后,Lua是如何找這個mod的呢?這里面就有說道了,我這里就詳細的說一說。
在搜索一個文件時,在windows上,很多都是根據windows的環境變量path來搜索,而require所使用的路徑與傳統的路徑不同,require采用的路徑是一連串的模式,其中每項都是一種將模塊名轉換為文件名的方式。require會用模塊名來替換每個“?”,然后根據替換的結果來檢查是否存在這樣一個文件,如果不存在,就會嘗試下一項。路徑中的每一項都是以分號隔開,比如路徑為以下字符串:
那么,當我們require “mod”時,就會嘗試著打開以下文件:
可以看到,require函數只處理了分號和問好,其它的都是由路徑自己定義的。在實際編程中,require用于搜索的Lua文件的路徑存放在變量package.path中,在我的電腦上,print(package.path)會輸出以下內容:
如果require無法找到與模塊名相符的Lua文件,那Lua就會開始找C程序庫;這個的搜索地址為package.cpath對應的地址,在我的電腦上,print(package.cpath)會輸出以下值:
當找到了這個文件以后,如果這個文件是一個Lua文件,它就通過loadfile來加載該文件;如果找到的是一個C程序庫,就通過loadlib來加載。loadfile和loadlib都只是加載了代碼,并沒有運行它們,為了運行代碼,require會以模塊名作為參數來調用這些代碼。如果lua文件和C程序庫都找不到,怎么辦?我們試一下,隨便require一個東西,比如:
是的,會報錯的。以上就是require的一般工作流程。
奇淫技巧
可以看到,上面總結的都是通過模塊的名稱來使用它們。但有的時候需要將一個模塊改名,以避免名稱沖突。比如有這樣的場景,在測試中需要加載同一模塊的不同版本,而獲得版本之間的性能區別。那么我們如何加載同一模塊的不同版本呢?對于一個Lua文件來說,我們可以很輕易的改掉它的名稱,但是對于一個C程序庫來說,我們是沒有辦法編輯其中的luaopen_*函數的名稱的。為了這種重命名的需求,require用到了一個小的技巧:如果一個模塊名中包含了連字符,require就會用連字符后的內容來創建luaopen_*函數名。比如:如果一個模塊的名稱為a-b,require就會認為它的open函數名為luaopen_b,并不是luaopen_a-b。現在好了,對于上面提出的不同版本進行測試的需求,就可以迎刃而解了。
寫一個我們自己的模塊
在Lua中創建一個模塊最簡單的方法是:創建一個table,并將所有需要導出的函數放入其中,最后返回這個table就可以了。相當于將導出的函數作為table的一個字段,在Lua中函數是第一類值,提供了天然的優勢。來寫一個我們自己的模塊,代碼如下:
上面就是一個最簡單的模塊。在編寫代碼的過程中,會發現必須顯式地將模塊名放到每個函數定義中;而且,一個函數在調用同一個模塊中的另一個函數時,必須限定被調用函數的名稱,然而我們可以稍作變通,在模塊中定義一個局部的table類型的變量,通過這個局部的變量來定義和調用模塊內的函數,然后將這個局部名稱賦予模塊的最終的名稱,代碼如下:
這樣,我們在模塊內部其實使用的是一個局部的變量。這樣看起來比較簡單粗暴,但是每個函數仍需要一個前綴。實際上,我們可以完全避免寫模塊名,因為require會將模塊名作為參數傳給模塊。讓我們來做個試驗:
將上述代碼保存為test1.lua。再寫一個文件,代碼如下:
將上述代碼保存為test2.lua
將上述代碼放在同一個文件夾下,運行test2.lua文件,打印結果如下:
(PS:如果對代碼中的三個點(…)不熟悉的同學,請參考:《Lua中的函數》一文)經過這樣的修改,我們就可以完全不用在模塊中定義模塊名稱,如果需要重命名一個模塊,只需要重命名定義它的文件就可以了。
細心的同學可能注意到了模塊結尾處的return語句,這樣的一個return語句,在定義模塊時,是非常容易漏寫的,怎么辦?如果將所有與模塊相關的設置任務都集中在模塊開頭,就會更好了。消除return語句的一種方法是,將模塊table直接賦值給package.loaded,代碼如下:
示例代碼下載:點擊這里下載
package.loaded是什么?
require會將返回值存儲到table package.loaded中;如果加載器沒有返回值,require就會返回table package.loaded中的值。可以看到,我們上面的代碼中,模塊沒有返回值,而是直接將模塊名賦值給table package.loaded了。這說明什么,package.loaded這個table中保存了已經加載的所有模塊。現在我們就可以看看require到底是如何加載的呢?
1.先判斷package.loaded這個table中有沒有對應模塊的信息;
2.如果有,就直接返回對應的模塊,不再進行第二次加載;
3.如果沒有,就加載,返回加載后的模塊。
再說“環境”
大家可能注意到了,當我訪問同一個模塊中的其它函數時,都需要限定名稱,就比如上面代碼中的M。當我把模塊內部的一個local函數由私有改變成公有以后,相應的調用local函數的地方都需要修改,加上限定名稱。怎么辦?總不能每次都修改代碼吧。如何一次搞定?是否還記得《Lua中的環境概念》這篇博文,里面講到的環境概念在這里就能派上用場。
我們可以讓模塊的主程序塊有一個獨占的環境,這樣不僅它的所有函數都可共享這個table,而且它的所有全局變量也都記錄在這個table中,還可以將所有公有函數聲明為全局變量,這樣它們就都自動地記錄在一個獨立的table中。而模塊所要做的就是將這個table賦予模塊名和package.loaded。比如以下代碼就可以完成:
這之后,當我們寫下下面的代碼:
當我調用同一個模塊中的函數new時,也不用指定M了。這樣就可以讓我們在寫自己的模塊時,省去了前綴;還有其它好處,你可以自己想想。但是,當我們調用setfenv之后,將一個空table M作為環境后,就無法訪問前一個環境中全局變量了。這該如何是好?現在提供幾種方法。
方法一:
最簡單的方法就是在《Lua中的環境概念》一文中說的那樣,使用元表,設置__index,模擬繼承來實現。代碼如下:
上述代碼很簡單,原理在之前的博文中都詳細的講過了,這里不再啰嗦了。由于需要設置元表,所有會有一定的開銷,但是可以忽略的。
方法二:
這樣在自己的模塊中保存一個全局的環境變量,當我們訪問前一個環境中的變量時,就需要添加前綴_G,貌似有點小麻煩。但是,由于沒有涉及到元方法,這種方法會比方法一略快。
方法三:
這種方法是最正規的方法,就是將那些需要用到的函數或模塊聲明為局部變量,看以下代碼:
方法三需要做的工作是最多的,而且也是最麻煩的,但是性能是最好的。怎么用,你自己看著辦吧。
module函數
大家可能也注意到了,在定義一個模塊時,前面的幾句代碼都是一樣的,就分為以下幾步:
1.從require傳入的參數中獲取模塊名;
2.建立一個空table;
3.在全局環境_G中添加模塊名對應的字段,將空table賦值給這個字段;
4.在已經加載table中設置該模塊;
5.設置環境變量。
就是這幾步,在每一個模塊的定義之前都需要加上,是不是有點麻煩,在Lua5.1中提供了一個新函數module,它包括了以上這些步驟完成的功能。在編寫一個模塊時,可以直接用以下代碼來取代前面的設置代碼:
子模塊與包
Lua支持具有層級性的模塊名,可以用一個點來分隔名稱中的層級。假設一個模塊名為mod.sub,那么它就是mod的一個子模塊。因此,可以認為模塊mod.sub會將其所有值都定義在table mod.sub中,也就是一個存儲在table mod中,且key為sub的table。就好比下述的定義:
那么,當我們require “mod.sub”時,就會嘗試著打開以下文件:
總結
這一篇文章主要總結了Lua中的兩個非常重要的函數require和module。希望對大家有用。對于今天的開發來說,什么都講究模塊開發,而這篇文章總結的就是進行模塊開發時需要使用的兩個重要函數。大家在日后構建自己的模塊時,如果有哪里不懂,哪里不清楚,可以再回過頭來閱讀這篇文章,或者可以直接留言和我交流。我相信,分享與交流使我們更進步。
新聞熱點
疑難解答