Lua 常見問答

lua-users home
Wiki

本網頁包含各種非官方的 Lua 常見問題解答。本網頁由 Lua 社群維護。Lua 官方常見問答 [1] 中的問題,將不會在本網頁中回答。

如果您對 Lua 有疑問,並且在可用的資源,例如 Lua 說明文件和此常見問答,中找不到解答,您可以將問題發布到 lua-l [2]。在最短的時間內讓最多人看到您的問題,這樣您很可能可以及時獲得回應。如果清單上有經驗的人士認為您的問題是常見問題,他們可能會將您的問題新增到此常見問答中。

另請參閱此獨立常見問答 [3]

常見問題解答主題

Lua 程式設計

在 Lua 中如何執行條件編譯?

Lua 不包含預處理器,因此該如何執行條件編譯?首先,條件編譯的目的是什麼?它通常只用於控制函式或變數在全域範圍的定義。在 Lua 中,可以使用 if 敘述輕鬆執行此操作
if some_option then
    my_table = { ... }

    function foo()
        ...
    end
else
    ...
end

有些人認為這仍然會產生位元組程式碼,浪費 CPU 時間和記憶體。由於轉換為位元組程式碼總是可以在離線時完成,因此 CPU 時間的問題可以輕易解決。關於記憶體,位元組程式碼的大小通常與動態記憶體使用量相比微不足道。儘管如此,避免不必要的位元組程式碼的解決方案是使用 C 預處理器

#if SOME_OPTION
    my_table = { ... }

    function foo()
        ...
    end
#else
    ...
#endif

例如,使用 gcc,以上的程式碼可以如下執行

gcc -E -P -DSOME_OPTION -x c test.lua | lua

為什麼 __gc__len 元方法在表格中不起作用?

注意:Lua 5.2 及之後版本都支援 __gc__len 元方法。以下答案僅針對之前版本。

使用者資料物件常常需要一些明確的解構器在物件即將被刪除時執行,而 Lua 提供了 __gc 元方法執行此目的。然而,出於效率考量,這不被允許在表格中執行。

通常,無需在表格中設定解構器,因為表格將自動刪除,而任何表格包含的參考值將會正常強制清除。一種可能的解決方法是建立一個使用者資料、將表格設為使用資料環境的表格,並在表格中放置使用者資料的參考值(確保這是唯一參考使用者資料的資訊)。當表格變得可強制清除時,使用資料的 __gc 元方法將會執行;在發生這件事之前,Lua 實際上不會銷毀表格,因為表格會被使用資料參考。

__len 在表格中計畫於 5.2 版支援。請參閱 LuaFiveTwo

為什麼 Lua 沒有 += 運算子等運算子?

Lua 的設計目標之一是簡潔。大多數語言都很龐大,意即它們建置了許多進階功能。例如是 C、C++、Python、Lisp、ML。少數語言很精簡。例如是 Forth 和 Lua。Lua 的目標是提供少數真正必要的原子功能,若需要,可從中建構許多其他進階功能。從語言中加入 Lua 的進階功能範例包括模組、物件導向、以及現在可以透過 Lua 5 中的協同程式實現的例外和執行緒。不存在的 += 運算子是另一個範例。

我認為原因更務實。因為可提升效率增益,很多人主張加入這些運算子。但在 Lua 中,事情會因為延伸系統而變得混亂。它在某個地方的 lua-l 檔案中... --JohnBelmonte

我想這再基本不過了 - 你必須為小語言應該有的運算子數量劃定界線。允許 += 之後,人們會詢問為什麼不 *=、-= 等。在你不注意的情況下,你已經加入許多運算子,而這些運算子只是讓程式語言更龐大。簡潔精小是 Lua 的教條。

如果你真的想的話,請參閱 CustomOperators

為什麼這段從網路上找來的 Lua 程式碼無法執行?(轉換為 Lua 5)

