Lua 速成笔记——1. 基础内容
简单学一下Lua这个有点过时的轻量级脚本语言吧,因为很多工具(nvim、xmake、MySQL等)都采用了Lua脚本提供配置, 而且直到现在,在c++项目使用Lua脚本提供配置也是一个可以考虑的方案。
关于Lua的教程很多都是速成版的,因为内容实在比较简单,比如Learn Lua in Y minutes。
编译安装
Lua是一种开源的脚本语言,完全使用C语言编写,Lua官网直接提供了源码,
在Linux系统中的下载和源码编译流程如下 1
2
3
4curl -L -R -O https://www.lua.org/ftp/lua-5.4.7.tar.gz
tar zxf lua-5.4.7.tar.gz
cd lua-5.4.7
make all test
编译命令非常简单,编译完成后可以得到两个产物:lua
和luac
,两者仍然存放在src/
文件夹中,将其移动到其它位置,
然后将路径添加到环境变量即可。其实编译产物中还有一个Lua库,用于嵌入到C语言项目中,但是暂时不需要。
由于Lua的源码编译过程本身非常简单,我们可以迁移到Windows上进行源码编译,为了方便还可以把Makefile改成CMakeLists.txt, 为了简化直接删除了安装和卸载的部分
1 | cmake_minimum_required(VERSION 3.10) |
基本使用
Lua是一种解释型的脚本语言,解释器lua
既可以交互式使用,也可以执行整个脚本文件(习惯使用.lua
作为文件后缀),
还可以预先使用编译器luac
将脚本编译为不可读的字节码文件(习惯使用.luac
作为文件后缀,但默认名为luac.out
),
解释器可以更高效地执行字节码文件,无需提供原始的脚本文件。
示例如下 1
2
3
4
5
6
7
8
9# (1)
lua
# (2)
lua hello.lua
# (3)
luac -o hello.luac hello.lua
lua hello.luac
注意:
lua
解释器的使用方式优点奇怪,需要使用os.exit()
才能退出解释器,不支持常见的退出操作和清屏指令,甚至不支持上下方向键和其它常见的快捷键;luac
的参数选项不能随便更改顺序,例如-o xxx
放在脚本文件名后面会解析错误。
基本语法
Lua的标识符通常包括大小写字母,下划线和数字,不允许以数字开头,区分大小写。 以下划线加大写字母形式的标识符为Lua的保留标识符,建议不要使用。
Lua 的保留关键词如下 1
2and break do else elseif end false for function if in
nil not or repeat return then true until while goto
Lua支持单行和多行注释,示例如下
1 | --两个减号是单行注释 |
虽然#
不被视作注释,Lua脚本对形如#!/path/to/lua
的脚本开头行也是支持的。
和Python类似,Lua的语句可以但是不要求使用分号;
结尾,但是如果希望把多个语句写在同一行,则必须加上分号。
在Lua中可以使用print()
函数进行输出,在解释器中如果一个表达式的结果没有被变量接收,也会立刻显示出来。
变量
Lua的变量机制大体上和Python类似,变量在使用之前不需要声明,直接赋值即可。
在Lua中访问没有经过初始化的变量甚至不会出错,而是会返回nil
。
Lua和Python在变量部分的最大语法区别是:默认的变量总是认为是全局的,除非在定义时专门加上local
修饰。
涉及作用域的具体规则如下:
- 在默认情况下,所有变量(包括函数)都是全局的,可以在整个程序中访问。
- 使用函数和控制结构可以创建独立的局部作用域。
- 使用
local
关键字声明变量,可以将作用域改为局部的。 - 局部变量会遮蔽同名的全局变量,在读取和修改变量时,优先查找局部变量,找不到时会向外查找全局变量。
- 对于找不到定义的变量,赋值意味着初始化,此时会自动创建全局变量而非局部变量。
和Python类似,Lua自带了一套包括垃圾回收的内存管理机制,我们不需要也没有办法直接管理内存。
基本数据类型
Lua
有八个基本数据类型:nil
、boolean
、number
、string
、userdata
、function
、thread
和 table
, 我们主要关注其中最基础的前四个类型。
使用type(X)
可以获取变量X
的类型字符串,例如
1
2
3
4type(x) -- nil
type(true) -- boolean
type(1.2) -- number
type('a') -- string
nil
类型只有一个同名的值nil
,语义与Python的None
类似,但是在Lua中发挥了更大的作用:
- 输出一个未定义的变量,会显示
nil
; - 对一个变量赋值为
nil
,相对于删除这个变量;
例如判断一个标识符是nil
(注意type()
返回的结果是字符串)
1
type(x) == 'nil' -- true
boolean
类型只有两个可选值:true
(真) 和
false
(假),
需要注意,Lua将其它类型变量转换为boolean
类型时的逻辑为:
nil
被视作false
;- 其它所有都视作
true
,包括数字0
和空字符串''
!
Lua只提供了双精度浮点数这一种数值类型,没有单独提供整数类型。使用tonumber
函数可以尝试将变量转换为数值
1
a = tonumber('123')
Lua的字符串可以由一对双引号或单引号包裹,例如 1
2string1 = "this is string1"
string2 = 'this is string2'
也可以用 2 个方括号 [[]]
来表示多行的字符串。
使用..
而不是通常的加号来拼接字符串 1
a = 'hello,' .. 'world.'
使用#
可以计算字符串的长度(也可以用于计算表的长度)
1
2
3
4print(#'abc') -- 3
s = 'abc'
print(#s) -- 3
字符串对于特殊字符需要使用转义处理,例如回车\n
。
使用tostring
函数可以尝试将变量转换为字符串
1
a = tostring(123)
对一个可以转换为数字的字符串进行算术操作时,Lua会尝试将这个数字字符串转成一个数字(很离谱的语法糖)
1
print('1'+3) -- 4
Lua内置了一个string
库,无需导入即可直接使用,里面提供了很多常用的字符串操作,例如
1
2
3
4
5
6
7
8
9
10
11-- 截取字符串
print(string.sub("Hello Lua", 4, 7)) -- 'lo L'
-- 大小写转换
a = string.lower('Abc') -- 'abc'
b = string.upper('Abc') -- 'ABC'
-- 字符串格式化
d,m,y = 20,7,2024
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))
-- today is: 20/07/2024
基本运算
Lua的赋值语法非常自由,支持多对多的赋值,例如交换两个值
1
a, b = b, a
并且两侧的个数可以不相等,对应的处理逻辑为:
- 如果左侧变量的个数少于右侧值的个数,将靠后的多余的值丢弃;
- 如果左侧变量的个数多于右侧值的个数,将多的变量赋值
nil
。
Lua支持常见的算术运算,包括取余%
和乘方^
,除法/
和整除//
,但是不支持++
和+=
等简化运算。
Lua支持常见的比较运算,值得注意的是不等号是~=
而不是!=
。
Lua
以关键词形式提供逻辑运算,包括and
、or
和not
,它们支持短路运算。
流程控制
直接给几个例子即可
if条件语句 1
2
3
4
5
6
7
8
9local n = 10
if n >= 0 and n < 5 then
print('level 1')
elseif n >= 5 and n < 10 then
print('level 2')
elseif n >= 10 then
print('level 3')
end
for循环语句 1
2
3
4
5
6
7local result = 0
for i=1,100 do
result = result + i
end
print(result)
while循环语句 1
2
3
4
5
6
7
8
9local result = 0
local num = 1
while num <= 100 do
result = result + num
num = num + 1
end
print(result)
在上述结构中支持break
和goto
控制语句。
表 Table
Lua的table
是一种强大且多功能的数据结构,类似于其他编程语言中的数组、字典或结构体。
作为事实上唯一的内置数据结构,Lua
的table
可以被用来表示数组、哈希表、集合和记录(结构体)等。
例如Lua的所有全局变量都存储在名为_G
的table
中。
创建
首先,Lua的表可以当作列表使用,可以直接使用字面量来创建表,{}
代表空表
1
2
3local tb1 = {}
local tb2 = {"apple", "pear", "orange", "grape"}
此时默认的索引从1开始。
其次,Lua的表在本质上更像是字典,可以用键值对的方式来创建表
1
2
3
4
5local tb = {
['key1'] = "value1",
['key2'] = "value2",
['key3'] = "value3"
}
这种创建方式中索引通常是字符串,也支持使用数字作为索引
1
2
3
4local t = {
[1] = 'a',
[2] = 'b'
}
这和前面的数组风格的创建方式实际是等效的,并且这里可以自定义索引的开始,不要求数字是连续的。
两种索引当然可以混合使用 1
2
3
4
5
6local t2 = {
['key1'] = "value1",
['key2'] = "value2",
[1] = "array1",
[2] = "array2"
}
table
支持嵌套定义,例如 1
2
3
4
5
6
7
8
9
10
11
12local t = {
apple = {
price = 7.52,
weight = 2.1,
},
banana = {
price = 8.31,
weight = 1.4,
year = '2018'
},
year = '2019'
}
使用
访问table
时主要通过键来读写对应的值 1
2tb['key1'] = 'value0'
print(tb[key1])
对于以数组形式创建的表,自动使用从1开始的连续整数作为索引
1
2print(tb2[1])
tb2[1] = 'banana'
读取使用不存在的键会返回nil
,对不存在的键进行赋值会直接创建对应的项,使用nil
进行赋值则代表删除对应的项。
如果键是字符串,在不引起歧义的情况下,Lua还提供如下的语法糖,使得读写类似于C语言中结构体的风格
1
2
3
4
5
6local tb = {
['key1'] = '1',
['key2'] = '2'}
print(tb['key1'])
print(tb.key1) -- 简化版
在定义时,如果键是字符串并且不会引起歧义,也有类似的语法糖
1
2
3
4local tb = {
key1 = '1',
key2 = '2'
}
遍历
对于使用从1开始连续整数索引的数组型table
,可以使用ipairs
函数进行遍历
1
2
3
4
5
6
7
8
9
10local array = { "a", "b", "c" }
for i, value in ipairs(array) do
print(i, value)
end
--[[
1 a
2 b
3 c
]]
如果表的索引不满足要求,不适合使用这个函数进行遍历,因为ipairs
的原理就是从1开始不断尝试访问元素,遇到nil
就会停止。
对于更一般的表,可以使用pairs
函数进行遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16local t = {
key1 = "value1",
key2 = "value2",
[1] = "value3",
[2] = "value4"
}
for key, value in pairs(t) do
print(key, value)
end
--[[
1 value3
2 value4
key1 value1
key2 value2
]]
函数
基础
Lua的函数语法和其它语言没什么区别,使用function
关键词,需要使用return
提供返回值,例如
1
2
3local function add(a, b)
return a + b
end
函数默认具有全局作用域,建议加上local
修改为局部作用域。
Lua的函数传参机制和Python是类似的:
- 对基本数据类型(如数值、字符串、布尔值)相当于值传递;
- 对表(table)和函数等复杂数据类型相当于引用传递。
Lua甚至允许函数的实参和形参个数不匹配:
- 如果实参多于形参,靠后的实参被舍弃;
- 如果实参少于形参,不足的形参被赋值为
nil
。
函数可以直接返回多值,不需要额外处理。 1
2
3
4
5
6local function swap(a, b)
return b, a
end
x, y = swap(1, 2)
print(x, y) -- 2 1
和Python一样,Lua把函数视为普通类型的一种,可以直接把函数作为变量传递,不需要使用函数指针或句柄的特殊处理。
1
2
3
4
5
6local function apply(func, a, b)
return func(a, b)
end
local result = apply(add, 2, 3)
print(result) -- 5
Lua对于可变参数使用特殊的...
表示,例如
1
2
3
4
5
6
7
8
9local function sum(...)
local s = 0
for _, v in ipairs{...} do
s = s + v
end
return s
end
print(sum(1, 2, 3, 4)) -- 10
在Lua中定义一个函数实际上等价于把一个匿名函数赋值给一个变量
1
2
3local add = function(a,b)
return a+b
end
例如我们可以将函数赋值给表中的一项 1
2
3
4
5local t = {}
t.add = function(a,b)
return a+b
end
它完全等价于下面的语法 1
2
3
4
5local t = {}
function t.add(a,b)
return a+b
end
这说明在Lua在没有函数标识符的概念,只有统一的变量标识符。
Lua还提供了一个比较离谱的函数调用语法糖:如果只有一个实参,且这个实参是一个字符串字面量或者是table
字面量,则可省略括号,例如
1
2func 'abc' -- func('abc')
func {'a','b'} -- func({'a','b'})
不过相比于MATLAB在无参数调用时可以直接省略括号的离谱语法糖,Lua的这个语法糖还是可以接受的。
进阶
Lua也支持嵌套函数、闭包和高阶函数,基本与Python相同,除了默认作用域的处理:Python的变量默认是局部的,而Lua的变量默认是全局的。
函数嵌套例如 1
2
3
4
5
6
7
8
9local function outer(a)
local function inner(b)
return a + b
end
return inner
end
local add5 = outer(5)
print(add5(3)) -- 8
闭包例如 1
2
3
4
5
6
7
8
9
10
11local function createCounter()
local count = 0
return function()
count = count + 1
return count
end
end
local counter = createCounter()
print(counter()) -- 1
print(counter()) -- 2
高阶函数例如 1
2
3
4
5
6
7
8
9
10
11
12
13local function derivative(f, delta)
delta = delta or 1e-4
return function(x)
return (f(x + delta) - f(x)) / delta
end
end
local function square(x)
return x * x
end
local dsquare = derivative(square)
print(dsquare(5))
I/O
基本I/O
print
函数用于向标准输出打印信息,支持多个参数,并在输出的各项之间自动添加空格。
1
2print("Hello, world!") -- 输出: Hello, world!
print("The answer is", 42) -- 输出: The answer is 42
io.write
用于输出到标准输出,不自动添加空格或换行符。
1
2io.write("Hello, world!") -- 输出: Hello, world!
io.write("The answer is", 42) -- 输出: The answer is42
io.read
:用于从标准输入读取数据。可以指定读取模式,如整行读取、按字符读取等。
1
2
3
4
5local line = io.read() -- 读取一行输入
print("You entered:", line)
local char = io.read(1) -- 读取一个字符
print("You entered:", char)
文件I/O
Lua 提供了对文件进行读写的操作,需要通过 io
库的函数实现,文件操作对各个语言都是类似的,直接提供几个例子即可。
io.open
函数以各种模式打开文件,并返回文件句柄
1
local file = io.open("example.txt", "r")
读取文件内容 1
2
3
4local file = io.open("example.txt", "r")
local content = file:read("*all") -- 读取整个文件
print(content)
file:close() -- 关闭文件
写入文件内容 1
2
3local file = io.open("example.txt", "w")
file:write("Hello, Lua!") -- 写入内容
file:close() -- 关闭文件
错误处理
使用error
函数可以直接生成一个错误 1
error("This is an error message!")
使用assert
函数可以创建断言:
如果条件为假,则生成一个错误。 1
2assert(1 == 1, "Condition failed!")
assert(1 == 2, "Condition failed!") -- 报错: Condition failed!
使用pcall
函数来调用一个函数,可以捕获该函数中出现的任何错误
1
2
3
4
5
6
7
8
9local status, result = pcall(function()
error("An error occurred!")
end)
if status then
print("Function succeeded:", result)
else
print("Function failed:", result)
end
模块与包
Lua 提供了模块化编程的支持,可以将代码组织成独立的模块和包,使用
module
表和 require
函数可以创建和加载模块。
例如我们创建一个名为 mymodule.lua
的文件,内容如下
1
2
3
4
5
6
7local mymodule = {}
function mymodule.sayHello()
print("Hello, from mymodule!")
end
return mymodule
这里将函数放在了表中,并且将这个表返回,这样就得到了一个名为mymodule
的模块。
在其他文件中,可以使用 require
加载并使用模块
1
2local mymodule = require("mymodule")
mymodule.sayHello() -- Hello, from mymodule!
Lua 使用 package.path
来确定搜索模块的路径,我们需要修改
package.path
来添加自定义路径,确保Lua可以找到对应模块
1
2package.path = package.path .. ";./my_modules/?.lua"
local mymodule = require("mymodule")