1. 程式人生 > >Lua學習筆記--模組與包

Lua學習筆記--模組與包

學習完了函式,那麼,一堆函式就成了一個模組,一堆模組就是一個包。今天來學習一下怎麼寫一個模組和怎麼呼叫模組。

一.簡介

Lua的感覺就是簡潔,自由,一個萬能的table可以搞定所有的事情。Lua從5.1開始提供了require(用於載入模組)和module(用於建立模組)的兩個函式增加對模組的支援,當然,不使用這兩個關鍵字也是可以用table自己實現模組載入的。 模組就是一個程式庫,可以通過require載入,然後我們就得到了一個全域性變數,是一個包含這個模組所有內容的table。其實感覺require就像#include一樣,將我們需要的內容包含進來。先看一個簡單的例子:
--載入另一個模組
require "mod"
--呼叫另一個模組中的func函式
mod.func()
當然,還可以把模組設定成一個較短的名稱,供區域性使用,或者設定模組中的某個函式為較短的名稱:
--設定模組的區域性名稱
local m = require "mod"
--設定模組函式區域性名稱
local f = m.func
--使用
m.func()
--或者
f()

二.關於全域性變數

我們之前用的變數,如果沒有加local關鍵字,那就是全域性變數。Lua將所有的全域性變數都儲存在了一個常規的table中,這個table稱為“環境”,這個table被命名為_G。這種實現方式一方面不需要再額外為全域性變數建立資料結構,另一方面,我們也可以像操作普通table那樣操作全域性的table。 一個例子,看一下_G中都有神馬鬼,lua程式碼如下:
--定義一個全域性變數
a = 100;
--自己定義全域性函式
function testfunc()
	print("hehe")
end
--輸出Lua中的全域性變數
for n in pairs(_G) do
	print(n)
end
結果: ipairs
load
package
require
coroutine
select
pairs
rawlen
tonumber
assert
type
os
io
math
debug
next
setmetatable
error
rawset
tostring
dofile
_VERSION
getmetatable
print
table
rawequal
string
rawget
_G
testfunc
pcall
utf8
xpcall
loadfile
a
collectgarbage
請按任意鍵繼續. . . 我們自己定義了兩個全域性變數和函式,不寫local就是全域性的,然後列印所有的全域性變數,發現這兩個也被打出來了。其他的就是偶們熟悉的比如tostring等等方法。但是,這樣的話,如果我們的程式比較大,那麼就會特別凌亂。大家都是全域性的,像找一個函式也比較費勁,還可能有命名衝突,這時候,模組就該登場了。

三.編寫一個簡單的模組

最簡單的方式,就是建立一個全域性的table,然後編寫一堆函式,新增到這個table中去,然後在模組的最後把這個table返回,例如:
--定義一個模組

--模組名
func = {}

--模組內部的內容加上模組名
function func.test1()
	print("test1 func!")
end

function func.test2()
	print("test2 func!")
end

func.num = 10

--最後需要將模組名返回
return func 
這樣,一個最簡單的模組就編寫好了,我們使用的時候,只需要載入這個模組就可以了。但是,這個模組的設計有很多很多缺陷,不過作為測試暫時夠了,先看看怎麼使用這個模組。

四.使用一個模組

載入一個模組還是相對比較簡單的,通過require就可以搞定一切。就是require "模組名",其實require就是負責一個載入的過程,關於table等都是模組內部寫好的,並且返回給require。

關於require的詳細操作:

function require (name)
    if not package.loaded[name] then<span style="white-space:pre">	</span>--判斷模組是否已經載入了,避免重複載入
        local loader = findload(name)
        if loader == nil then
            error("unable to load module " .. name)
        end
        package.loaded[name] = true --避免遞迴載入時死迴圈。
        local res = loader(name)<span style="white-space:pre">	</span>--初始化模組
        if res ~= nil then
            package.loaded[name] = res
        end
    end
    return package.loaded[name]
