iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Software Development

30天 Lua重拾筆記系列 第 26

【30天Lua重拾筆記25】進階議題: 模組化

同步發表於個人環境

Lua並沒有完整的模組系統,更多的是依賴模組開發者的設計。在Lua 5.1曾經有module()的函數可用,但於Lua 5.2已經被移除。更多的需要使用_G_ENV來使用,相關說明可以參考全局表與環境表

module (name [, ...])

require()之前,需要先說一下如何載入程式檔案並執行。

載入檔案

對於模組化設計,最重要的一點是如何切割程式為不同檔案,接著就是如何串連不同檔案。在之前介紹過load,Lua提供了另一個類似的方法loadfile

示例

首先先建立個 hello.lua 檔案,簡單就好:

print "Hello, World"

然後才是主程式 loadfile_hello_example.lua

hello = loadfile("./hello.lua")
hello() --> Output: Hello, World

如果你分別於REPL環境打入這兩行指令,你會發現第一行指令並沒有任何輸出結果,只得到一個hello函數。只有在第二行執行函式時,才真正執行hello.lua程式檔案的內容。

執行檔案

要執行一個檔案,有更直接的方法:

dofile("./hello.lua") --> Output: Hello, World

這一行就相等於上面兩行~

Require

package.loaded

如果你寫過C/C++的模組,應該使用過#ifndef,來處理同一個檔案多次載入的問題。Lua雖然模組系統是由模組開發者可以客自化的,但提供一個基礎、簡單的require()函數,這個函數會紀錄已經載入過得檔案、模組於package.loaded

hello = require "hello"  --> Output: Hello, World

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

package table: 0x5608ff174710
table table: 0x5608ff174cf0
io table: 0x5608ff1752e0
os table: 0x5608ff175b70
string table: 0x5608ff176760
math table: 0x5608ff176fb0
hello true
debug table: 0x5608ff178730
_G table: 0x5608ff172c00
utf8 table: 0x5608ff178370
coroutine table: 0x5608ff174d60

package.loaded可以發現已經有了hello這個欄位。另外,可以注意到的是,我們當前的寫法使得其值是true,而其他的是table。通常有一些慣例,最後會提到。

喔對,如果再require一次,並不會多一次執行,會直接回傳true

hello = require "hello"  --> nothing
print(hello) --> true

package.path

你可以注意到,require 後面接的參數路徑,並不需要加上副檔名。這與package.path有關。package.path保存了require尋找模組的路徑,以我的環境來看,其結果是:

print(package.path)
--> /usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua

可以注意到當中有./?.lua這段,也就是表示會從當前目錄尋找副檔名為.lua的檔案。package.path中的?;/等等,與package.config有關:

print(package.config)
--[[
/
;
?
!
-

--]]

通常的設計模式

載入、執行檔案,與主程式,為了確保函數安全的執行,有時候會對全局環境(_G)多做一些處理,以保護全局環境。而雖然與主程式執行方式類似,但其實loadfileload很像,對於模組檔案,也可以視為一個chunk,這表示你可以為檔案保留一些僅該檔案可用的局部變數。

在示例之前,習慣上,僅會註冊一個與模組名稱同明得全局變數,並最後回傳它(return/export)。而其餘的僅僅是檔案中的局部變數,並不匯出。Lua自帶的模組osutf8等都符合這樣的設計慣例。

hello1.lua

------ 隱藏的變數 --------
local __VERSION<const> = "1.0.0"
local example_name = "World"
local function helloWorld(name)
  name = name or example_name
  print("Hello, " .. name)
end

-------------


------------
--- 匯出 ---
------------
hello1 = {} -- 要匯出的變數
hello1.__VERSION = __VERSION
hello1.hello = helloWorld

-- 或是明確的註冊於環境裡
_ENV.hello1 = hello1

------------
--- 回傳 ---
--- 緩衝 ---
------------
return hello1

在上例中,於環境註冊了hello結構,並回傳他。這個結構保留了欲匯出的其他變數--__VERSIONhelloWorld函數(重新命名為hello()),example_name並不匯出。

來使用看看:

----- require ------
helloabc = require "hello1"
print(hello1) --> table: 0x55a1dcc6f1b0
print(helloabc) --> table: 0x55a1dcc6f1b0

----- 回傳結果被暫存於`package.loaded` ------
helloabc == package.loaded.hello1  --> true
hello1 == helloabc --> true

----- 無法使用未匯出的變數 -----
print(example_name) --> nil
print(helloWorld) --> nil

---- 可以使用匯出的變數 ------
print(hello1.hello) --> function: 0x55a1dcc70390
hello1.hello() --> Output: Hello, World
helloabc.hello() --> Output: Hello, World

※ 不過你也可以不註冊環境變數,僅僅是回傳,讓使用者自訂模組使用名稱。


上一篇
【30天Lua重拾筆記24】中級議題: coroutine
下一篇
【30天Lua重拾筆記26】進階議題: 錯誤處理
系列文
30天 Lua重拾筆記36

尚未有邦友留言

立即登入留言