從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會用模塊名來替換每個“?”,然後根據替換的結果來檢查是否存在這樣一個文件,如果不存在,就會嘗試下一項。路徑中的每一項都是以分號隔開,比如路徑為以下字符串:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
那麼,當我們require “mod”時,就會嘗試著打開以下文件:
mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua
可以看到,require函數只處理了分號和問號,其它的都是由路徑自己定義的。在實際編程中,require用於搜索的Lua文件的路徑存放在變量package.path中,在我的電腦上,print(package.path)會輸出以下內容:
;.\?.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對應的地址,在我的電腦上,print(package.cpath)會輸出以下值:
.\?.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會以模塊名作為參數來調用這些代碼。如果lua文件和C程序庫都找不到,怎麼辦?我們試一下,隨便require一個東西,比如:
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的一般工作流程。
奇淫技巧
可以看到,上面總結的都是通過模塊的名稱來使用它們。但有的時候需要將一個模塊改名,以避免名稱沖突。比如有這樣的場景,在測試中需要加載同一模塊的不同版本,而獲得版本之間的性能區別。那麼我們如何加載同一模塊的不同版本呢?對於一個Lua文件來說,我們可以很輕易的改掉它的名稱,但是對於一個C程序庫來說,我們是沒有辦法編輯其中的luaopen_*函數的名稱的。為了這種重命名的需求,require用到了一個小的技巧:如果一個模塊名中包含了連字符,require就會用連字符後的內容來創建luaopen_*函數名。比如:如果一個模塊的名稱為a-b,require就會認為它的open函數名為luaopen_b,並不是luaopen_a-b。現在好了,對於上面提出的不同版本進行測試的需求,就可以迎刃而解了。
寫一個我們自己的模塊
在Lua中創建一個模塊最簡單的方法是:創建一個table,並將所有需要導出的函數放入其中,最後返回這個table就可以了。相當於將導出的函數作為table的一個字段,在Lua中函數是第一類值,提供了天然的優勢。來寫一個我們自己的模塊,代碼如下:
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
上面就是一個最簡單的模塊。在編寫代碼的過程中,會發現必須顯式地將模塊名放到每個函數定義中;而且,一個函數在調用同一個模塊中的另一個函數時,必須限定被調用函數的名稱,然而我們可以稍作變通,在模塊中定義一個局部的table類型的變量,通過這個局部的變量來定義和調用模塊內的函數,然後將這個局部名稱賦予模塊的最終的名稱,代碼如下:
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
這樣,我們在模塊內部其實使用的是一個局部的變量。這樣看起來比較簡單粗暴,但是每個函數仍需要一個前綴。實際上,我們可以完全避免寫模塊名,因為require會將模塊名作為參數傳給模塊。讓我們來做個試驗:
local moduleName = ...
-- 打印參數
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。再寫一個文件,代碼如下:
require "test1"
c1 = test1.new(0, 1)
c2 = test1.new(1, 2)
ret = test1.add(c1, c2)
print(ret.r, ret.i)
將上述代碼保存為test2.lua 將上述代碼放在同一個文件夾下,運行test2.lua文件,打印結果如下:
test1
1 3
(PS:如果對代碼中的三個點(…)不熟悉的同學,請參考:《Lua中的函數》一文) 經過這樣的修改,我們就可以完全不用在模塊中定義模塊名稱,如果需要重命名一個模塊,只需要重命名定義它的文件就可以了。 細心的同學可能注意到了模塊結尾處的return語句,這樣的一個return語句,在定義模塊時,是非常容易漏寫的,怎麼辦?如果將所有與模塊相關的設置任務都集中在模塊開頭,就會更好了。消除return語句的一種方法是,將模塊table直接賦值給package.loaded,代碼如下:
local moduleName = ...
local M = {} -- 局部的變量
_G[moduleName] = M -- 將這個局部變量最終賦值給模塊名
package.loaded[moduleName] = M
-- 後續代碼省略
示例代碼下載:點擊這裡下載
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。比如以下代碼就可以完成:
local moduleName = ...