end
分析一下上面的操作: 1.當我們給出模組名後,Lua並不是無腦的去載入,而是先去package.loaded這個table中檢視,如果這個表中已經有了這個模組,那麼就不會重複載入這個模組。然後通過loader載入相關模組。如果我們想要強制重新載入一個模組,那麼需要先將其刪除,即package.loaded["mod"]=nil,然後再重新require即可。 2.關於findload,因為Lua支援Lua本身載入模組,也支援載入C程式庫的模組,載入Lua模組使用loadfile,載入C程式庫使用loadlib。注意,這一步lua並不會執行這些模組,僅僅是載入。 3.如果開始載入了,就將package.load[mod]置為true,防止有關聯載入時,返回又載入本模組,導致死迴圈。 4.載入完之後,會將package.loaded[mod]置為載入的模組名,注意這個地方,如果載入器中沒有返回模組名,即我們寫模組的時候沒有返回名字,那麼就返回package.loaded中的值。

使用一下上面編寫好的模組:

模組檔案func.lua
--定義一個模組

--模組名
func = {}

--模組內部的內容加上模組名
function func.test1()
	print("test1 func!")
end

function func.test2()
	print("test2 func!")
end

func.num = 10

--最後需要將模組名返回
return func 
測試檔案test.lua:
local m = require "func"

--使用模組中的內容
m.test1()
m.test2()
print(m.num)

print("\n")

--再看一下全域性變數的情況
for n in pairs(_G) do
	print(n)
end
結果: test1 func!
test2 func!
10

setmetatable
rawget
next
tostring
getmetatable
load
rawset
pairs
pcall
os
coroutine
xpcall
dofile
print
select
type
table
tonumber
func
error
debug
utf8
math
ipairs
loadfile
rawlen
string
collectgarbage
_VERSION
rawequal
io
package
require
_G
assert
請按任意鍵繼續. . . 可見,我們使用了另一個模組中的內容,實現了功能的模組化。而且,這次的全域性變數中只有func一個,即我們這個模組,不會再造成特別麻煩的命名衝突等的問題。

關於模組路徑的問題:

還有一個很關鍵的問題,我們寫好了一個模組,要放在哪裡才能被Lua載入呢?按照國際慣例,放在當前路徑是肯定可以的,剛才的例子就是將test.lua,func.lua都放在了編譯出來的Lua直譯器的路徑下的。 搜尋一個檔案時,在windows上,很多都是根據windows的環境變數path來搜尋,而require所使用的路徑與傳統的路徑不同,require採用的路徑是一連串的模式,其中每項都是一種將模組名轉換為檔名的方式。require會用模組名來替換每個“?”,然後根據替換的結果來檢查是否存在這樣一個檔案,如果不存在,就會嘗試下一項。路徑中的每一項都是以分號隔開,比如路徑為以下字串:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
那麼,lua就會嘗試載入下面的內容:
mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua
那麼,這樣,我們就知道Lua載入的預設規則了,但是,萬一我們想把Lua的東東全都放在一個目錄裡面,想改一下這個預設的設定,要怎麼辦呢? 上面的那一串內容儲存在package的一個欄位中,而且,Lua對於Lua模組和C模組是分開的,Lua的路徑在packet.path中,C的在packet.cpath中。我們列印一下瞧瞧:
--輸出Lua預設的載入搜尋路徑
print("Lua:", package.path)
--輸出C預設的載入搜尋路徑
print("C:",package.cpath)
結果: Lua:    C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\lua\?.lua;C:\Users\puppet_master \Desktop\LuaTest\Lua Scrip\lua\?\init.lua;C:\Users\zhangjian \Deskto
p\LuaTest\Lua Scrip\?.lua;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\?\ini
t.lua;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\..\share\lua\5.3\?.lua;C:
\Users\puppet_masterDesktop\LuaTest\Lua Scrip\..\share\lua\5.3\?\init.lua;.\?.l
ua;.\?\init.lua
C:      C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\?.dll;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\..\lib\lua\5.3\?.dll;C:\Users\puppet_master\Desk
top\LuaTest\Lua Scrip\loadall.dll;.\?.dll
請按任意鍵繼續. . .
好大一串,那麼,我們如果想改的話,要在哪裡改呢?當然好改了,我們有Lua的原始碼啊,想改什麼改什麼。在luaconf.h中的LUA_PATH中定義了這個路徑:
#define LUA_PATH_DEFAULT  \
		LUA_LDIR"?.lua;"  LUA_LDIR"?\\init.lua;" \
		LUA_CDIR"?.lua;"  LUA_CDIR"?\\init.lua;" \
		LUA_SHRDIR"?.lua;" LUA_SHRDIR"?\\init.lua;" \
		".\\?.lua;" ".\\?\\init.lua"
