一、编译
Lua 是一门解说型言语,意味着他能履行动态生成的代码,而这主要由于 dofile
和 loadfile
函数的存在。
这两个函数能够让咱们的代码加载和履行代码,具体的咱们一个个进行共享
1-1、loadfile(filename, mode, env)
类似于 load
函数( 1-3 小节会共享),会从文件 filename
或规范输入中获取代码块。
loadfile 仅仅将文件内容进行编译,当需求运转时,调用一下即可。 比较于 dofile
, 在屡次调用的情况下,能够节省不少的功用开支。
参数:
- filename:需求编译的文件名。
- mode:控制块是文本仍是二进制(即预编译块)。可选值:“b”(仅二进制块,即预编译代码)、“t”(仅文本块)、“bt”(二进制和文本)。默以为“bt”。
- env:上值环境(具体什么是上值,咱们后续再共享)
回来值(有两个回来值):
- 第一个回来值:经过 loadfile 加载的可运转的代码块函数,但假如加载文件有反常,则该值会回来 nil
- 第二个回来值:假如加载文件有反常,则该值为过错信息,假如加载成功则为 nil
举些比如:
第一个比如: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("---- 第2次调用 ----")
load()
--> ---- 第2次调用 ----
--> 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
1-2、dofile(filename)
翻开 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("")
1-3、load(chunk, chunkname, mode, env)
从 chunk 中获取内容,编译为代码块。
参数:
- chunk:能够是字符串或函数。假如是函数,会重复调用函数,然后将成果进行衔接为字符串,当函数回来空字符串、 nil 、没有值(即
return
)时表明完毕。 - chunkname:用作过错音讯和调试信息的块的称号。假如
chunk
是字符串,当没有设置时,则默以为chunk
内容(即chunk
字符串),若有设置chunkname
则为chunkname
的值;假如chunk
是函数,没有设置时,默以为load
,若有设置chunkname
则为chunkname
的值。 - mode:控制块是文本仍是二进制(即预编译块)。可选值:“b”(仅二进制块,即预编译代码)、“t”(仅文本块)、“bt”(二进制和文本)。默以为“bt”。
- env:上值环境(具体什么是上值,咱们后续文章共享)
回来值:
假如没有语法过错,则将编译后的块作为函数回来;不然,回来 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 <eof>
1-3-1、load 函数的词法定界
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 环境” 的文章会进行共享。
1-3-2、load 的更多玩法
能够运用 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")
1-4、loadfile、dofile、load 的差异
loadfile | dofile | load | |
---|---|---|---|
过错处理 | 不会抛出过错,会作为回来值回来 | 直接抛出过错 | 不会抛出过错,会作为回来值回来 |
履行 | 编译文件中的代码后,回来一个可履行函数 | 编译完文件后,立马履行 | 编译完字符串,回来一个可履行函数 |
1-5、loadfile、dofile、load 副效果
这些函数没有任何的副效果,它们既不改变或创建变量,也不向文件写入等。 这些函数仅仅将程序段编译为一种中间办法,然后将成果作为匿名函数回来。Lua 言语中函数界说是在运转时而不是在编译时产生的一种赋值操作。
即加载文件代码,只有在履行了回来的函数后,内部的变量才会赋值。
local f = load("i=1")
print(i) --> nil
f()
print(i) --> 1
二、预编译
2-1、预编译办法
有两种办法能够进行预编译:
第一种: 指令行
luac -o 输出预编译的文件称号 需求被预编译的文件
能够运用 -l
选项,列出编译器为代码段生成的操作码。例如运转以下指令
luac -l -o 预编译的文件.lc 预编译的文件.lua
第二种: 凭借 string.dump(func, strip)
进行实现
string.dump
会回来将 func
函数回来的字符串编译的二进制字符串。
参数:
- func:需求进行编译的内容函数
- strip:假如设置为 true ,则编译的内容不包含调试信息,以节省空间
看个比如:
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()
2-2、运转预编译文件
有两种办法能够进行预编译:
第一种: 指令行进行运转,和未进行预编译的运转是相同的
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
2-3、预编译的效果
- 预编译办法的代码纷歧定能比源代码更小,可是加载会更快。
- 能够避免被歹意篡改(代码块内部被篡改),但需求留意的是,不要被整个文件的替换,导致运转环境出问题。
三、过错
Lua 是一门嵌入于其他言语的扩展言语,所以当产生过错时,不能是简略的奔溃或闪退,终究导致宿主程序奔溃,而是要有一套牢靠的过错处理机制。
3-1、抛出反常
关于一个函数或一段代码产生反常时,基本是两个采纳两种办法:
- 回来过错码,一般采纳的结构是两个回来参数,第一个值为 nil 、 false ,第二个过错原因
- 经过调用函数 error 或 assert 往外抛出过错
3-1-1、error(message, level)
会以 message
为原因抛出反常,并终止程序。假如音讯是字符串,error
办法 会在音讯的最初增加一些关于过错方位的信息。(假如回来的不是字符串,则会导致过错方位丢掉)
参数:
- message:抛出反常的内容,纷歧定是字符串,能够是任意的类型数据,例如结合 pcall(下一章节共享)则能够运用这一特性,获取更多的反常信息。
- level:指定过错方位。假如为 1 则表明
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
3-1-2、assert(v, message)
假如参数 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()))
3-2、反常捕获
和 java、kotlin 相同,有反常的抛出,就有反常的捕获,Lua 运用 pcall 进行对反常的捕获
3-2-1、pcall(f, arg1, …)
会在保护办法下运用给定参数(arg1 , ...
)调用函数 f
。
f
中的任何过错都不会传达,pcall
会捕获过错并回来状况码。
参数:
- f:需求被捕获反常的函数
- arg1 , … :传入
f
函数的参数
回来值:
- 第一个成果是状况码(一个 boolean ), 假如调用成功且没有过错,则为 true,后面会回来调用的
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
3-2-2、xpcall(f, msgh, arg1, …)
和 pcall
的功用是相同的,都是 try-catch
捕获反常。但 pcall
有一个问题缺少调用栈,虽然有出错代码的方位,可是在排查问题时,这是不够的。所以就有了 xpcall
函数,多了一个 msgh
参数,其他和 pcall
是相同的。
为什么
pcall
没有调用栈?由于在pcall
回来过错时,部分的调用栈就已经被破坏了(从 pcall 到出错之处的部分)
参数:
- msgh:会在产生过错的时分,先调用该函数,咱们能够借用
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吧,码字不易,请多多支撑)
假如觉得本篇博文对你有所启示或是解决了困惑,点个赞或重视我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间共享与你。