佳木斯湛栽影视文化发展公司

主頁 > 知識庫 > Lua中的模塊(module)和包(package)詳解

Lua中的模塊(module)和包(package)詳解

熱門標簽:百度競價排名 網(wǎng)站排名優(yōu)化 地方門戶網(wǎng)站 服務外包 Linux服務器 呼叫中心市場需求 鐵路電話系統(tǒng) AI電銷

前言

從Lua5.1版本開始,就對模塊和包添加了新的支持,可是使用require和module來定義和使用模塊和包。require用于使用模塊,module用于創(chuàng)建模塊。簡單的說,一個模塊就是一個程序庫,可以通過require來加載。然后便得到了一個全局變量,表示一個table。這個table就像是一個命名空間,其內(nèi)容就是模塊中導出的所有東西,比如函數(shù)和常量,一個符合規(guī)范的模塊還應使require返回這個table?,F(xiàn)在就來具體的總結一下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采用的路徑是一連串的模式,其中每項都是一種將模塊名轉換為文件名的方式。require會用模塊名來替換每個“?”,然后根據(jù)替換的結果來檢查是否存在這樣一個文件,如果不存在,就會嘗試下一項。路徑中的每一項都是以分號隔開,比如路徑為以下字符串:

復制代碼 代碼如下:

?;?.lua;c:\windows\&;;/usr/local/lua/?/?.lua

那么,當我們require “mod”時,就會嘗試著打開以下文件:

復制代碼 代碼如下:

mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua

可以看到,require函數(shù)只處理了分號和問好,其它的都是由路徑自己定義的。在實際編程中,require用于搜索的Lua文件的路徑存放在變量package.path中,在我的電腦上,print(package.path)會輸出以下內(nèi)容:

復制代碼 代碼如下:

;.\&;.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會以模塊名作為參數(shù)來調(diào)用這些代碼。如果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的一般工作流程。

奇淫技巧

可以看到,上面總結的都是通過模塊的名稱來使用它們。但有的時候需要將一個模塊改名,以避免名稱沖突。比如有這樣的場景,在測試中需要加載同一模塊的不同版本,而獲得版本之間的性能區(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?,F(xiàn)在好了,對于上面提出的不同版本進行測試的需求,就可以迎刃而解了。

寫一個我們自己的模塊

在Lua中創(chuàng)建一個模塊最簡單的方法是:創(chuàng)建一個table,并將所有需要導出的函數(shù)放入其中,最后返回這個table就可以了。相當于將導出的函數(shù)作為table的一個字段,在Lua中函數(shù)是第一類值,提供了天然的優(yōu)勢。來寫一個我們自己的模塊,代碼如下:

復制代碼 代碼如下:

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ù),然后將這個局部名稱賦予模塊的最終的名稱,代碼如下:

復制代碼 代碼如下:

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ù)傳給模塊。讓我們來做個試驗:

復制代碼 代碼如下:

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。再寫一個文件,代碼如下:

復制代碼 代碼如下:

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文件,打印結果如下:

復制代碼 代碼如下:

test1
1     3

(PS:如果對代碼中的三個點(…)不熟悉的同學,請參考:《Lua中的函數(shù)》一文)經(jīng)過這樣的修改,我們就可以完全不用在模塊中定義模塊名稱,如果需要重命名一個模塊,只需要重命名定義它的文件就可以了。

細心的同學可能注意到了模塊結尾處的return語句,這樣的一個return語句,在定義模塊時,是非常容易漏寫的,怎么辦?如果將所有與模塊相關的設置任務都集中在模塊開頭,就會更好了。消除return語句的一種方法是,將模塊table直接賦值給package.loaded,代碼如下:

復制代碼 代碼如下:

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中有沒有對應模塊的信息;
2.如果有,就直接返回對應的模塊,不再進行第二次加載;
3.如果沒有,就加載,返回加載后的模塊。

再說“環(huán)境”

大家可能注意到了,當我訪問同一個模塊中的其它函數(shù)時,都需要限定名稱,就比如上面代碼中的M。當我把模塊內(nèi)部的一個local函數(shù)由私有改變成公有以后,相應的調(diào)用local函數(shù)的地方都需要修改,加上限定名稱。怎么辦?總不能每次都修改代碼吧。如何一次搞定?是否還記得《Lua中的環(huán)境概念》這篇博文,里面講到的環(huán)境概念在這里就能派上用場。

我們可以讓模塊的主程序塊有一個獨占的環(huán)境,這樣不僅它的所有函數(shù)都可共享這個table,而且它的所有全局變量也都記錄在這個table中,還可以將所有公有函數(shù)聲明為全局變量,這樣它們就都自動地記錄在一個獨立的table中。而模塊所要做的就是將這個table賦予模塊名和package.loaded。比如以下代碼就可以完成:

復制代碼 代碼如下:

local moduleName = ...
 
local M = {}    -- 局部的變量
_G[moduleName] = M     -- 將這個局部變量最終賦值給模塊名
 
package.loaded[moduleName] = M
setfenv(1, M)

這之后,當我們寫下下面的代碼:

復制代碼 代碼如下:

function add(c1, c2)
    return new(c1.r + c2.r, c1.i + c2.i)
end

它其實是和下面的代碼是等價的:
復制代碼 代碼如下:

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)境概念》一文中說的那樣,使用元表,設置__index,模擬繼承來實現(xiàn)。代碼如下:

復制代碼 代碼如下:

local moduleName = ...
 
local M = {}    -- 局部的變量
_G[moduleName] = M     -- 將這個局部變量最終賦值給模塊名
 
package.loaded[moduleName] = M
 
setmetatable(M, {__index = _G})
setfenv(1, M)

上述代碼很簡單,原理在之前的博文中都詳細的講過了,這里不再啰嗦了。由于需要設置元表,所有會有一定的開銷,但是可以忽略的。

方法二:

復制代碼 代碼如下:

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ù)或模塊聲明為局部變量,看以下代碼:

復制代碼 代碼如下:

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) -- 設置完成以后,就不能再使用_G table中的內(nèi)容了

方法三需要做的工作是最多的,而且也是最麻煩的,但是性能是最好的。怎么用,你自己看著辦吧。

module函數(shù)

大家可能也注意到了,在定義一個模塊時,前面的幾句代碼都是一樣的,就分為以下幾步:

1.從require傳入的參數(shù)中獲取模塊名;
2.建立一個空table;
3.在全局環(huán)境_G中添加模塊名對應的字段,將空table賦值給這個字段;
4.在已經(jīng)加載table中設置該模塊;
5.設置環(huán)境變量。

就是這幾步,在每一個模塊的定義之前都需要加上,是不是有點麻煩,在Lua5.1中提供了一個新函數(shù)module,它包括了以上這些步驟完成的功能。在編寫一個模塊時,可以直接用以下代碼來取代前面的設置代碼:

復制代碼 代碼如下:

module(...)

就上面這一小句代碼,它會創(chuàng)建一個新的table,并將其賦予給模塊名對應的全局字段和loaded table,最后還會將這個table設為主程序塊的環(huán)境。默認的情況下,module不提供外部的訪問的,也就是說,你無法訪問前一個環(huán)境了,在再說“環(huán)境”一節(jié),我專門說了三種解決方案。在使用module時是這樣解決的:
復制代碼 代碼如下:

module(..., package.seeall)

這句話的功能就好比之前的功能再加上了setmetatable(M, {__index = _G})。有了這一句代碼,基本上就可以說萬事不愁了。

子模塊與包

Lua支持具有層級性的模塊名,可以用一個點來分隔名稱中的層級。假設一個模塊名為mod.sub,那么它就是mod的一個子模塊。因此,可以認為模塊mod.sub會將其所有值都定義在table mod.sub中,也就是一個存儲在table mod中,且key為sub的table。就好比下述的定義:

復制代碼 代碼如下:

local mod = {sub = {}}

當require一個模塊mod.sub時,require會用原始的模塊名“mod.sub”作為key來查詢table package.loaded和package.preload,其中,模塊名中的點在搜索時沒有任何意義。但是,當搜索一個定義子模塊的文件時,require會將點轉換成另一個字符,通常就是系統(tǒng)的目錄分隔符,轉換之后require就像搜索其他名稱一樣來搜索這個名稱。比如路徑為以下字符串:
復制代碼 代碼如下:

?;?.lua;c:\windows\&;;/usr/local/lua/?/?.lua

那么,當我們require “mod.sub”時,就會嘗試著打開以下文件:

復制代碼 代碼如下:

mod\sub
mod\sub.lua
c:\windows\mod\sub
/usr/local/lua/mod/mod/sub.lua

通過這樣的加載策略,就可以將一個包中的所有模塊組織到一個目錄中。像這些小的功能,都會組合成很多的奇淫技巧,雖然在實際項目中用的不會很多,但是玩起來還是很有意思的。

總結

這一篇文章主要總結了Lua中的兩個非常重要的函數(shù)require和module。希望對大家有用。對于今天的開發(fā)來說,什么都講究模塊開發(fā),而這篇文章總結的就是進行模塊開發(fā)時需要使用的兩個重要函數(shù)。大家在日后構建自己的模塊時,如果有哪里不懂,哪里不清楚,可以再回過頭來閱讀這篇文章,或者可以直接留言和我交流。我相信,分享與交流使我們更進步。

您可能感興趣的文章:
  • Lua教程(十一):模塊與包詳解
  • Lua模塊與包學習筆記
  • Lua的函數(shù)環(huán)境、包實例講解
  • Lua調(diào)用自定義C模塊
  • Lua中使用模塊的一些基礎知識
  • 使用Lua編寫Nginx服務器的認證模塊的方法
  • 在Lua中使用模塊的基礎教程
  • Lua極簡入門指南(六):模塊
  • Lua模塊和模塊載入淺析
  • Lua中的模塊與module函數(shù)詳解
  • 解析Lua中的全局環(huán)境、包、模塊組織結構

標簽:崇左 湖南 湘潭 蘭州 仙桃 銅川 黃山 衡水

巨人網(wǎng)絡通訊聲明:本文標題《Lua中的模塊(module)和包(package)詳解》,本文關鍵詞  ;如發(fā)現(xiàn)本文內(nèi)容存在版權問題,煩請?zhí)峁┫嚓P信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡,涉及言論、版權與本站無關。
  • 相關文章
  • 收縮
    • 微信客服
    • 微信二維碼
    • 電話咨詢

    • 400-1100-266
    芦山县| 永和县| 江口县| 泗阳县| 芜湖县| 汨罗市| 资源县| 老河口市| 江阴市| 辉南县| 涿鹿县| 巫溪县| 库伦旗| 黄石市| 大英县| 油尖旺区| 湄潭县| 策勒县| 宁晋县| 深州市| 松江区| 光山县| 麻城市| 新野县| 安乡县| 资兴市| 余姚市| 遵化市| 木里| 永寿县| 团风县| 永和县| 陕西省| 柳河县| 曲阜市| 瓦房店市| 秀山| 梅州市| 丹江口市| 泽库县| 东光县|