Lua

  • 动态
  • 协程
  • 单线程
  • 函数是一等公民
  • 闭包
  • 尾调用优化: https://www.lua.org/pil/6.3.html
  • 扩展性
  • 极简主义, 核心足够小, 可用于嵌入式硬件
lua prog.lua # 运行lua脚本
lua -i prog.lua # 运行完脚本内容后进入交互模式
lua -o prog.lc prog.lua # 将lua代码预编译为中间格式
lua prog.lc # 运行预编译的lua脚本

https://luarocks.org/

Lua的全局变量实际上被被存在一个名为 _G 的变量里, 它相当于JavaScript的 =window=/=global=/=globalThis=, 因此 _G._G == _G.

通常不建议修改 _G, 因为会引入魔法.

_G 支持通过元表来改变它的行为, 例如通过设置 __index__newindex 防止自定义全局变量, 但是不建议手动这么做:
Lua提供了一个 strict.lua 模块, 相当于启用严格模式, 其中已经包含了阻止使用全局变量的实现, 只需要在文件头用 require strict 导入它就可以了.

_ENV_G 的局部版本, 类似于Python的 locals().

_ENV_G 一样是可写的, 可以被用来阻止访问 _G.

load和loadfile函数会将当前环境的 _ENV 作为被加载的代码的全局环境.

通常不建议修改 _ENV, 因为会引入魔法.

require的行为类似于Node.js, 同一个文件只会加载一次.

在只有一个参数时, require调用的括号可以省略.

-- 在Lua里, 需要用"."表示文件系统的"/", 模块不需要包含扩展名
require('path/to/module')
-- 在模块里使用return向外导出
--! file: example.lua
return 100
-- 接收模块的返回值
--! file: main.lua
local mod = require('example')
print(mod)
-- 100

加载和执行文件.
如果执行两遍, 则代码会执行两遍.

dofile('file.lua')

加载文件, 返回一个执行文件代码的函数.

函数本身不会抛出错误, 当出现错误时, 只会返回nil和错误信息.

-- file: lib.lua
function echo(x)
print(x)
end
-- file: main.lua
local f = loadfile('lib.lua')
print(echo) -- nil
f()
echo('ok') -- ok

加载字符串, 返回一个执行字符串代码的函数.

函数本身不会抛出错误, 当出现错误时, 只会返回nil和错误信息.

error('invalid') -- 抛出错误
assert(condition, 'invalid') -- 断言
-- 捕捉错误
local result, err = pcall(function ()
...
end)

Lua不支持多线程.
Lua使用非抢占式多线程, 或协作式多线程来形容其协程特性, 这制造了很多迷惑性, 好像它是一种类似goroutine的多线程机制似的.
实际上Lua的协程几乎等价于Node.js基于libuv的异步I/O, 以及它基于generator或CPS变换的语法糖async/await, 只不过有一套专门的API.

Lua的协程有四种状态, 可以用 coroutine.status 检查其状态:

  • 挂起 suspended, 这是协程的起始状态, 中断在函数入口处.
  • 运行 running, 当前协程正在运行中
  • 正常 normal, 当前协程既没有挂起, 也不处于正在运行中, 也没有死亡: 在协程内切换到了另一个协程
  • 死亡 dead, 协程运行完毕
co = coroutine.create(function ()
print('hi')
end)
type(co) -- thread
coroutine.status(co) -- suspended
coroutine.resume(co) -- true
coroutine.status(co) -- dead
coroutine.resume(co) -- false, cannot resume dead coroutine 不会抛出异常

在协程中调用 coroutine.yield() 可以让出计算资源和返回数据(带参数的情况下), 直到再次用 coroutine.resume 恢复它的运行.
coroutine.resume 也可以通过参数向协程的中断处传参.

与create类似, 区别在于wrap返回的是一个函数, 调用此函数就会唤醒线程, 从而免去使用API.

co = coroutine.wrap(function ()
print('hi')
end)
type(co) -- function
co()
co() -- 抛出异常, 协程已经 dead, 不能反复重用.

https://github.com/TypeScriptToLua/TypeScriptToLua

将TypeScript代码转换为Lua.
这个项目提供Lua标准库, Love2D, Defold等常见Lua使用环境的类型定义.

  • 转换过程中有一些地方会与JavaScript的行为不一致.
  • 大量使用declare定义类型.
  • 大量使用JSDoc以解决一些语法层面的问题.

在该项目成为真正的主流选择之前, 使用它显然是有风险的.

https://github.com/teal-language/tl

具有类型的Lua方言.

https://github.com/rxi/classic

OOP库.

https://github.com/kikito/middleclass

OOP库.

https://github.com/kikito/bump.lua

https://vrld.github.io/HardonCollider/index.html

https://github.com/a327ex/windfield

除非使用 local 声明局部作用域, 否则Lua里的变量默认使用全局作用域.

局部作用域的变量可以仅声明而不赋值(未赋值的变量值总是为 nil).

Lua有8种基本类型:

  • nil
  • boolean
  • number: Lua 5.3之前的版本数值都是双精度浮点数, 5.3版本开始分为integer(64位整型)和float(双精度浮点数).
  • string: Lua 5.3开始引入支持UTF-8编码的标准库
  • function
  • thread
  • table
  • userdata: 保存任意C语言数据类型, 内置的一些API就属于这个类型.
type(100) -- number
type('100') -- string

Lua带有类似Perl和JavaScript的隐式类型转换行为, 建议采用显式类型转换.

tostring(100) -- '100'
tonumber('100', 2) -- 4

Lua的用 ""'' 表示字符串, 二者没有语义上的区别.

-- 多行字符串
str = [[
content
]]
-- 多行字符串的符号之间可以插入等号, 只有等号数量匹配时会被当作字符串
str = [===[
content
]===]

Table是Lua里唯一的复合数据结构, 被用来表示其他语言里的多种数据结构.
它本质上是和JavaScript的数组类似的稀疏数组.

尽管Table构造器会从1开始按整数递增的方式生成元素, 但Lua的索引值类型是不受限制的,
因此负数, 浮点数, 甚至其他Table也可以作为索引值, 这一点很像JavaScript的Map类型.

-- Table构造器语法
local fruits = {'apple', 'banana'}
-- or
local fruits = {
[1] = 'apple'
, [2] = 'banana'
}
-- Lua是从1开始计数的
fruits[1] -- 'apple'
fruits[2] -- 'banana'
-- 获取Table的长度
#fruits -- 2
-- 如果给不连续的索引位置赋值, 则Table的长度会扩大到最后一个元素的索引值
-- 如果从Table中不存在的索引取值, 会得到nil
table.insert(fruits, 'pear') -- 在尾部追加元素
table.remove(fruits, 2) -- 移除元素(其他元素会向前移动一位补上空缺)
-- Lua会将用作列表的Table尾部的nil忽略.
local rect = {}
rect['width'] = 100
rect.height = 50
-- or
-- 记录式表构造(由于解释器提前知晓数组元素的数量, 创建更快)
local rect = {
width = 100
, height = 50
}

元表是JavaScript的原型(prototype)和Python对象的魔法方法的结合体, 通过在元表上设置元方法, 可以重载Lua的运算符.

t = {}
print(getmetatable(t)) -- nil, table默认没有元表
metatable = {}
setmetatable(t, metatable) -- 为t设置元表

常见的元方法字段:

  • __add 加法
  • __sub 减法
  • __mul 乘法
  • __div 除法
  • __idiv floor除法
  • __unm 取负值
  • __mod 取模
  • __pow
  • __eq 等于
  • __lt 小于
  • __le 小于等于
  • __tostring 字符串化
  • __pairs 遍历
  • __index 重载getter, 可以是一个方法, 也可以是一个table, 当它是table时就相当于JavaScript的prototype.
  • __newindex 重载setter
  • =__mode== 引用模式
  • __gc 析构器, 必须在调用setmetatable时就带有此字段: 先设置metatable, 后追加的 __gc 字段不会启用析构行为

Lua的table类型的键和值都是强引用, 因此直到table类型失去引用之前, 它里面的(非primitive)元素都不会被垃圾回收.
有时这会成为问题, 所以需要弱引用版本的table, 就像JavaScript里的WeakMap那样.
通过元表可以将一个table类型的对象设置为弱引用表, 一个弱引用表的元素引用都是弱引用, 这些元素从而允许被垃圾回收.

通过设置元表 { __mode = 'k' }, 可以将对象的键变成弱引用.
通过设置元表 { __mode = 'v' }, 可以将对象的值变成弱引用.
通过设置元表 { __mode = 'kv' }, 可以将对象的键和值变成弱引用.

