16 分钟
LuaJIT 和 Lua 5.1
概述
Lua 在众多领域使用广泛,如游戏等。本文主要面向学习和使用 OpenResty 开发者。因此本文介绍的 Lua 版本是 OpenResty 使用的 Lua 解释器,是兼容 Lua 5.1 语法 LuaJIT。
此外需要注意的是 OpenResty 使用的是其自己维护的 LuaJIT 的分支 (总体来看,在特性上:OpenResty LuaJIT 分支 > LuaJIT > Lua 5.1
)。
示例代码参见 github: rectcircle/lua-learn
开发环境搭建
编译安装
git clone https://luajit.org/git/luajit.git
cd luajit
MACOSX_DEPLOYMENT_TARGET=12.5.1 make && sudo make install
sudo ln -sf luajit-2.1.0-beta3 /usr/local/bin/luajit
luajit --help
cd ..
rm -rf luajit
说明:
- 官方建议:始终使用 v2.1 分支的代码编译。
- Mac 编译需要
MACOSX_DEPLOYMENT_TARGET
环境变量,指向系统版本(系统偏好设置 > 软件更新查看) - 本例使用的代码版本为: 03080b795aa3496ed62d4a0697c9f4767e7ca7e5。
- 不使用
PREFIX=/home/myself/lj2
参数,将安装到/usr/local
相关目录下。 - 卸载使用
sudo make uninstall
命令。
更多参见:官方安装手册。
VSCode 扩展
- 语言服务器 sumneko.lua。更多参见:官方 wiki。
- 调试器 actboy168.lua-debug。更多参见:官方 wiki。
cmd + ,
, 配置使用 luajit 版本:
{
"Lua.telemetry.enable": false,
"Lua.runtime.version": "LuaJIT",
"lua.debug.settings.luaVersion": "jit",
}
运行测试
hello.lua
print('hello')
- 方式 1:命令行执行
luajit hello.lua
。 - 方式 2:VSCode 调试,按
F5
。
语言特性
定位
Lua 是一个语法简单的,被定位为嵌入到 C 语言中,作为动态配置、数据处理等业务场景的脚本语言。
本部分仅介绍 Lua 语言自身的部分,不介绍 C 和 Lua 的交互。
注释
和 sql 语言类似,使用 --
开启一个注释。
-- 短注释,以 -- 开头
Lua 还支持一种长注释。--[[内容支持换行]]
或 --[==[内容支持换行]==]
,这里的 =
可以有多个,只要能对的上,且长度匹配都可以。
--[[
长注释,--紧接着长括号,本例中为 0 级长括号
]]
--[==[
长注释,--紧接着长括号,本例中为 2 级长括号
]==]
类型和操作符
Lua 一共有 8 种数据类型,分别是: nil, boolean, number, string, function, userdata, thread, table。
下文将使用了两个内置函数介绍这 8 种数据类型:
print
打印 0 到多个值到标准输出。type
获取一个值的类型名。
nil
nil 即空类型(和其他语言的 null/nil 类型),只有一种值 nil。
print(type(nil), nil)
-- nil nil
boolean
boolean 值只有两种值 true、false。
print(type(true), true)
print(type(false), false)
-- boolean true
-- boolean false
和 Python 类似, Lua 采用 and
、 or
、 not
作为逻辑运算符。
print(true and false, true or false, not false)
-- false true true
number
number 类型,实现为双精度浮点数。
print(type(1.1), 1.1)
print(type(1), 1)
print('字面量: 数字', 3, 3.0, 3.1416, 314.16e-2, 0.31416E1, 0xff, 0x56)
-- number 1.1
-- number 1
-- 字面量: 数字 3 3 3.1416 3.1416 3.1416 255 86
和其他语言类似,Lua 支持 +
、 -
、 *
、 /
、 %
算数运算符,除此之外,Lua 还原生支持 ^
次幂运算符。
print(1+1, 2-1, 2*3, 1/2, 5%2, 2^10)
-- 2 1 6 0.5 1 1024
同样, Lua 支持关系运算符, ==
、 ~=
、 <=
、 >=
、 <
、 >
。这里需要特别说明的是:不等于使用的时 ~=
而不是常见的 !=
。
print(1 == 1, 1 == '1', 1 ~= 2, 1 <= 2, 1 >= 2, 1 < 2, 1 > 2)
-- true false true true false true false
需要特别说明的时,Lua 并没有提供原生的位运算运算符,如需使用,可以搜索一些第三方库。
string
string 类型,即字节数组。需要特别说明的是 Lua 不关心 string 的字符集。
print(type('string'), 'string')
-- string string
在 Lua 中,字符串字面量支持单引号、双引号以及长括号。长括号形如 [[内容]]
或 [==[内容]==]
,这里的内容
支持换行等任意特殊字符,=
支持 0 个或多个。
print([[支持单引号,使用 \ 转义]], 'alo\n123"')
print([[支持双引号,使用 \ 转义]], "alo\n123\"")
print([[\数字字面量,转移 ascii 码]], '\97lo\10\04923"')
print('[[ ]] 支持多行字符串(0 级长括号)',[[alo
123"]])
print('[==[ ]==] 支持多行字符串(可以有多个=号)(2 级长括号)', [==[
alo
123"]==])
-- 支持单引号,使用 \ 转义 alo
-- 123"
-- 支持双引号,使用 \ 转义 alo
-- 123"
-- \数字字面量,转移 ascii 码 alo
-- 123"
-- [[ ]] 支持多行字符串(0 级长括号) alo
-- 123"
-- [==[ ]==] 支持多行字符串(可以有多个=号)(2 级长括号) alo
-- 123"
function
Lua 本质上以一种长得很像 C 语言的函数式编程语言,因此 Lua 的函数也是第一公民,函数也是一种数据类型。
function Add(a, b)
return a + b
end
print(type(Add), Add)
-- function function: 0x010564a058
更多关于 function 的详细介绍,参见下文。
userdata
userdata 类型,该类型的具体类型由通过 C 语言定义,并提供 Lua 可以调用的相关函数。该数据类型涉及到与 C 语言互操作,本文不多介绍。
thread
thread 类型,即 coroutine,协同程序,协程。和操作系统线程相比相同点事,拥有独立的堆栈、局部变量、和指令指针。但是不同点在于:
- 协程切换只能通过代码来实现,而不是操作系统线程是由操作系统来控制。
协程是串行的,而不是并发的。
T = coroutine.create(function() print('thread running') end) print(type(T), T) coroutine.resume(T) -- thread thread: 0x01053a07e0 -- thread running
更多关于 thread 的详细介绍,参见下文。
table
table 类型,可以用来实现对应其他语言的 array 和 map 相同的能力。
print(type({}), {})
-- table table: 0x010539e4c0
table 作为数组使用。
for i, v in ipairs({'a', 'b', 3}) do
print(i, v)
end
-- 1 a
-- 2 b
-- 3 3
for i, v in ipairs({ a = 1, [0] = 'aa', [1] = 'bb', [2] = 'cc', [4] = 3 }) do
print(i, v)
end
-- 1 bb
-- 2 cc
可以看出:
- table 作为数组使用时,下标是从 1 开始的,这和大多数主流编程语言都不一样。
- 在遍历数组时,如果 table 中,有数字非数字的,不是从 1 连续的,都会被忽略。
- 在遍历数组时,一定会按照从 1 开始从小到大依次遍历,因此可以使用 table.sort 对数组进行排序。
table 作为 map 使用。
for i, v in pairs({ a = 1, [0] = 'aa', [1] = 'bb', [2] = 'cc', [4] = 3 }) do
print(i, v)
end
-- 0 aa
-- 1 bb
-- 2 cc
-- a 1
-- 4 3
需要特别说明的是,table 作为 map 时,遍历是 key 的顺序时不保证的,如需保证顺序,需转换为数组再排序。
table 元素的访问。
T = { a = 1, [1] = 'b', ['1c']= 2 }
print(T['a'], T[1], T['1c'])
print(T.a)
-- 1 b 2
-- 1
即支持两种模式:
T['a']
中括号,支持访问任意元素。T.a
点号,只支持下标为符合 lua 标识符规则的元素,如上的 1 和1c
都不行。
自动类型转换
Lua 在进行 string 和 number 的操作时,会进行自动类型转换。
print(type(1 + "2")) -- 自动转为数字
print(type(1 .. "2")) -- 自动转为字符串
-- number
-- string
当然,也可以手动进行转换
print(type(tonumber('1')))
print(type(tostring(1)))
-- number
-- string
#
运算符
可用于获取
- 字符串的字节长度。
table 的 下标为数字 1 开始的连续的下标的最大值。
print(#'abc', #{1,2,3}, #{a=1, b=2}, #{a=1, b=2, 1, 2, 3}, #{[0]=0, [2]=2, [4]=4}) -- 3 3 0 3 4 -- 注意:这里最后一个实测返回 4,有些奇怪。
变量
Lua 的变量分为全局变量和局部变量。如下所示:
- 定义一个全局变量的方式为:
变量名 = 表达式
,表达式为必选。 定义一个局部变量的方式为:
local 变量名 = 表达式
,表达式为可选。A = 1 local b = 2 local c print(A, b, c) -- 1 2 nil
关于 Lua 变量,需要注意的是:
Lua 的全局变量的作用域是整个程序,也就是说,假如在一个函数中定一个了全局变量,在调用后,该变量在函数外面仍能够访问到。
function F1() D1 = 1 end F1() print(D1) -- 1
Lua 的局部变量的表现和其他语言类似,脱离作用域后,将不存在。
function F2() local d2 = 1 end F2() print(d2) -- nil
Lua 是动态类型的,也就是说,Lua 变量不会和类型绑定,不同类型的值可以复制给同一个变量。
E = 1 print(type(E), E) E = 'string' print(type(E), E) -- number 1 -- string string
Lua 的同一个名字的局部变量可以定义多次,后面定义的会隐藏前面定义的。
流程控制
if elseif else
function IfNumber(i)
if i > 0 then
print('>0')
elseif i < 0 then -- 可选
print('<0')
else -- 可选
print('==0')
end
end
IfNumber(1)
IfNumber(0)
IfNumber(-1)
-- >0
-- ==0
-- <0
while 和 until
local i = 0
while i < 10 do
if i == 5 then
break
end
print('while', i)
i = i + 1
end
i = 0
repeat
print('until', i)
i = i + 1
until i >= 5
-- while 0
-- while 1
-- while 2
-- while 3
-- while 4
-- until 0
-- until 1
-- until 2
-- until 3
-- until 4
需要特别说明的是,Lua 不支持 continue。
for
数字循环 for i = start, end, step do ... end
。
for i = 1, 3, 1 do -- 起始(包括), 结束(包括), 步长(可以省略, 默认为 1)
print('for-num', i)
end
-- for-num 1
-- for-num 2
-- for-num 3
table (数组) 通过 ipairs
函数返回的迭代器进行遍历。
for k, v in ipairs({ "one", "two", "three" }) do
print('for-it', k, v)
end
-- for-it 1 one
-- for-it 2 two
-- for-it 3 three
以上等价于:
do
local f, s, var = ipairs({ "one", "two", "three" })
while true do
local k, v = f(s, var)
var = k
if var == nil then break end
print('for-mock', k, v)
end
end
-- for-mock 1 one
-- for-mock 2 two
-- for-mock 3 three
table (map) 通过 pairs
函数返回的迭代器进行遍历。
for k, v in pairs({a=1, b=2, c=3}) do
print('for-it', k, v)
end
-- for-it c 3
-- for-it a 1
-- for-it b 2
函数
函数定义
在 Lua 中,函数也是一种数据类型,因此也分全局和局部函数。
function GlobalAdd1(a, b) -- 全局函数(方式 1)
return a + b
end
GlobalAdd2 = function(a, b) -- 全局函数(方式 2)
return a + b
end
local function localAdd1(a, b) -- 局部函数(方式 1)
return a + b
end
local localAdd2;
localAdd2 = function(a, b) -- 局部函数(方式 2)
return a + b
end
print('add', GlobalAdd1(1, 1), GlobalAdd2(1, 1), localAdd1(1, 1), localAdd2(1, 1))
-- add 2 2 2 2
定义到 table 中
local t1 = { name = "abc" }
function t1.PrintName1() -- 方式 1
print("print t1.name 1", t1.name)
end
t1.PrintName2 = function() -- 方式 2
print("print t1.name 2", t1.name)
end
t1.PrintName1()
t1.PrintName2()
-- print t1.name 1 abc
-- print t1.name 2 abc
函数返回值
Lua 函数支持返回 0 到多个返回值。
local nilReturn = (function() return end)()
print("nil return", nilReturn)
function r() return 1, 2, 3 end
local rr1 = r()
print('返回多个值, 接收 1 个', rr1)
local rr1, rr2 = r()
print('返回多个值, 接收 2 个', rr1, rr2)
local rr1, rr2, rr3 = r()
print('返回多个值, 接收 3 个', rr1, rr2, rr3)
-- nil return nil
-- 返回多个值, 接收 1 个 1
-- 返回多个值, 接收 2 个 1 2
-- 返回多个值, 接收 3 个 1 2 3
函数参数
Lua 函数参数在调用时:
- 如果传递的参数少于函数声明的数目,则填充 nil。
- 如果传递的参数多余函数声明的数目,多余的将被忽略。
如果将一个函数的调用作为函数参数,且这个函数有多个返回值。
- 如果函数调用作为函数的最后一个参数,则所有返回值都会作为参数都会传递。
如果函数调用不是作为函数的最后一个参数,则之后将第一个返回值作为参数传递。
function r() return 1, 2, 3 end function f(a, b, c) print('f(a, b, c) params:', a, b, c) end f(3, 4) f(3, 4, 5) f(3, 4, 5, 6) f(r()) f(r(), 10) f(10, r()) -- f(a, b, c) params: 3 4 nil -- f(a, b, c) params: 3 4 5 -- f(a, b, c) params: 3 4 5 -- f(a, b, c) params: 1 2 3 -- f(a, b, c) params: 1 10 nil -- f(a, b, c) params: 10 1 2
Lua 支持可变参数 ...
。
- 在函数形参列表的最后可以通过
...
声明可变参数。 - 可以通过
{...}
将可变参数转换为一个 table (数组)。 - 可以通过
unpack
函数,将一个 table (数组) 作为可变参数进行传递。 ...
可以直接传递给其他函数的可变参数。通过
select
函数可以获取可变参数的长度或者截取从 index 开始到之后的可变参数。function r() return 1, 2, 3 end function g(a, b, ...) print('params a, b, {...}, select(#, ...), #{select(2, ...)}, ... :', a, b, { ... }, select('#', ...), #{select(2, ...)}, ...) end g(3) g(3, 4) g(3, 4, 5, 8) g(5, r()) g(unpack({ 'a', 'b', 'c', 'd', 'e' })) -- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... : 3 nil table: 0x010818d160 0 0 -- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... : 3 4 table: 0x010818d6f8 0 0 -- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... : 3 4 table: 0x010818d518 2 1 5 8 -- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... : 5 1 table: 0x0108184c80 2 1 2 3 -- params a, b, {...}, select(#, ...), #{select(2, ...)}, ... : a b table: 0x01081854f8 3 2 c d e
方法
调用或定义 table 的一个函数时,可以使用 :
语法糖,以实现类似其他语言方法的能力:
- 定义时使用
:
,则可以在函数体里面隐含一个self
变量,指向调用者。 调用时使用
:
,则会将调用对象作为参数传递到函数的第一个参数的位置。local t2 = { total = 0 } function t2:Add1(a) -- 语法糖,隐含一个 self 变量,等价于下方 t2 self.total = self.total + a end function t2.Add2(self, a) self.total = self.total + a end t2:Add1(1) -- 语法糖,隐含一个 self 变量传递 t2.Add1(t2, 1) t2:Add2(1) -- 语法糖,隐含一个 self 变量传递 t2.Add2(t2, 1) print('t2.total = ', t2.total) -- t2.total = 4
错误处理
产生错误
通过 assert 或者 error 函数可以产生一个错误。如果没有捕捉的话,错误会中断整个程序的执行。
assert(1 == 1, '断言函数的消息, 如果第一个参数是 false, 则触发 error')
-- error (message [, level]) -- 抛出异常
捕获错误
通过 pcall 和 xpcall 可以捕捉错误。
print('pcall has error', pcall(function() error("my error") end))
print('pcall success', pcall(function() return 1 end))
print('xpcall success', pcall(function() return 1 end, function(err) print(err) end))
print('xpcall 1', xpcall(function() error("my error") end, function(err) print(err) end))
print('xpcall 2', xpcall(function() error("my error") end, function(err) print(err) return err end))
-- pcall has error false hello.lua:1: my error
-- pcall success true 1
-- xpcall success true 1
-- hello.lua:4: my error
-- xpcall 1 false nil
-- hello.lua:5: my error
-- xpcall 2 false hello.lua:5: my error
协程
创建协程
通过 coroutine.wrap
和 coroutine.create
可以创建一个协程。
local cof1 = coroutine.wrap(function()
print('coroutine.wrap called')
end)
local co1 = coroutine.create(function()
print('coroutine.create called')
end)
启动协程
通过 coroutine.wrap
创建的协程,可以通过函数调用的方式启动。
通过 coroutine.create
创建的协程,可以通过 coroutine.resume
方式启动。
cof1('cof1')
coroutine.resume(co1, 'co1')
-- coroutine.wrap called cof1
-- coroutine.create called co1
协程 yield 和 返回值
- 在协程内部,可以通过
coroutine.yield
函数:- 暂停该协程的执行。
- 对该协程
coroutine.resume
的调用将返回,第一个返回值是 bool,表示协程是否没有发生错误。- 如果第一个返回值为 false,则第二个返回值为错误信息。
- 如果第一个返回值为 true,第二个极其之后的返回值,是
coroutine.yield
函数传递的内容。
- 当协程函数返回后, 对该协程
coroutine.resume
的调用将返回,返回内容似乎协程函数的返回值。 对协程调用
coroutine.resume
时:- 如果协程没有启动过,
coroutine.resume
的参数将作为协程函数的参数进行传递。 如果协程启动过,并通过
coroutine.yield
暂停执行,coroutine.resume
的参数将作为coroutine.yield
的返回值返回。local co2 = coroutine.create(function(a, msg) print('[co2] a, msg: ', a, msg) local a3, msg3 = coroutine.yield(2, 'coroutine.yield 被调用') print('[co2] a3, msg3: ', a3, msg3) return 4, '返回' end) local ok2, a2, msg2 = coroutine.resume(co2, 1, 'coroutine.resume 第一次调用') print('[main] ok2, a2, msg2: ', ok2, a2, msg2) local ok4, a4, msg4 = coroutine.resume(co2, 3, 'coroutine.resume 第二次调用') print('[main] ok4, a4, msg4: ', ok4, a4, msg4) -- [co2] a, msg: 1 coroutine.resume 第一次调用 -- [main] ok2, a2, msg2: true 2 coroutine.yield 被调用 -- [co2] a3, msg3: 3 coroutine.resume 第二次调用 -- [main] ok4, a4, msg4: true 4 返回
- 如果协程没有启动过,
其他协程函数
coroutine.status(thread)
获取给定协程对象的状态, dead, running, suspend, normal。coroutine.running()
获取当前函数所在协程对象,main 协程将返回 nil。
元表
在 Lua 中,8 种数据类型值的各种操作,如 + - * / . [] 等,都是通过一种称为 metatable 的机制实现的。
- 除了 userdata、table 类型外,其他每种类型的所有值,都共享一套内建的 metatable。
- userdata 和 table,每个对象(实例),都可以配置绑定一个自定义的 metatable。
- 只有 table 的 metatable 可以 Lua 代码更改,其他只能通过 C 语言修改。即通过
function setmetatable(table: table, metatable?: table) -> table
函数,可以设置一个 table 的元表。- table 参数,要自定义元表的 table 类型的值。
- metatable 要给 table 绑定的元表,如果为 nil 表示清楚元表。
- 返回 table 参数。
通过给 table 自定义 metatable ,可以实现类似 Python 、C++ 的运算符重载特性。
一个复数的例子如下所示:
function NewComplex(r, i)
local o = { r = r, i = i }
function o:print()
print(r .. "+" .. i .. "i")
end
setmetatable(o, {
__add = function(a, b)
return NewComplex(a.r + b.r, a.i + b.i)
end
})
return o
end
local a = NewComplex(1, 2)
local b = NewComplex(3, 4)
local c = a + b
c.print()
-- 4+6i
元表如果包含key __metatable
,则表示:
- 如果某个 table 一旦绑定该元表,则不再允许通过
setmetatable
修改,如果修改,抛出错误。 通过
getmetatable
获取到的值为__metatable
对应的 value。D = {} setmetatable(D, {}) setmetatable(D, { __metatable = "not allow change metatable" }) -- setmetatable(d, {}) -- 将报错 print(getmetatable(D)) -- 获取 metatable 只会返回 __metatable 的值。 -- not allow change metatable
上文介绍了元表 key __add
对应的是 +
运算符,Lua 中的所有运算符都有对应的 key,如 .
对应 __index
,需要注意的是元表的 value 不一定是函数,也可能是其他类型,比如 __index
可以是函数也可以是 table。详细说明参见:官方手册。
最后,元表除了可以实现运算符重载外,还可以对 table 的垃圾回收进行配置,更多参见:官方手册。
env
回顾一下上文的变量章节。 Lua 的变量,分为全局变量和局部变量。其中全局变量一旦被定义,则在后续的所有代码中,都可以通过该变量名直接访问。
实际上,在 Lua 中,全局变量(也包括全局函数标准库)实际上是存储在一个被称为 env 的 table 中的,全局变量名为该表的 key。每个函数都会和一张 env 表绑定。
- Lua 入口脚本可以理解为一个函数,Lua 解释器会在执行脚本前,创建一张 env 表,并和入口脚本绑定,这个 env 表中包含了标准库中的各种函数如
print
等,需要特别说明的是,这个 env 表中包含一个_G
指向 env 表自身。 - 当在入口脚本定义一个函数时,Lua 会该函数的 env 设置为函数定义所在位置的函数绑定 env 表,也就是说当前函数和待调用函数共用一张 env 表。这就是为什么全局函数在函数中定义后,在函数调用结束后,任然可以访问的原因。
- 在函数中,可以通过
getfenv
获取当前函数绑定的 env 表,可以通过setfenv
给当前函数设置一张新的 env 表。
描述调用一个全局函数的过程(以 print("hello")
为例):
- 获取到当前函数绑定的 env 表,假设这个表为
E
,后续操作等价于E.print("hello")
,即触发gettable_event
的行为。 - 查找
E
中是否存在 keyprint
,如果存在则直接返回并调用函数。否则继续执行后续流程。 获取
E
的 metatable 判断是否存在 key__index
,如果存在- 如果是 table 类型,则对该 table 继续触发
gettable_event
行为。 如果是函数类型,则返回
__index(E, 'print')
。function F1() E3 = 3 end function Ef() print("C", C) -- 第一个参数和 getfenv 类似,第二个参数为要设置的表 -- 可以通过 setmetatable 继承上层环境,形成类似链表的结构 local newEnv = {} setmetatable(newEnv, { __index = getfenv(1) }) setfenv(1, newEnv) E1 = 1 -- E1 不会逃逸到全局环境中了 print("C", C) -- 会查找旧的环境 print("getfenv", getfenv) -- 会递归的查找 _G F1() -- F1 定义在顶层,所以 env 仍然是全局 env,所以在外层仍然存在 function F2() -- 该函数定义在 newEnv ,所以 env 是 newEnv E4 = 4 end F2() -- 环境表不设置 _G 的,则找不到全局变量和函数 setfenv(1, { print = print }) print("C", C) -- 可以看出已经找不到上层定义的 C 函数了。 print("getfenv", getfenv) -- 可以看出已经找不到 getfenv 全局函数了。 E2 = 2 -- E1 不会逃逸到全局环境中了 end Ef() print('E1', E1) print('E2', E2) print('E3', E3) print('E4', E4) -- C nil -- C nil -- getfenv function: builtin#10 -- C nil -- getfenv nil -- E1 nil -- E2 nil -- E3 3 -- E4 nil
- 如果是 table 类型,则对该 table 继续触发
最后,需要特别说明的是:
- 从函数视角看,每个函数都会绑定一个 env 表。
- 函数默认 env 表的确定,发生在函数定义阶段,而非调用阶段。
- 通过
setfenv(f: integer|fun(), table: table) -> function
可以手动设置一个函数的 env 表。第一个参数为要配置的函数,可以函数或者数字,1 表示当前函数,2 表示调用当前函数的函数,以此类推。 - 同样的通过
getfenv(f?: integer|fun()) -> table
可以获取当前函数的 env 表。
模块和包
上文介绍的都是单个 Lua 脚本文件,Lua 也提供了模块和包相关能力,可以通过目录和文件来组织代码。
Lua 模块支持 C 语言编写的动态链接库和 Lua 脚本,本文仅介绍 Lua 脚本。
动态执行代码
再介绍模块和包之前,先介绍 Lua 动态执行代码相关的能力。
Lua 是动态的解释型语言,因此也提供了在运行时执行,内容为 lua 代码的字符串或者文件的能力。
function dofile(filename?: string) -> ...any
将执行 filename 文件的内容,并获取返回值。function load(chunk: string|function, chunkname?: string, mode?: "b"|"bt"|"t", env?: table) -> function?, 2. error_message: string?
加载一个代码块。如果 chunk 是一个字符串,代码块指这个字符串。 如果 chunk 是一个函数, load 不断地调用它获取代码块的片断。 每次对 chunk 的调用都必须返回一个字符串紧紧连接在上次调用的返回串之后。 当返回空串、nil、或是不返回值时,都表示代码块结束。最终,代码不会执行,而是封住到函数里面返回。function loadstring(text: string, chunkname?: string) -> function?, error_message: string?
将字符串作为 lua 代码到一个函数里面。function loadfile(filename?: string, mode?: "b"|"bt"|"t", env?: table) -> function?, error_message: string?
和 load 类似。
模块定义
在 Lua 中,一个模块就是一个 lua 代码文件,可以通过如下方式导出函数或变量:
- 通过
return
导出。 - 全局变量 (不推荐)。
08-module-declare/2.lua
通过 return 导出变量。
local module = {}
-- 导出的函数
function module.PrintModule()
print('my module 2')
end
-- 导出模块
return module
08-module-declare/4_1.lua
通过全局变量导出(方式 1),注意如果没有 package.seeall
,将无法使用标准库相关函数,如 print
。
module("mymod4_1", package.seeall) -- 等价于: 08-module-declare/4_2.lua
function PrintModule()
print('my module 4_1')
end
08-module-declare/4_2.lua
通过全局变量导出(方式 2)。
mymod4_2 = {}
setmetatable(mymod4_2, {__index=getfenv(1)})
setfenv(1, mymod4_2)
function PrintModule()
print('my module 4_2')
end
导入模块
通过 function require(modname: string) -> ...
函数可以导入一个模块。
- 参数为模块名,影响代码文件的搜索过程,参见下文。
返回值为模块代码的
return
语句的内容。local mymod2 = require('08-module-declare.2') mymod2.PrintModule() require('08-module-declare.4_1') mymod4_1.PrintModule() require('08-module-declare.4_2') mymod4_2.PrintModule() -- my module 2 -- my module 4_1 -- my module 4_2
require
的执行过程如下所示:
- 依次调用
package.loaders
数组中的模块加载器函数(声明和require
函数一致),默认情况包含 4 个加载函数。- 第 1 个加载器会从
package.preload
表中查找 - 第 2 个加载器会从从
package.path
中查找对应的 lua 文件。- 如
package.path = ./?.lua;/usr/local/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua
。 - 此时,会将 require 参数的
.
替换为/
然后替换 package.path 参数中的 ?,到对应的文件中去加载模块。
- 如
- 第 3 个加载器会从
package.cpath
中查找对应的 C 动态库。并调用luaopen_
+ 模块名最后一个 - 的后面的字符串并将 . 替换为 _(如a.v1-b.c
->luaopen_b_c
)。 - 第 4 个加载器,如
a.b.c
模块,会搜索a
库,并调用luaopen_a_b_c
函数。
- 第 1 个加载器会从
- 加载完成后,结果记录将到
package.loaded[modname]
中,下次再次require
将直接从这个变量中取。 - 如果 reqire 找不到,将抛出错误。
标准库
标准库也是 Lua 语言的一部分,上文提到的函数实际上都是标准库的一部分,更多关于标准库的详解,参见:官方手册。
高级用法
面相对象风格
Lua 原生没有提供面向对象的能力,但是可以通过元表实现类似的效果,一个示例如下:
-- 通过 table 和 metatable 实现面向对象
-- 类、抽象父类、子类、单继承、实例化。
-- 抽象父类
Shape = { type='none' }
function Shape:new(type) -- 父类构造函数
local o = {type=type}
setmetatable(o, { __index = Shape })
return o
end
function Shape:print() -- 父类方法
print("type: ", self.type , "area:", self:getArea())
end
function Shape:getArea() -- 待子类实现的方法
error('no impl')
end
-- 子类
Rectangle = { length = 0, breadth = 0 }
setmetatable(Rectangle, { __index = Shape }) -- 继承父类
function Rectangle:new(length, breadth) -- 子类构造函数
local o = Shape:new('Rectangle')
o.length = length
o.breadth = breadth
setmetatable(o, { __index = Rectangle })
return o
end
function Rectangle:getArea() -- 子类实现父类方法 getArea
return self.length * self.breadth
end
function Rectangle:diagonal() -- 子类自己的方法
return (self.length^2 + self.breadth^2)^(0.5)
end
function Rectangle:print() -- 子类覆写方法 print
print('length: ', self.length, 'breadth: ', self.breadth)
Shape.print(self) -- 子类调用父类的方法
print('and diagonal: ', self:diagonal())
end
-- 实例化
local r = Rectangle:new(3, 4)
r:print()
推荐的模块写法
定义模块
-- square.lua 长方形模块
local _M = {} -- 局部的变量
_M._VERSION = '1.0' -- 模块版本
local mt = { __index = _M }
function _M.new(self, width, height)
return setmetatable({ width=width, height=height }, mt)
end
function _M.get_square(self)
return self.width * self.height
end
function _M.get_circumference(self)
return (self.width + self.height) * 2
end
return _M
导入模块
local square = require "square"
local s1 = square:new(1, 2)
print(s1:get_square()) --output: 2
print(s1:get_circumference()) --output: 6
更多参见: