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 档案中 somewhere... --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 版本开始具有真正的词法作用域;% 现在是取余运算符。

对于可变参数函数,使用特殊变量 arg 已被弃用,并将从未来版本中删除。可变参数函数应该被重写为使用新的 ... 伪值,但短期解决方法是在函数开头使用

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)

任何使用 tagmethods 的代码都必须重写为使用元方法。

在绝望的情况下,旧版本的 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 #链接数学库

-ldl #链接 dl 库

Lua 不适用于我的 C++ 程序!为什么我收到编译器和链接器错误?

Lua 确实可以与 C++ 一起使用。由于 Lua 头文件是 ANSI C 头文件,因此需要使用 extern "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 的最简单方法是使用 luaL_openlibs(),该函数在文件 linit.c 中定义,或者从该文件复制代码,根据您的需要修改要初始化的库列表。

为什么 API 标头中没有 C++ 所需的 extern "C" 块?

Lua 是用 ANSI C 实现的。对于将 Lua 接口导入其他语言(包括 C++)没有特殊处理。要在 C++ 中使用 Lua 头文件,请将其外部包装
extern "C" {
    #include "lua.h"
}

另一个原因是 Lua 也是正确的 C++ 代码。您可以使用 C++ 编译器编译 Lua,无需任何更改。如果它具有 extern "C" 声明,即使 Lua 作为 C++ 库编译,它也会生成 C 接口。请参阅 BuildingLua

我听说 Lua 是使用 VM(如 Java)实现的。它有文档吗?我可以直接编写 VM 指令吗?

是的,Lua 是使用虚拟机实现的。Lua 将 Lua 代码转换为 VM 指令块,然后执行 VM 指令。

VM 格式没有公开文档,可能会根据 Lua 开发人员的意愿而改变。不建议直接编写 VM 指令。

我怀疑列表中有人(不是 Lua 开发人员)写了一份描述 VM 指令的文档。

如果您对此类内容感兴趣,可以使用 luac -lluac 在标准发行版中提供)查看 Lua 将源代码编译成的 VM 指令。查看 Lua 源代码以了解发生了什么也不难(请参阅标准发行版中的 lvm.c)。

当发生 _CrtIsValidHeapPointer 断言失败时该怎么办?

在进行 Lua 嵌入时,有时如果您没有正确清理堆栈,可能会导致 _CrtIsValidHeapPointer 断言失败。

正如函数 _CrtIsValidHeapPointer 的注释所说:“验证指针不仅是一个有效指针,而且它来自“本地”堆。来自 C 运行时库的另一个副本的指针(即使在同一个进程中)也会被捕获。”

让我们检查一下“无效指针”的详细信息。当 lua_open() 返回一个 lua_State* L 时,它带有一个默认大小为 45 的堆栈。如果您犯了一些错误没有清除函数的返回值,这些垃圾最终会堆积在堆栈上并导致堆栈内存块崩溃。当新的操作需要增加堆栈大小时,会发生 realloc,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 代码可以以预编译的二进制形式存储和加载,该形式可以通过其标准编译器工具 luac 生成。 二进制形式比源代码更节省内存,但也为解释器提供了另一种可能性:解析/编译代码可以完全删除。 这将已经很小的 Lua 核心的大小减少到大约 40K。 以二进制形式存在的 Lua 程序也可以编码为 C 字符串,并轻松存储在主机的源代码中。 因此,在没有文件系统的设备中使用 Lua 很方便。


说明


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2020 年 5 月 15 日下午 7:38 GMT (差异)