result = { value = 0 }
function result.add(self, value)
self.value = self.value + value
end
result.add(result, 1)
-- 语法糖, 省略self
function result:add(value)
self.value = self.value + value
end
result:add(1)
Account = { balance = 0 } -- 默认值
metatable = { __index = Account }
function Account.new(obj)
-- 如果没有设置balance, 则实例在访问时会通过__index取Account.balance的值, 并且在赋值后会遮蔽掉Account.balance.
obj = obj or {}
setmetatable(obj, metatable)
return obj
end
function Account:deposit(value)
self.balance = self.balance + value
end
a = Account.new{ balance = 0 } -- 可以省略括号
a:deposit(100)
Account = { balance = 0 } -- 默认值
function Account:new(obj)
obj = obj or {}
-- 把Account自己同时当作元表和原型, 这样就不需要额外准备一张元表.
-- 但更重要的是, 其他类继承此类时, self会变成子类, 从而对子类的扩展将不会影响到父类.
self.__index = self
setmetatable(obj, self)
return obj
end
function Account:deposit(value)
self.balance = self.balance + value
end
-- 继承Account的构造函数(new)的返回值
SpecialAccount = Account:new()
-- 添加子类特有的属性
SpecialAccount.limit = 100
-- 由于new只是一个普通方法, 因此它在Account的实例中仍然存在,
-- 此时再调用new方法, 就会因为self指向的是SpecialAccount的值而实现继承.
SpecialAccount:new{balance = 50} -- { limit = 100, balance = 50 }
  • 继承的子类如果覆盖父类的同名方法, 则无法在不具名的情况下调用父类的同名方法.
-- 语法糖
function echo(x)
return x
end
local function echo(x)
return x
end
function mod.draw(x)
return x
end
-- or
echo = function (x)
return x
end
local echo = function (x)
return x
end
mod.draw = function (x)
return x
end
-- Lua函数支持多返回值(根本上是因为Lua支持一次给多个变量赋值的语法)
function echo(a, b)
return a, b
end
local a, b = echo(1, 2) -- a = 1, b = 2
local c = echo(1, 2) -- c = 1
local d, e, f = echo(1, 2) -- d = 1, e = 2, f = nil
-- 变长参数, `...`必须是最后一个参数
function sum(...)
local result = 0
for _, v in ipairs{...} do
result = result + v
end
return result
end
-- Lua会省略列表尾部的nil,
-- 如果想要处理尾部带有nil的传参, 就需要`table.pack(...)`或`select('#', ...)`
-- 面向对象风格的函数调用
obj:method(args)
--[[
comment
--]]
-- 跟字符串一样, 可以在符号之间加入等号, 只有等号数量匹配时会被当作多行注释
--[===[
comment
--]===]
if condition then
-- code
elseif condition then
-- code
else
-- code
end
  • nil
  • false

允许用 break 跳出循环.

for i = 1, 3 do
print(i)
end
-- 1
-- 2
-- 3
-- 带有步进
for i = 1, 3, 3 do
print(i)
end
-- 1
-- 由于第二轮循环开始前检查发现1+3大于3, 故结束循环
-- 遍历Table
fruits = {'apple', 'banana'}
for i = 1, #fruits do
-- code
end
-- ipairs: 为index优化的遍历, 保证顺序
fruits = {'apple', 'banana'}
for i, value in ipairs(fruits) do
print(i, value)
end
-- pairs: 更通用的遍历, 不保证顺序
dict = {
a = 1
, b = 2
}
for key, value in pairs(dict) do
print(key, value)
end

Lua的迭代器只是一个返回下一个迭代值的函数.
该函数由迭代器生成器, 即"返回一个用于迭代的函数"的函数生成.
当迭代器的返回值为nil时意味着迭代结束.

迭代器生成器最多可以返回3个返回值:

  1. 1.
    迭代函数
  2. 2.
    不可变状态, 该值在循环中不变
  3. 3.
    控制变量的初始值, 该值会在循环中递增1

后两个返回值在使用for-in循环时会变成迭代器的参数, 即:

function iter(t, i)
i = i + 1
local v = t[i]
if v then
return i, v
end
end
function ipairs(t)
return iter, t, 0 -- t和0会被for-in循环用作iter函数的参数
end
while condition do
-- code
end

相当于其他语言的do-until, 在repeat后生成的局部变量对until是可见的.

local line
repeat
line = io.read()
until line ~= ''
print(line)
-- 算术运算符
// -- Lua 5.3引入的floor除法, 即"向下取整的除法"
^ -- 幂
% -- 取模
# -- 取字符串的字节数
.. -- 连接字符串
-- 关系运算符
== -- 等于
~= -- 不等于
-- 逻辑运算符
-- Lua的逻辑运算符支持短路.
and -- 逻辑与
or -- 逻辑或
not -- 逻辑非