#define LUA_CPATH_DEFAULT \
		LUA_CDIR"?.dll;" \
		LUA_CDIR"..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \
		LUA_CDIR"loadall.dll;" ".\\?.dll"
如果想要進行修改就可以改啦。 關於模組名字和模組檔名的問題: 這個我感覺還是一樣的好。省去不少麻煩,還好記。

五.編寫一個真正的模組

上面編寫了一個測試是模組,但是這個模組漏洞百出,非常不嚴格。下面,逐步修改一下這個模組:

為了方便修改模組名稱:

上面的那個模組,直接定義了一個全域性的變數,為了防止衝突,這個全域性變數必定比較長,模組中每個東東都要加上這麼長的一個字首顯然不合適,更要名的是,入過我想改掉模組的名字,那麼,所有的東東的名字就都得重新寫,這簡直是災難。所以,一種偷懶的方法油然而生:
--定義一個模組

--本地模組名
local M = {}
--真正的模組名
func = M


--模組內部的內容加上模組名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

--最後需要將模組名返回
return func 
這種方法就是用了一個本地的,簡單的變數,操作本地的內容,將內容都建立好了,把這個table的引用再給真正的全域性引用,然後返回真正的模組名即可。

如果模組名和模組所在的檔案不同名時,會發生什麼?

我也不知道,事實勝於雄辯: func.lua檔案:
--定義一個模組

--本地模組名
local M = {}
--真正的模組名
mod = M


--模組內部的內容加上模組名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

--最後需要將模組名返回
return mod 
test.lua檔案:
--require 必須跟模組檔名,直接給模組名找不到的
--這裡的檔名不考慮字尾的問題,lua會自動補全字尾
require "func"
for n in pairs(_G) do
	print(n)
end
結果: xpcall
collectgarbage
utf8
require
next
select
package
rawset
error
ipairs
tostring
getmetatable
rawequal
type
dofile
table
os
print
coroutine
string
setmetatable
load
pairs
rawlen
mod
_G
debug
loadfile
pcall
io
assert
tonumber
math
_VERSION
rawget
請按任意鍵繼續. . . 例子中,模組名為mod,模組檔名為func,require時跟的是檔名,但是加載出來之後仍然是模組原來的名字。所以,這樣搞亂七八糟,一般而言,一個檔案作為一個模組再好不過,所以檔名和模組名還是一致為好,否則載入的名字和使用的名字不一樣,蛋疼得要命。

通過改檔名改模組名:

既然一個模組就是一個檔案了,就更想偷懶了,當我們想改名字的時候,改檔名就可以改掉模組名,那該多好啊。下面是一個例子:
--定義一個模組

--由於require函式會將需要的模組名傳遞給該函式,所以...就代表了模組名
local modname = ...
--本地模組名
local M = {}
--真正的模組名直接祖冊到全域性變數中
_G[modname] = M


--模組內部的內容加上模組名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

這樣,require這個模組的時候,模組名會傳遞給...,這個就作為模組名,就是檔名。所以,我們就不需要自己定義檔名了。