前言
從Lua5.1版本開始,就對模塊和包添加了新的支持,可是使用require和module來定義和使用模塊和包。require用于使用模塊,module用于創(chuàng)建模塊。簡單的說,一個模塊就是一個程序庫,可以通過require來加載。然后便得到了一個全局變量,表示一個table。這個table就像是一個命名空間,其內(nèi)容就是模塊中導(dǎo)出的所有東西,比如函數(shù)和常量,一個符合規(guī)范的模塊還應(yīng)使require返回這個table?,F(xiàn)在就來具體的總結(jié)一下require和module這兩個函數(shù)。
require函數(shù)
Lua提供了一個名為require的函數(shù)用來加載模塊。要加載一個模塊,只需要簡單地調(diào)用require “模塊名>”就可以了。這個調(diào)用會返回一個由模塊函數(shù)組成的table,并且還會定義一個包含該table的全局變量。但是,這些行為都是由模塊完成的,而非require。所以,有些模塊會選擇返回其它值,或者具有其它的效果。那么require到底是如何加載模塊的呢?
首先,要加載一個模塊,就必須的知道這個模塊在哪里。知道了這個模塊在哪里以后,才能進行正確的加載。當我們寫下require “mod”這樣的代碼以后,Lua是如何找這個mod的呢?這里面就有說道了,我這里就詳細的說一說。
在搜索一個文件時,在windows上,很多都是根據(jù)windows的環(huán)境變量path來搜索,而require所使用的路徑與傳統(tǒng)的路徑不同,require采用的路徑是一連串的模式,其中每項都是一種將模塊名轉(zhuǎn)換為文件名的方式。require會用模塊名來替換每個“?”,然后根據(jù)替換的結(jié)果來檢查是否存在這樣一個文件,如果不存在,就會嘗試下一項。路徑中的每一項都是以分號隔開,比如路徑為以下字符串:
復(fù)制代碼 代碼如下:
?;?.lua;c:\windows\&;;/usr/local/lua/?/?.lua
那么,當我們require “mod”時,就會嘗試著打開以下文件:
復(fù)制代碼 代碼如下:
mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua
可以看到,require函數(shù)只處理了分號和問好,其它的都是由路徑自己定義的。在實際編程中,require用于搜索的Lua文件的路徑存放在變量package.path中,在我的電腦上,print(package.path)會輸出以下內(nèi)容:
復(fù)制代碼 代碼如下:
;.\&;.lua;D:\Lua\5.1\lua\&;.lua;D:\Lua\5.1\lua\&;\init.lua;D:\Lua\5.1\&;.lua;D:\Lua\5.1\&;\init.lua;D:\Lua\5.1\lua\&;.luac
如果require無法找到與模塊名相符的Lua文件,那Lua就會開始找C程序庫;這個的搜索地址為package.cpath對應(yīng)的地址,在我的電腦上,print(package.cpath)會輸出以下值:
復(fù)制代碼 代碼如下:
.\&;.dll;.\&;51.dll;D:\Lua\5.1\&;.dll;D:\Lua\5.1\&;51.dll;D:\Lua\5.1\clibs\&;.dll;D:\Lua\5.1\clibs\&;51.dll;D:\Lua\5.1\loadall.dll;D:\Lua\5.1\clibs\loadall.dll
當找到了這個文件以后,如果這個文件是一個Lua文件,它就通過loadfile來加載該文件;如果找到的是一個C程序庫,就通過loadlib來加載。loadfile和loadlib都只是加載了代碼,并沒有運行它們,為了運行代碼,require會以模塊名作為參數(shù)來調(diào)用這些代碼。如果lua文件和C程序庫都找不到,怎么辦?我們試一下,隨便require一個東西,比如:
復(fù)制代碼 代碼如下:
require "jellythink"
lua: test.lua:1: module 'jellythink' not found:
no field package.preload['jellythink']
no file '.\jellythink.lua'
no file 'D:\Lua\5.1\lua\jellythink.lua'
no file 'D:\Lua\5.1\lua\jellythink\init.lua'
no file 'D:\Lua\5.1\jellythink.lua'
no file 'D:\Lua\5.1\jellythink\init.lua'
no file 'D:\Lua\5.1\lua\jellythink.luac'
no file '.\jellythink.dll'
no file '.\jellythink51.dll'
no file 'D:\Lua\5.1\jellythink.dll'
no file 'D:\Lua\5.1\jellythink51.dll'
no file 'D:\Lua\5.1\clibs\jellythink.dll'
no file 'D:\Lua\5.1\clibs\jellythink51.dll'
no file 'D:\Lua\5.1\loadall.dll'
no file 'D:\Lua\5.1\clibs\loadall.dll'
是的,會報錯的。以上就是require的一般工作流程。
奇淫技巧
可以看到,上面總結(jié)的都是通過模塊的名稱來使用它們。但有的時候需要將一個模塊改名,以避免名稱沖突。比如有這樣的場景,在測試中需要加載同一模塊的不同版本,而獲得版本之間的性能區(qū)別。那么我們?nèi)绾渭虞d同一模塊的不同版本呢?對于一個Lua文件來說,我們可以很輕易的改掉它的名稱,但是對于一個C程序庫來說,我們是沒有辦法編輯其中的luaopen_*函數(shù)的名稱的。為了這種重命名的需求,require用到了一個小的技巧:如果一個模塊名中包含了連字符,require就會用連字符后的內(nèi)容來創(chuàng)建luaopen_*函數(shù)名。比如:如果一個模塊的名稱為a-b,require就會認為它的open函數(shù)名為luaopen_b,并不是luaopen_a-b。現(xiàn)在好了,對于上面提出的不同版本進行測試的需求,就可以迎刃而解了。
寫一個我們自己的模塊
在Lua中創(chuàng)建一個模塊最簡單的方法是:創(chuàng)建一個table,并將所有需要導(dǎo)出的函數(shù)放入其中,最后返回這個table就可以了。相當于將導(dǎo)出的函數(shù)作為table的一個字段,在Lua中函數(shù)是第一類值,提供了天然的優(yōu)勢。來寫一個我們自己的模塊,代碼如下:
復(fù)制代碼 代碼如下:
complex = {} -- 全局的變量,模塊名稱
function complex.new(r, i) return {r = r, i = i} end
-- 定義一個常量i
complex.i = complex.new(0, 1)
function complex.add(c1, c2)
return complex.new(c1.r + c2.r, c1.i + c2.i)
end
function complex.sub(c1, c2)
return complex.new(c1.r - c2.r, c1.i - c2.i)
end
return complex -- 返回模塊的table
上面就是一個最簡單的模塊。在編寫代碼的過程中,會發(fā)現(xiàn)必須顯式地將模塊名放到每個函數(shù)定義中;而且,一個函數(shù)在調(diào)用同一個模塊中的另一個函數(shù)時,必須限定被調(diào)用函數(shù)的名稱,然而我們可以稍作變通,在模塊中定義一個局部的table類型的變量,通過這個局部的變量來定義和調(diào)用模塊內(nèi)的函數(shù),然后將這個局部名稱賦予模塊的最終的名稱,代碼如下:
復(fù)制代碼 代碼如下:
local M = {} -- 局部的變量
complex = M -- 將這個局部變量最終賦值給模塊名
function M.new(r, i) return {r = r, i = i} end
-- 定義一個常量i
M.i = M.new(0, 1)
function M.add(c1, c2)
return M.new(c1.r + c2.r, c1.i + c2.i)
end
function M.sub(c1, c2)
return M.new(c1.r - c2.r, c1.i - c2.i)
end
return complex -- 返回模塊的table
這樣,我們在模塊內(nèi)部其實使用的是一個局部的變量。這樣看起來比較簡單粗暴,但是每個函數(shù)仍需要一個前綴。實際上,我們可以完全避免寫模塊名,因為require會將模塊名作為參數(shù)傳給模塊。讓我們來做個試驗:
復(fù)制代碼 代碼如下:
local moduleName = ...
-- 打印參數(shù)
for i = 1, select('#', ...) do
print(select(i, ...))
end
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
complex = M
function M.new(r, i) return {r = r, i = i} end
-- 定義一個常量i
M.i = M.new(0, 1)
function M.add(c1, c2)
return M.new(c1.r + c2.r, c1.i + c2.i)
end
function M.sub(c1, c2)
return M.new(c1.r - c2.r, c1.i - c2.i)
end
return complex -- 返回模塊的table
將上述代碼保存為test1.lua。再寫一個文件,代碼如下:
復(fù)制代碼 代碼如下:
require "test"
c1 = test.new(0, 1)
c2 = test.new(1, 2)
ret = test.add(c1, c2)
print(ret.r, ret.i)
將上述代碼保存為test2.lua
將上述代碼放在同一個文件夾下,運行test2.lua文件,打印結(jié)果如下:
復(fù)制代碼 代碼如下:
test1
1 3
(PS:如果對代碼中的三個點(…)不熟悉的同學,請參考:《Lua中的函數(shù)》一文)經(jīng)過這樣的修改,我們就可以完全不用在模塊中定義模塊名稱,如果需要重命名一個模塊,只需要重命名定義它的文件就可以了。
細心的同學可能注意到了模塊結(jié)尾處的return語句,這樣的一個return語句,在定義模塊時,是非常容易漏寫的,怎么辦?如果將所有與模塊相關(guān)的設(shè)置任務(wù)都集中在模塊開頭,就會更好了。消除return語句的一種方法是,將模塊table直接賦值給package.loaded,代碼如下:
復(fù)制代碼 代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
-- 后續(xù)代碼省略
示例代碼下載:點擊這里下載
package.loaded是什么?
require會將返回值存儲到table package.loaded中;如果加載器沒有返回值,require就會返回table package.loaded中的值。可以看到,我們上面的代碼中,模塊沒有返回值,而是直接將模塊名賦值給table package.loaded了。這說明什么,package.loaded這個table中保存了已經(jīng)加載的所有模塊?,F(xiàn)在我們就可以看看require到底是如何加載的呢?
1.先判斷package.loaded這個table中有沒有對應(yīng)模塊的信息;
2.如果有,就直接返回對應(yīng)的模塊,不再進行第二次加載;
3.如果沒有,就加載,返回加載后的模塊。
再說“環(huán)境”
大家可能注意到了,當我訪問同一個模塊中的其它函數(shù)時,都需要限定名稱,就比如上面代碼中的M。當我把模塊內(nèi)部的一個local函數(shù)由私有改變成公有以后,相應(yīng)的調(diào)用local函數(shù)的地方都需要修改,加上限定名稱。怎么辦?總不能每次都修改代碼吧。如何一次搞定?是否還記得《Lua中的環(huán)境概念》這篇博文,里面講到的環(huán)境概念在這里就能派上用場。
我們可以讓模塊的主程序塊有一個獨占的環(huán)境,這樣不僅它的所有函數(shù)都可共享這個table,而且它的所有全局變量也都記錄在這個table中,還可以將所有公有函數(shù)聲明為全局變量,這樣它們就都自動地記錄在一個獨立的table中。而模塊所要做的就是將這個table賦予模塊名和package.loaded。比如以下代碼就可以完成:
復(fù)制代碼 代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
setfenv(1, M)
這之后,當我們寫下下面的代碼:
復(fù)制代碼 代碼如下:
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
它其實是和下面的代碼是等價的:
復(fù)制代碼 代碼如下:
function M.add(c1, c2)
return M.new(c1.r + c2.r, c1.i + c2.i)
end
當我調(diào)用同一個模塊中的函數(shù)new時,也不用指定M了。這樣就可以讓我們在寫自己的模塊時,省去了前綴;還有其它好處,你可以自己想想。但是,當我們調(diào)用setfenv之后,將一個空table M作為環(huán)境后,就無法訪問前一個環(huán)境中全局變量了。這該如何是好?現(xiàn)在提供幾種方法。
方法一:
最簡單的方法就是在《Lua中的環(huán)境概念》一文中說的那樣,使用元表,設(shè)置__index,模擬繼承來實現(xiàn)。代碼如下:
復(fù)制代碼 代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
setmetatable(M, {__index = _G})
setfenv(1, M)
上述代碼很簡單,原理在之前的博文中都詳細的講過了,這里不再啰嗦了。由于需要設(shè)置元表,所有會有一定的開銷,但是可以忽略的。
方法二:
復(fù)制代碼 代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
local _G = _G -- 保存了全局的環(huán)境變量
setfenv(1, M)
這樣在自己的模塊中保存一個全局的環(huán)境變量,當我們訪問前一個環(huán)境中的變量時,就需要添加前綴_G,貌似有點小麻煩。但是,由于沒有涉及到元方法,這種方法會比方法一略快。
方法三:
這種方法是最正規(guī)的方法,就是將那些需要用到的函數(shù)或模塊聲明為局部變量,看以下代碼:
復(fù)制代碼 代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
local sqrt = math.sqrt -- 在我們自己的模塊中需要用到math.sqrt這個函數(shù),所以就先保存下來
local io = io -- 需要用到io庫,也保存下來
setfenv(1, M) -- 設(shè)置完成以后,就不能再使用_G table中的內(nèi)容了
方法三需要做的工作是最多的,而且也是最麻煩的,但是性能是最好的。怎么用,你自己看著辦吧。
module函數(shù)
大家可能也注意到了,在定義一個模塊時,前面的幾句代碼都是一樣的,就分為以下幾步:
1.從require傳入的參數(shù)中獲取模塊名;
2.建立一個空table;
3.在全局環(huán)境_G中添加模塊名對應(yīng)的字段,將空table賦值給這個字段;
4.在已經(jīng)加載table中設(shè)置該模塊;
5.設(shè)置環(huán)境變量。
就是這幾步,在每一個模塊的定義之前都需要加上,是不是有點麻煩,在Lua5.1中提供了一個新函數(shù)module,它包括了以上這些步驟完成的功能。在編寫一個模塊時,可以直接用以下代碼來取代前面的設(shè)置代碼:
復(fù)制代碼 代碼如下:
module(...)
就上面這一小句代碼,它會創(chuàng)建一個新的table,并將其賦予給模塊名對應(yīng)的全局字段和loaded table,最后還會將這個table設(shè)為主程序塊的環(huán)境。默認的情況下,module不提供外部的訪問的,也就是說,你無法訪問前一個環(huán)境了,在再說“環(huán)境”一節(jié),我專門說了三種解決方案。在使用module時是這樣解決的:
復(fù)制代碼 代碼如下:
module(..., package.seeall)
這句話的功能就好比之前的功能再加上了setmetatable(M, {__index = _G})。有了這一句代碼,基本上就可以說萬事不愁了。
子模塊與包
Lua支持具有層級性的模塊名,可以用一個點來分隔名稱中的層級。假設(shè)一個模塊名為mod.sub,那么它就是mod的一個子模塊。因此,可以認為模塊mod.sub會將其所有值都定義在table mod.sub中,也就是一個存儲在table mod中,且key為sub的table。就好比下述的定義:
復(fù)制代碼 代碼如下:
local mod = {sub = {}}
當require一個模塊mod.sub時,require會用原始的模塊名“mod.sub”作為key來查詢table package.loaded和package.preload,其中,模塊名中的點在搜索時沒有任何意義。但是,當搜索一個定義子模塊的文件時,require會將點轉(zhuǎn)換成另一個字符,通常就是系統(tǒng)的目錄分隔符,轉(zhuǎn)換之后require就像搜索其他名稱一樣來搜索這個名稱。比如路徑為以下字符串:
復(fù)制代碼 代碼如下:
?;?.lua;c:\windows\&;;/usr/local/lua/?/?.lua
那么,當我們require “mod.sub”時,就會嘗試著打開以下文件:
復(fù)制代碼 代碼如下:
mod\sub
mod\sub.lua
c:\windows\mod\sub
/usr/local/lua/mod/mod/sub.lua
通過這樣的加載策略,就可以將一個包中的所有模塊組織到一個目錄中。像這些小的功能,都會組合成很多的奇淫技巧,雖然在實際項目中用的不會很多,但是玩起來還是很有意思的。
總結(jié)
這一篇文章主要總結(jié)了Lua中的兩個非常重要的函數(shù)require和module。希望對大家有用。對于今天的開發(fā)來說,什么都講究模塊開發(fā),而這篇文章總結(jié)的就是進行模塊開發(fā)時需要使用的兩個重要函數(shù)。大家在日后構(gòu)建自己的模塊時,如果有哪里不懂,哪里不清楚,可以再回過頭來閱讀這篇文章,或者可以直接留言和我交流。我相信,分享與交流使我們更進步。
您可能感興趣的文章:- Lua教程(十一):模塊與包詳解
- Lua模塊與包學習筆記
- Lua的函數(shù)環(huán)境、包實例講解
- Lua調(diào)用自定義C模塊
- Lua中使用模塊的一些基礎(chǔ)知識
- 使用Lua編寫Nginx服務(wù)器的認證模塊的方法
- 在Lua中使用模塊的基礎(chǔ)教程
- Lua極簡入門指南(六):模塊
- Lua模塊和模塊載入淺析
- Lua中的模塊與module函數(shù)詳解
- 解析Lua中的全局環(huán)境、包、模塊組織結(jié)構(gòu)