最近对脚本语言 Lua 很感兴趣,其一是因为它可以很方便地调用 C 语言库,其二是因为它是很多现代化工具的配置语言(比如 NeoVim ),另外 Lua 的源代码不是很长,因此研究学习难度会比其他语言更低。

基本语法

注释

  • 单行注释以双减号开始--
  • 多行注释为:
--[[
comment
--]]

标识符

与C语言规定相同

保留字

Lua 语言中保留字有 22 个:

and break do else elseif end false for function if in local nil not or repeat return then true until while goto

一般约定,以下划线开头连接一串大写字母的名字(比如 VERSION )被保留用于 Lua 内部全局变量。

全局变量

Lua 中变量默认为全局变量,使用全局变量前不需要声明,赋值之后即创建了这个变量。

如果访问了一个不存在的全局变量不会报错,只是它的值是nil。同样地,删除一个变量就可以通过给它赋值为nil来实现。

数据类型

Lua 是动态类型语言,有八个基本类型,而且其中的一些较为特殊:

数据类型 描述
nil 只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean false和true。
number 数字类型
string 字符串
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中表是一种"关联数组",数组的索引可以是数字、字符串或表类型。

我们可以使用 type 函数测试给定变量或者值的类型:

print(type("Hello"))
-- > string
print(type(114514))
-- > number
print(type(print))
-- > function
print(type(true))
-- > boolean
print(type(nil))
-- > nil
print(type(type(nil)))
-- > string
-- type() 函数返回值为字符串
  • number

Lua 默认只有一种 number 类型,为 double 双精度类型。

  • string

可以使用一对双引号或者单引号来表示,也可以使用[[]]来表示一块字符串。

如果对字符串进行数字运算,会尝试将字符串转换为一个数字再进行运算;在 Lua 中,真正的字符串拼接是..

类似于一些脚本语言, Lua 使用#来计算字符串的长度。

  • table

通过构造表达式来创建一个table

-- 创建一个空表
table1 = {}
-- 在创建时进行初始化
table2 = {'a', 'b', 'c'}

向表中添加元素:

a = {}
a["key"] = "value"
a[1] = 1

当表的索引为字符串时,可以使用.来像结构体一样访问:

t = {}
t.key = 'value'
-- 以下两种方法等价
print(t.key)
print(t["key"])

一种遍历表中元素的方法:

for k, v in pairs(a) do
	print(k .. " : " .. v)
end

Lua 中表的默认初始索引为1

  • function

在 Lua 中,function 是第一类值,可以作为对象传递,也可以作为函数返回的值,可以存在变量之中。

function 可以使用匿名函数,也可以使用闭包。

事实上,Lua 全部的函数都是匿名函数,函数名是对其的引用。

  • thread

Lua 中主要的线程是协程。

  • userdata

可以将 C/C++ 中的类型数据存在 Lua 变量中调用。

变量与赋值

Lua 变量有三种类型:全局变量、局部变量、表中的域。变量默认为全局变量

在多值赋值时,出现变量的个数和值的个数不一致情况:

  • 如果值更多,则忽略掉多余的值
  • 如果变量更多,则多出来的变量赋值为 nil

多值赋值经常用来交换变量,或将函数调用返回给变量:

a, b = func()
a, b = b, a

流程控制

循环

  • while 循环
while (test_expression) do
	do_something
end
  • for 循环

var 的值从 exp1 到 exp2 (闭区间),exp3 可选,默认为 1 :

for var=exp1, exp2, exp3 do
	do_something
end

Lua 中也有泛型 for 循环for .. in ...,与 python 类似。

  • repeat … until 循环

类似 C 语言中的 do … while :

repeat
	do_something
until(test_expression)
  • 循环控制语句

break goto 与 C 语言类似

分支判断

if 使用与 shell 中类似:

if ( test_expression1 ) then
	do_something
elseif ( test_expression2 ) then
	do_something
else
	do_something
end

函数

  • optional_func_scope: 默认为全局,使用 local 将函数设置为局部。
  • ret: Lua 语言中函数返回值可以是多个,用逗号隔开。
optional_func_scope function func_name(arg1, arg2, ... , argn)
	function_body
	return ret_val
end

函数可以作为参数传递

Lua 支持可变参数,使用时在参数列表中用...表示:

function average(...)
	local res = 0
	local arg = {...}
	for i, v in ipairs(arg) do
		res = res + v
	end
	res = res / #arg
	return res
end

函数 select("#", …) 可以返回可变参数的数量

select(n, …) 可以返回从起点 n 开始到结束位置的所有参数列表

运算符

与 C 语言基本相同,逻辑运算为and or not,此外还有乘幂运算^和整除运算//,关系运算符中不等于为~=

Lua5.3 之后才支持整除运算

字符串

在 string 模块中有很多方法可以操作字符串,以下函数原型可以望文生义:

