Lua 是一门解释型语言,意味着他能执行动态生成的代码,而这主要由于 dofile
和 loadfile
函数的存在。
这两个函数能够让我们的代码加载和执行代码,具体的我们一个个进行分享
类似于 load
函数( 1-3 小节会分享),会从文件 filename
或标准输入中获取代码块。
loadfile 只是将文件内容进行编译,当需要运行时,调用一下即可。 相比于 dofile
, 在多次调用的情况下,可以节省不少的性能开销。
参数:
返回值(有两个返回值):
举些例子:
第一个例子:loadfile 运行正常的 Lua 代码文件
-- 此处不能用 local ,否则 loadfile 就无法使用
-- local age = 28
age = 28
function showAge()
print("main age", age)
end
local load = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/编译/加载的文件.lua")
print("---- 第一次调用 ----")
load()
--> ---- 第一次调用 ----
--> Hello, jiang pengyong.
--> 28
--> main age 28
print("---- 第二次调用 ----")
load()
--> ---- 第二次调用 ----
--> Hello, jiang pengyong.
--> 28
--> main age 28
print(name) --> 江澎涌
showName() --> lua file 江澎涌
以下是 “加载的文件.lua” 的文件内容
print("Hello, jiang pengyong.")
print(age)
showAge()
--- 此处不能用 local ,否则外部就不能使用
--- local name = "江澎涌"
name = "江澎涌"
function showName()
print("lua file", name)
end
第二个例子:loadfile 运行内部有异常的 Lua 代码文件
local load, error = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/编译/error 的文件.lua")
print(load, error) --> function: 0x6000021e8a50 nil
load() --> 运行后有错误,如下图所示
以下是 error 的文件.lua
的文件内容
-- 因为 info 为 nil,调用会有异常
local name = info.name
第三个例子:loadfile 运行不存在的文件
local load, error = loadfile("")
print(load, error) --> nil cannot open : No such file or directory
打开 filename 文件,并编译其内容作为 Lua 块,并执行。
当不带参数调用时,dofile
执行标准输入 stdin
的内容。
如果出现错误,dofile
会将错误抛出来。
dofile
的内部还是调用了 loadfile
函数,可以认为是以下伪代码:
function dofile(filename)
-- 如果有错误,loadfile 则直接抛出
local f = assert(loadfile(filename))
-- 会执行 loadfile 编译完的代码函数,并返回
return f()
end
举些例子:
第一个例子:dofile 运行正常的 Lua 代码文件
-- 此处不能用 local ,否则 dofile 就无法使用
-- local age = 28
age = 28
function showAge()
print("main age", age)
end
dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/编译/加载的文件.lua")
--> (因为 dofile 会直接运行加载的 lua 文件,以下为运行 lua 文件输出的内容)
--> Hello, jiang pengyong.
--> 28
--> main age 28
print(name) --> 江澎涌
showName() --> lua file 江澎涌
以下是 “加载的文件.lua” 的文件内容
print("Hello, jiang pengyong.")
print(age)
showAge()
--- 此处不能用 local ,否则外部就不能使用
--- local name = "江澎涌"
name = "江澎涌"
function showName()
print("lua file", name)
end
第二个例子:dofile 运行内部有异常的 Lua 代码文件
dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/编译/error 的文件.lua")
以下是 error 的文件.lua
的文件内容
-- 因为 info 为 nil,调用会有异常
local name = info.name
第三个例子:dofile 运行不存在的文件
dofile("")
从 chunk 中获取内容,编译为代码块。
参数:
return
)时表示结束。chunk
是字符串,当没有设置时,则默认为 chunk
内容(即 chunk
字符串),若有设置 chunkname
则为 chunkname
的值;如果 chunk
是函数,没有设置时,默认为 load
,若有设置 chunkname
则为 chunkname
的值。返回值:
如果没有语法错误,则将编译后的块作为函数返回;否则,返回 nil 加上错误消息。
Lua 不检查二进制块的一致性,恶意制作的二进制块可能会使解释器崩溃。
load 的功能很强大,但需要谨慎使用。该函数的开销较大且可能会引起诡异问题,所以当有其他的可选方案时,则不使用该函数。
举些例子:
第一个例子:load 加载字符串
name = "江澎涌"
local l, error = load("name = name..'!'")
print("返回值", l, error) --> 返回值 function: 0x600002088e10 nil
print(name) --> 江澎涌
l()
print(name) --> 江澎涌!
l()
print(name) --> 江澎涌!!
第二个例子,load 加载函数
local i = 0
function loadContent()
i = i + 1
if i == 1 then
return "name"
elseif i == 2 then
return " = "
elseif i == 3 then
return "name.."
elseif i == 4 then
return "'.'"
else
-- 空字符串、 nil 、没有值表示结束
--return ""
--return nil
return
end
end
print("常规加载函数:")
name = "江澎涌"
local l, error = load(loadContent)
print("返回值", l, error) --> 返回值 function: 0x60000371cf60 nil
print(name) --> 江澎涌
l()
print(name) --> 江澎涌.
l()
print(name) --> 江澎涌..
第三个例子,load 加载有语法异常的函数
-- info 是 nil,load 加载块则会出异常
local f, error = load("info.name")
print(f, error) --> nil [string "info.name"]:1: syntax error near
load 函数总是在全局环境中编译代码段, 所以即使身处统一作用域的 local
变量也不会被使用。
通过以下的代码,就能很清晰的体会出,load
拿的是全局的变量 num
,而非局部。
num = 10
local num = 1
local l, error = load("num = num + 1; print(num)")
print(l, error) --> function: 0x600000499110 nil
l() --> 11
-- 打印局部的 num
print(num) --> 1
-- 打印全局的 num
print(_G.num) --> 11
如何让 load 能使用局部的
num
和_G
是什么,后续 “Lua 环境” 的文章会进行分享。
可以使用 io.lines
的方法,给 load
提供函数,让 load
的内容来源于文件,这样其实就和 loadfile
的效果是一样的了
local lines = io.lines("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/加载的文件.lua","L")
local l, error = load(lines)
--- 和下面是等效的
local load = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/加载的文件.lua")
loadfile | dofile | load | |
---|---|---|---|
错误处理 | 不会抛出错误,会作为返回值返回 | 直接抛出错误 | 不会抛出错误,会作为返回值返回 |
执行 | 编译文件中的代码后,返回一个可执行函数 | 编译完文件后,立马执行 | 编译完字符串,返回一个可执行函数 |
这些函数没有任何的副作用,它们既不改变或创建变量,也不向文件写入等。 这些函数只是将程序段编译为一种中间形式,然后将结果作为匿名函数返回。Lua 语言中函数定义是在运行时而不是在编译时发生的一种赋值操作。
即加载文件代码,只有在执行了返回的函数后,内部的变量才会赋值。
local f = load("i=1")
print(i) --> nil
f()
print(i) --> 1
有两种方式可以进行预编译:
第一种: 命令行
luac -o 输出预编译的文件名称 需要被预编译的文件
可以使用 -l
选项,列出编译器为代码段生成的操作码。例如运行以下命令
luac -l -o 预编译的文件.lc 预编译的文件.lua
第二种: 借助 string.dump(func, strip)
进行实现
string.dump
会返回将 func
函数返回的字符串编译的二进制字符串。
参数:
看个例子:
p = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/预编译/预编译的文件.lua")
f = io.open("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/预编译/预编译的文件1.lc","wb")
f:write(string.dump(p))
f:close()
有两种方式可以进行预编译:
第一种: 命令行进行运行,和未进行预编译的运行是一样的
lua 预编译的文件.lc
第二种:用前面分享的 loadfile、dofile、load 方法
这三个方法都能执行预编译的内容,所以可以和平常一样使用他们即可,只是 loadfile
和 dofile
要注意他们的 mode
参数,需要使用有二进制的模式 b
和 bt
。
下面这三段的运行结果是一样的
print("----------------------------")
print("loadfile 加载:")
local fun, error = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/预编译/预编译的文件.lc", "b")
print(fun, error)
fun()
--> ----------------------------
--> loadfile 加载:
--> function: 0x600002378cf0 nil
--> Hello, jiang pengyong.
--> 江澎涌 29
print("----------------------------")
print("dofile 加载:")
dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/预编译/预编译的文件.lc", "b")
--> ----------------------------
--> dofile 加载:
--> Hello, jiang pengyong.
--> 江澎涌 29
print("----------------------------")
print("load 加载:")
local lines = io.lines("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、执行和错误/预编译/预编译的文件.lc", 1024)
local l = load(lines)
l()
--> ----------------------------
--> load 加载:
--> Hello, jiang pengyong.
--> 江澎涌 29
Lua 是一门嵌入于其他语言的扩展语言,所以当发生错误时,不能是简单的奔溃或闪退,最终导致宿主程序奔溃,而是要有一套可靠的错误处理机制。
对于一个函数或一段代码发生异常时,基本是两个采取两种方式:
会以 message
为原因抛出异常,并终止程序。如果消息是字符串,error
方法 会在消息的开头添加一些关于错误位置的信息。(如果返回的不是字符串,则会导致错误位置丢失)
参数:
error
抛出的位置,如果为 2 则表示调用 error
的函数的位置,依次往上走。如果为 0 则表示不将位置添加到消息中。默认值为 1举个例子:
function throwError()
--- 不记录抛出异常的位置
error("error test", 0)
--- 抛出异常的位置,即当前
--error("error test", 1)
--- 抛出异常的函数位置,即调用 throwError 的位置
--error("error test", 2)
end
throwError()
function throwError()
--- 不记录抛出异常的位置
--error("error test", 0)
--- 抛出异常的位置,即当前
error("error test", 1)
--- 抛出异常的函数位置,即调用 throwError 的位置
--error("error test", 2)
end
throwError()
此时 error
在第 14 行,具体可移步 github
function throwError()
--- 不记录抛出异常的位置
--error("error test", 0)
--- 抛出异常的位置,即当前
--error("error test", 1)
--- 抛出异常的函数位置,即调用 throwError 的位置
error("error test", 2)
end
throwError()
此时 throwError
在第 18 行,具体可移步 github
如果参数 v
的值为 false(即 nil 或 false),则会抛出错误;否则,返回 assert
所有参数。 如果出现错误,则会以 message
为内容抛出错误。
值得注意,如果验证通过,assert 返回值是他的两个入参,而不是 v
的所有返回值。
function showInfo()
return "江澎涌", 29, 170
end
print(showInfo()) --> 江澎涌 29 170
print(assert(showInfo(), "error test")) --> 江澎涌 error test
print(assert(nil, "error test"))
小技巧:
还记得多值返回和多值入参吗?其实这里可以将函数的返回值直接作为 assert
的入参,直接作为 assert
的错误 message
function showInfoWithError()
return false, "error test inner"
end
print(showInfoWithError()) --> false error test inner
print(assert(showInfoWithError()))
和 java、kotlin 一样,有异常的抛出,就有异常的捕获,Lua 使用 pcall 进行对异常的捕获
会在保护模式下使用给定参数(arg1 , ...
)调用函数 f
。
f
中的任何错误都不会传播,pcall
会捕获错误并返回状态码。
参数:
f
函数的参数返回值:
f
函数所有返回值pcall
会返回 false 以及错误消息举几个例子
正常捕获异常
local ok, msg = pcall(function()
error("error inner")
end)
print(ok, msg) --> false ...2022/10 编译、执行和错误/错误/错误处理.lua:43: error via pcall catch
无异常,携带参数并且多值返回,这里需要多个值承载
local ok, name, age = pcall(function(name, age)
print("receive args: ", name, age) --> receive args: Jiang pengyong 29
return name, age
end, "Jiang pengyong", 29)
print(ok, name, age) --> true Jiang pengyong 29
error 返回一个 table,正如前面所说,error 不止只能返回 string ,可以返回 Lua 各种类型,这里使用 table 可以携带更多的信息。但这里会有一个问题,就是丢失了错误位置。
local ok3, error = pcall(function()
error({ code = 100, msg = "error in a table" })
end)
print(ok3, error.code, error.msg) --> false 100 error in a table
和 pcall
的功能是一样的,都是 try-catch
捕获异常。但 pcall
有一个问题缺少调用栈,虽然有出错代码的位置,但是在排查问题时,这是不够的。所以就有了 xpcall
函数,多了一个 msgh
参数,其他和 pcall
是一样的。
为什么
pcall
没有调用栈?因为在pcall
返回错误时,部分的调用栈就已经被破坏了(从 pcall 到出错之处的部分)
参数:
debug
的函数进行调试,如果需要查看调用栈 debug.traceback
便可查看。local ok, msg = xpcall(function()
error("error via pcall catch")
end, function()
print(debug.traceback())
return "error via msg handle"
end)
print(ok, msg) --> false error via msg handle
debug.traceback() 的详细用法会在后续的文章中分享
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。