從 Lua 4.x 到 Lua 5.0,大部分的函式庫函式都移進表格中,例如現在必須從全域表格存取 read()write(),例如 io.read()io.write()。其他函式,例如 readfrom() 不再存在。(如果你的平台支援,可以使用 io.popen()

也有多種語法變化;%upvalue 語法不再存在,因為 Lua 從版本 5.0 起具備真正的詞彙範圍;% 現在是取餘數運算子。

在 vararg 函數中特殊使用 arg 變數已被棄用,且會在未來版本移除。Vararg 函數應改寫以使用新的 ... 偽值,但一個短期的解決方案則是以以下方式來開始函數

function va(x, ...)
  local arg = { n=select('#', ...), ... }
  -- rest of function
end

從 Lua 5.1 開始,簡寫的 for k, v in tab 語法不再可用;應以 for k, v in pairs(tab) 取代。

使用標籤方法的任何程式碼都必須改寫,以便改用元方法。

在令人絕望的情況下,Lua 的舊版本仍可以從 Lua 網頁中的原始碼方式獲得。

相關資訊 - LuaFiveFeaturesMigratingToFiveOne

在錯誤訊息中,"[C]:" 指的是什麼?

這表示錯誤發生在 C/C++ 函數中。

如何避免從硬碟重新載入 Lua 檔案?

讀取檔案時,整個內容會編譯成正常的 Lua 函數。由於 Lua 函數可以儲存在任何常數變數中,你可以透過保留編譯的函數到某個地方,來避免重新載入檔案。舉例來說

foo = assert(loadfile("foo.lua")) -- if there is an compilation error, the script will raise an error here
for i=1,100 do 
  foo() -- call the script file's function 100 time - the file is not loaded from HD!
end

Lua API 編寫

如何編譯使用 Lua 的 C 程式?

如果你使用 gcc 作為編譯器,執行以下指令

gcc -llua program.c -o outputfile

你也可能需要連結額外的函式庫。如果需要,將以下參數加入以上命令中。

-lm #連結到 math 函式庫

-ldl #連結到 dl 函式庫

Lua 沒有正常與我的 C++ 程式運作!為何我會取得編譯器和連結器錯誤?

Lua 確實與 C++ 相容。你必須以 extern "C" 宣告 Lua 標頭檔,因為它們是 ANSI C 標頭檔。詳見下一則常見問題集及 BuildingLua。如需更精緻的解決方案,請參閱 LuaAddons 上的「程式碼包裝器」。

我在執行 Lua 腳本時取得分段錯誤,為何 Lua 無法偵測到錯誤?

Lua 沒有驗證 API 呼叫是否有正確的引數。你可能破壞了堆疊,參照無效的堆疊索引,或在堆疊上推入太多內容(請參閱 luaL_checkstack())。你在除錯時請啟用 API 檢查。針對 Lua 5.1,請以 -DLUA_USE_APICHECK 進行編譯;針對 Lua 5.0 指令,請參閱此訊息:[4]

我在 ltable.c 中間取得神秘崩潰或 malloc 錯誤

你可能建立了一個包含 liblua 兩份拷貝的可執行檔。這通常發生在你將 liblua 以靜態方式連結到一個動態可載入的擴充模組時。

自 Lua 5.0 開始,不支援此項組態;你只能在某特定執行影像中有一個 liblua 執行個體。

我的應用程式曾經正常運作,但我把它更新到 Lua 5.1,它現在在初始化時會崩潰(或:為何我取得「沒有呼叫環境」錯誤?)

從 Lua 5.0 到 Lua 5.1,狀態初始化程序已變更。現在有必要以 lua_call 呼叫各種 luaopen_* 函數。以前這些僅以一般的 C 呼叫來呼叫,但現在這麼做會在 io 函式庫初始化時造成當機(或以上提到的錯誤)。

初始化 lua_State 最簡單的方式是使用定義在檔案 linit.c 中的 luaL_openlibs(),或從那份檔案複製程式碼,並修改函式庫清單以初始化,來符合你的需求。

為何 API 標頭中沒有 C++ 所需的 extern "C" 區塊?

Lua 於 ANSI C 中實現。並未針對其介面導入至其他程式語言(包括 C++)提供特殊處理。若要使用 C++ 中的 Lua 標頭,請外部整合
extern "C" {
    #include "lua.h"
}

另一個原因是 Lua 也是正確的 C++ 程式碼。你可以不用任何變更,以 C++ 編譯器編譯 Lua。如果其具有 `extern "C"` 宣告,則即使將 Lua 編譯為 C++ 函式庫,它也會產生 C 介面。請參閱BuildingLua

我聽說 Lua 是使用 VM(像是 Java)實作的。它有文件記載嗎?我可以在直接寫入 VM 指令嗎?

是的,Lua 是使用虛擬機器 (Virtual Machine) 實作。Lua 將 Lua 程式碼轉換為 VM 指令區塊,然後執行 VM 指令。

VM 格式未公開記載,而且可能會在 Lua 開發者一時興起時變更。並不是真的建議直接嘗試寫入 VM 指令。

我偷偷懷疑名單上的某個人(非 Lua 開發者)寫了一份文件說明 VM 指令。

如果你有興趣了解這類資訊,可以使用 `luac -l`(`luac` 在標準 dağıtım 中提供) 查看 Lua 將來源編譯成的 VM 指令。在 Lua 的來源中探討運作情形也不算太難(請參閱標準 dağıtım 中的 lvm.c)。

當 _CrtIsValidHeapPointer 斷言失敗發生時,該怎麼做?

執行 Lua 內嵌時,有時若你沒有正確清理堆疊,可能會導致 _CrtIsValidHeapPointer 確認失敗。

如同函式 _CrtIsValidHeapPointer 的註解所述:「驗證指標不僅是有效的指標,也驗證它是來自「本機」堆疊的。來自另一個 C 執行時間的指標(即使在同一個程序中)將會被偵測到。」

讓我們檢查「無效指標」的詳細資料。當 lua_open() 回傳 lua_State* L 時,它會搭配預設大小為 45 的堆疊。如果你在某些功能上出錯,無法清除功能的回傳值,這些垃圾最終將堆積在堆疊中,造成堆疊記憶體區塊當機。當新的 op 需要增加堆疊大小時,會發生重新分配,而且 CRT 將發現舊指標無效,這將導致 _CrtIsValidHeapPointer 失敗。

若要修正,只需仔細檢查你的程式碼(使用 lua_gettop(L) 檢查堆疊大小),並清除堆疊中未使用的插槽。

對於「非本機堆疊」的一個,請檢查並確保 Lua 函式庫沒有 CRT 的個別複本。

更多詳細資訊可在此處找到:http://www.blogcn.com/User4/al_lea/blog/39582295.html

為何需要 luaopen_io 呼叫 lua_called?(或:為何我的程式庫建立的文件在 :close() 時會分段錯誤,但其他功能都正常運作?)

POSIX.1 標準規定,必須使用 pclose 而不是 fclose 來關閉使用 popen 建立的 FILE*。IO 函式庫使用函式環境在每個檔案的基礎上指定要使用的關閉函式。luaopen_io 設定自己的函式環境,讓未另行指定的 iolib 函式隱含使用 fclose。

如果你直接呼叫 luaopen_io 而不是透過 lua_call 呼叫它,它會設定呼叫 C 函式的函式環境。(更糟的是,如果你從「外部」呼叫它,甚至不會有呼叫 C 函式,而且會發生不妙的事。)使用 lua_call 可避免此情形發生。

此外,如果你正在撰寫 Lua 函式庫,而且想要傳回一個 iolib 檔案控制代碼,方式並不如想像中容易。除了在「FILE*」中取得並使用元資料表之外,還必須在使用者資料的函式環境(或建立它的函式)中指定一個 __close。

例如

  /* when registering the C function that returns a file handle */
  lua_pushcfunction(L, mycfunction);
  lua_newtable(L);
  lua_pushcfunction(L, myclose);
  lua_setfield(L, -2, "__close");
  lua_setfenv(L, -2);

  /* when creating the actual filehandle */
  FILE** p = (FILE**)lua_newuserdata(L, sizeof(FILE*));
  luaL_getmetatable(L, "FILE*");
  lua_setmetatable(L, -2);
  *p = myfile;

我可以在 C 中如何遍歷表格?

使用 lua_next[5]。如果表格為陣列,則可以呼叫 lua_gettable[6]lua_rawgeti[7] 在迴圈中退出,直到發現 nil 或達到 lua_objlen[8] 為止。在其他情況下,可呼叫 Lua 函式,以對表格進行反覆運算(請參閱 TableSerialization)。

是什麼讓 Lua 比其他語言更容易嵌入?

嵌入第三方程式碼的主要問題在於,主機軟體(或程式設計師)有時必須適應程式碼才能順利運作。Lua 採用多種策略,以防止嵌入式直譯器和主機程式之間產生相互影響的問題。如果你有過使用嵌入式直譯器的經驗,你可能會想問以下問題。

嵌入單元不會限制我的平台和編譯器嗎?

某些專案的核心原始程式碼高度依賴特定作業系統的系統呼叫。當軟體主要針對熱門平台開發,而其他平台則透過獨立移植專案獲得支援時,常會發生這種情況。每個移植專案都可以根據自己的規畫進行,而不用擔心其他專案。條件編譯(#ifdef 等)通常用於為每個受支援平台提供獨立的程式碼序列。

這種方法看似合理,但程式碼在各平台之間的品質可能會有所不同,而且較不熱門的移植版本可能要好幾個月後才會出現。如果嵌入此類程式碼,表示主機程式只能在嵌入單元所支援的平台上執行。如果主機要在所有目標平台上具備相同的功能,則可能有必要延後開發,直到所有移植版本都到位,不然就是使用較早版本的單元。

此外,有些 C 原始程式碼會使用特定編譯器的非標準功能。這是獨立移植專案的另一個常見現象──每個專案可以要求開發人員為自己目標平台使用最熱門的編譯器。如果嵌入式單元依賴編譯器 X 的特殊功能,則主機可能也需要使用編譯器 X 編譯。如果此時主機已依賴編譯器 Y 的不同非標準功能,那麼單元或主機就需要大幅修改。

Lua 在原始程式碼中採用 ANSI C 和 C++ 的共通子集,以避免產生上述所有問題。這讓直譯器很可能可以使用主機已經使用的任何編譯器。此外,Lua 不使用任何特定作業系統的函式,而且完全相同的程式碼庫在所有已知平台上都能正確建置。

如果我嵌入 Lua 原始程式碼,它會不會與我的編碼習慣衝突?

有些組織執行禁止程式設計人員進行作業時出現編譯器警告的程式品質政策。在這些條件下,如果其程式碼產生警告,嵌入式單元可能會是一個主要問題。最好的情況是,可能需要在每個編譯期間檢查警告清單,以確保沒有任何警告來自主機程式。最壞的情況下,可能需要修改單元中所有有問題的程式碼。修改過的單元當然不再符合規格分發,因此每次發佈單元的新版本時,可能都必須重複相同的修改。(開源專案可能會允許修改提交回主要程式庫,但不能保證其他開發人員在未來能保持相同的紀律水準。)

Lua 的設計有考慮到這個問題。在測試期間,原始程式碼會在多個編譯器上使用最高層級的警告進行編譯。所有產生的警告會在程式碼發佈之前消除。

這個 API 會強迫我使用直譯器的複雜結構來交換資料嗎?

在內部,可嵌入程式碼單元可能會使用複雜且深奧的資料結構,通常是基於完全有效的理由。然而,如果單元的 API 也依賴於這些結構,則嵌入程式設計人員可能被迫花時間學習單元的工作方式。這種所謂的「阻抗失配」在嵌入指令碼語言時尤其可能發生。有些直譯器需要主機程式使用直譯器的資料結構來設定其初始狀態(全域變數等)。此外,主機程式和直譯器通常需要不時交換資料值。理想情況下,不應該強迫主機程式為自己的資料使用直譯器的結構,或者不應該在每次交換資料時執行繁瑣的轉換。

Lua 有個簡單的 API,其中可以在不使用任何直譯器的內部結構情況下,建立初始狀態。此外,資料會透過簡單的堆疊與主機程式交換,該堆疊接收並產生其自然 C 資料類型的值。

這個問題的反面是,直譯器可以在其 API 中使用簡單且表現良好的型別,但接著要求主機程式將其自己的深奧資料還原成這些型別以進行交換。這也強迫主機程式永遠使用轉換常式或以簡單的非結構化型別表示自己的資料。Lua 的答案是允許使用所謂的使用者資料型別。這些是 Lua 沒有直接「理解」的記憶體區塊,但 Lua 可以透過呼叫回主機程式的功能來處理這些記憶體區塊。因此,可以讓標準 Lua 作業直接對主機程式的資料進行作業,就像它是直譯器的原生資料一樣。

Lua 的記憶體管理會與主機需求衝突嗎?

Lua 使用它自己的自動記憶體管理,在由主機程式決定的固定大小記憶體空間中執行作業。這表示在執行指令碼期間不會需要任何系統呼叫來配置記憶體。因此,可以在此類呼叫不安全的狀況下使用直譯器。

不過,自動記憶體管理本身可能會造成問題。有些垃圾收集系統會出現「暫停」,在收集週期中主程式會凍結。Lua 的漸進式收集和其它技術有相同的時間負擔,不過它的分佈很平均,是在程式執行過程中以小步驟進行。這樣的系統延遲性非常一致,甚至比使用手動記憶體管理的系統更一致。此外,收集步驟之間的時間和每個步驟所花的時間都能由主機即時控制。因此,內嵌的 Lua 可以輕鬆地針對特定的延遲需求進行組態和測試,並且在開發過程中可以順利通過或失敗測試,而不是在部署後才驚覺出問題。

為了最大的彈性,Lua 可以選擇將主機的使用者資料物件儲存在自己的記憶體空間,或讓主機明確管理其記憶體。對於與直譯器共用的資料,主機不需要擔心垃圾收集。

直譯器對嵌入在有限系統中來說,不會太耗用資源嗎?

Lua 程式碼可以用二進位形式儲存和載入,這個形式可以由它的標準編譯器 luac 產生。二進位形式比原始文字更省記憶體,而且也讓直譯器有另一個可能性:可以完全移除解析器/編譯器程式碼。這可以把已經很小的 Lua 核心縮減到大約 40K。二進位形式的 Lua 程式也可以編碼成 C 字串,並輕鬆地儲存在主機的原始程式碼中。因此,在缺乏檔案系統的裝置中使用 Lua 很方便。


備註


近期變更 · 喜好設定
編輯 · 歷程
上次編輯時間為 2020 年 5 月 15 日下午 7:38 GMT (diff)