string.upper(str)
string.lower(str)
string.gsub(str, find, rep)
-- 返回子串的开始和结束索引,[] 表示可选参数
string.find(str, sub [, beg [, end]])
string.reverse(str)
-- 返回类似 printf 的格式化字符串
string.format(...)
-- 将整型数字转为字符串并连接
string.char(...)
-- 转换字符为整数值,默认第一个字符
string.byte(str [, n])
string.len(str)
-- 返回 str 的 n 个拷贝
string.rep(str, n)
-- 返回一个迭代器函数,init 可选,是搜索的起点
string.gmatch(str, pattern [, init])
-- 字符串截取
string.sub(str, beg [, end])

匹配模式:Lua 中的匹配模式是一种特殊的正则表达式,详见以下链接

http://lua-users.org/wiki/PatternsTutorial

https://riptutorial.com/lua/example/20315/lua-pattern-matching

数组

Lua 中的数组实际上就是下标为整数的 table 。

迭代器

通用形式的 for 通过一个叫作 迭代器 的函数工作。 每次迭代,迭代器函数都会被调用以产生一个新的值, 当这个值为 nil 时,循环停止。 通用形式的 for 循环的语法如下:

for namelist in explist do block end

这样的 for 语句

for var_1, ···, var_n in explist do block end

它等价于这样一段代码:

do
local f, s, var = explist
while true do
	local var_1, ···, var_n = f(s, var)
	if var_1 == nil then
		break
	end
	var = var_1
	block
end

explist 只会被计算一次。它返回三个值,一个迭代器函数 f ,一个状态 s ,一个迭代器的初始值 var 。

参考:http://cloudwu.github.io/lua53doc/manual.html#3.3.5

无状态的迭代器

function _square_iter(ending, current)
	if current < ending then
		current = current + 1
		return current, current * current
	end
end

-- 此处相当于对 _square_iter 进行了一个包装
function square_iter(ending, start)
	return square_iter_, ending, start
end

for i, n in square_iter(3, 0) do
	print(i .. '*' .. i .. '=' .. n)
end

输出:

1*1=1
2*2=4
3*3=9

有状态的迭代器

上面代码的等价形式:

function square_iter(a)
	idx = 0
	len = #a
	-- 迭代函数需要的信息被外层函数传递给了内层函数
	return function()
		idx = idx + 1
		if idx <= len then
			return idx, a[idx] * a[idx]
		end
	end
end

for i, n in square_iter({1, 2, 3}) do
	print(i .. '*' .. i .. '=' .. n)
end

移除一个表的引用可以通过赋值为 nil 来完成,如果没有引用指向该表,该表占用的内存就会被释放。

在 table 模块中有很多方法可以操作表:

-- 列出参数中指定table的数组部分从start位置到end位置的所有元素, 元素间以指定的分隔符(sep)隔开
table.concat (table [, sep [, start [, end]]]):
-- 默认插入到数组尾部
table.insert (table, [pos,] value)
-- 指定table中所有正数key值中最大的key值。如果不存在key值为正数的元素, 则返回0。(Lua5.2之后该方法已经不存在了)
table.maxn (table)
-- 默认删除最后一个元素
table.remove (table [, pos])
-- 默认升序
table.sort(table [, comp])

手动实现 maxn :

function maxn(t)
	if (t) then
		local max_key = t[1]
		for k, v in pairs(t) do
			if k > max_key then
				max_key = k
			end
		end
		return max_key
	end
end

注意:

当我们使用#时,如果索引不是连续的,则不会返回真正的表长度,因此我们可以自己实现一个函数:

function table.len(t)
	local len = 0
	for k, v in pairs(t) do
		len = len + 1
	end
	return len
end

模块与包

Lua 包

Lua 中的模块是一个由变量、函数等已知元素组成的表,只需要最后将这个 table 返回即可创建一个模块。

文件格式:

# filename module.lua
module = {}
module.const_var = 114514

function module.func()
	do_something
end

-- private function
local function local_func()
	do_something
end

return module

可以使用 require 函数来加载模块:

require("module_name")
-- or
require "module_name"

可以给加载的模块定义一个别名,方便调用(本质是又创建了一个引用):

-- 使用局部变量避免污染命名
local m = require("module")

函数 require 有自己的文件路径加载策略,会尝试从 Lua 文件或者 C 程序库中加载模块。

可以搜到的 Lua 文件的路径存放于全局变量 package.path 之中,Lua 解释器启动时会使用系统环境变量 LUA_PATH 来初始化。

可以像这样将路径加入环境变量:

#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"

C 包

C 包在使用之前一定要加载并且链接:

local path = '/usr/local/lua/lib/libluasocket.so'
local f = loadlib(path, 'luaopen_socket')
local path = "/usr/local/lua/lib/libluasocket.so"
local f = assert(loadlib(path, "luaopen_socket"))
f()  -- 真正打开库