Lua的require小結


在游戲開發中會經常使用到lua作為游戲邏輯層的腳本語言,各種優勢就不說了,雖然平時用的比較多,但對lua語言本身和內部的一些實現並不是很了解,讓我們先從lua的require入手來一探require的各種用法吧。

require其實類似與C/C++中的#include,就是加載一個指定名稱的模塊進來,該模塊可以來自於lua,也可能來自於C/C++,在lua虛擬機啟動時,默認會給我們預先加載好一些模塊,保存在package.loaded中,我們可以實際打印一波看看:

for k, v in pairs(package.loaded) do
	print(k, v)
end

可以看到預先加載好的模塊名稱,一目了然。那么,lua又是從哪些地方去加載模塊呢?加載模塊又有什么規則呢?這個就是由package.path指定,同樣可以實際打印一波看看:

如果我們希望修改lua加載模塊的路徑,只要修改這個package.path就可以了。

讓我們回到前面打印的package.loaded結果,我們發現這個table的value都是table,這不禁讓人好奇:require的返回值是什么呢?我們可以自己寫一個簡單的自定義模塊去驗證下:

--mypackage.lua
print("hello world")

然后,執行:

require("mypackage")
for k, v in pairs(package.loaded) do
	print(k, v)
end

誒,發現自定義模塊返回的值是true。這是為什么呢?明明我們的代碼里沒有任何一句return語句。難道是在沒有寫返回值的情況下,默認給我們返回true了?那既然如此,手動顯式加一句return試試:

--mypackage.lua
print("hello world")
return nil

然后require,我們發現結果還是一樣,返回值為true:

其實,lua之所以這么做,是為了避免重復加載同一個模塊,每加載一個模塊,就將模塊的name作為key,模塊的返回值(如果有且不為nil)作為value插入到package.loaded中去。這樣下次再去加載這個模塊時,就無需加載再去執行該模塊的代碼,直接返回package.loaded對應key的value即可。怎么驗證呢?我們可以嘗試require一個模塊兩次試試:

require("mypackage")
require("mypackage")

注意到,hello world只被打印了一次,說明第二次require的時候並沒有執行mypackage中的代碼。require內部實際上是調用了loadfile接口來進行模塊加載,loadfile的返回值是一個函數,執行該函數,相當於執行該模塊的代碼:

f = loadfile("D:/lua/mypackage.lua")
f()

那么,有沒有辦法讓重復require時都去執行模塊的代碼呢?答案是顯而易見的,只要將package.loaded中對應的key刪掉就可以了:

require("mypackage")
package.loaded.mypackage = nil
require("mypackage")

有意思的是,如果我們的模塊返回值為false,或者我們設置package.loaded.mypackage = false時,無論require多少次,都會觸發模塊的加載執行。不過根據我們之前的驗證,這也是符合情理的hhh。說到這里,其實我們就可以自己寫一個簡單的require了:

function require_ex(module)
    if package.loaded[module] then
        return package.loaded[module]
    end

    for pattern in string.gmatch(package.path, '[^;]+%?[^;]+') do
        local path = string.gsub(pattern, '%?', module)
        local fp = loadfile(path)
        if fp then
            local ret = fp()
            if ret ~= nil then
                package.loaded[module] = ret
            else
                package.loaded[module] = true
            end
            return package.loaded[module]
        end
    end
end

有時候,我們希望require進來的模塊是不允許定義全局變量的,因為全局變量會污染我們整個環境,並可能造成意想不到的后果,在lua 5.1,我們可以使用setfenv函數來設置函數環境,而在lua 5.2以上版本,則可以通過修改env參數來解決,loadfile的第三個參數就是函數環境:

    local env = {}
    setmetatable(env, {__index = _G, __newindex = function(t, k, v) print("forbidden global var ", k) end})
    local fp = loadfile(path, nil, env)

如果lua在package.path中找不到對應的lua模塊,那么接下來它會嘗試從C++模塊中加載,類似地,C++路徑是由package.cpath指定的:

針對dll,require內部是使用package.loadlib方法實現的,它接受兩個參數,一是模塊的路徑,二是給lua調用的函數名稱(lua_openxxx)。其他的就基本和前面加載lua模塊一致了,完整的require_ex代碼如下:

function require_ex(module)
    if package.loaded[module] then
        return package.loaded[module]
    end

    for pattern in string.gmatch(package.path, '[^;]+%?[^;]+') do
        local path = string.gsub(pattern, '%?', module)
        local env = {}
        setmetatable(env, {__index = _G, __newindex = function(t, k, v) print("forbidden global var ", k) end})
        local fp = loadfile(path, nil, env)
        if fp then
            local ret = fp()
            if ret ~= nil then
                package.loaded[module] = ret
            else
                package.loaded[module] = true
            end
            return package.loaded[module]
        end
    end

    for pattern in string.gmatch(package.cpath, '[^;]+%?[^;]+') do
        local path = string.gsub(pattern, '%?', module)
        local fp = package.loadlib(path, "luaopen_" .. module)
        if fp then
            local ret = fp()
            if ret ~= nil then
                package.loaded[module] = ret
            else
                package.loaded[module] = true
            end
            return package.loaded[module]
        end
    end
end

如果你覺得我的文章有幫助,歡迎關注我的微信公眾號(大齡社畜的游戲開發之路-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM