Lua 基础知识手册
封面来源:由博主个人绘制,如需使用请联系博主。
参考链接:菜鸟教程 Lua 教程
本文所用代码仓库:lua-study
本文目标用户:有一门或多门语言基础的人群,其中以 C++ 为首,Java 和 JavaScript 次之。
1. Lua 的简介与安装
1.1 Lua 的简介
什么是 Lua?
Lua[1] 是一种轻量小巧的 脚本 语言。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua 由标准 C 语言编写而成并以源代码形式开放,几乎在所有操作系统和平台上都可以编译、运行。
Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合作为开发独立应用程序的语言。
Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。
Lua 能干什么?
1、游戏开发
2、独立应用脚本
3、Web 应用脚本
4、扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
5、安全系统,如入侵检测系统
1.2 Lua 的安装
Windows 上的安装
参考链接:windows下安装lua环境
1、在自己计划安装 Lua 的位置创建名为 gcc-lua-install
的文件夹,此后的相关文件都放在这个文件夹下。
2、前往 TDM-GCC 的下载地址,下载它到 gcc-lua-install
文件夹下:
3、前往 Lua 的下载地址,将 Lua 资源下载到 gcc-lua-install
文件夹下:
比如,我现在 gcc-lua-install
文件夹下的内容是这样的:
1 | D:\environment\gcc-lua-install\tdm-gcc-10.3.0.exe |
4、在 gcc-lua-install
文件夹下新增文件夹 tdm-gcc
,将其作为 TDM-GCC 的安装路径。然后双击刚刚下载的 tdm-gcc-10.3.0.exe
文件进行安装:
5、点击上述图片中的 Create
,进入以下画面:
6、使用默认版本,点击 Next
选择前面新建的 tdm-gcc
作为其安装路径,并进入以下画面:
7、一路 Next
,直到 TDM-GCC 安装完毕。
8、把 lua-5.4.4.tar.gz
解压到 D:\environment\gcc-lua-install\lua-5.4.4 文件夹。
9、在最开始新建的 gcc-lua-install
文件夹下创建 build.txt
文件,并以下内容复制到文件中,保存后,修改文件拓展名为 .bat
:
1 | @echo off |
如果下载的 Lua 版本不是 5.4.4
,请修改上述第 8 行;同时请注意第 15 行和第 19 行的配置,如果完全按照本文安装步骤进行则无需关注,只关注版本即可。
10、当前 gcc-lua-install
文件夹下的内容如下:
1 | D:\environment\gcc-lua-install\lua-5.4.4 |
11、双击运行 build.bat
文件,运行成功后,会在 gcc-lua-install
文件夹下生成 lua
文件夹,lua
文件夹中的内容如下:
1 | D:\environment\gcc-lua-install\lua\bin |
12、将 D:\environment\gcc-lua-install\lua\bin
加入到环境变量 Path
中,然后在 cmd 中运行 lua -v
,如果出现类似如下显示,证明 Lua 环境已经安装成功:
1.3 配置编码环境
计划在 Visual Studio Code (即:VS Code)上进行 Lua 代码的编写,需要安装以下三个插件:
1、Lua(Lua Language Server coded by Lua)- sumneko
2、Lua Debug(Visual Studio Code debugger extension for Lua)- actboy168
3、Code Runner - Jun Han
前两者作为 VS Code 中编写 Lua 的支持,最后一个插件用于运行 Lua 代码。
终端输出中文乱码
参考链接:永久解决VS Code终端中文乱码问题
使用 Code Runner 插件运行 Lua 时,终端上的中文字符可能会乱码。如果出现了中文乱码,在 VS Code 的 settings.json 文件中增加以下配置:
1 | "terminal.integrated.profiles.windows": { |
找不到 settings.json 文件?可以点击【设置】页面右上角的转换按钮打开 settings.json 文件:
更多内容可以参考给出的参考链接指向的文章。
2. Lua 基础语法
2.1 Hello World
1 | print("hello world") |
使用 Lua 输出字符串字面量时,可以不用 ()
包裹。
2.2 标识符与全局变量
Lua 的标识符
Lua 标示符用于定义一个变量或函数来获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上 0 个或多个字母,下划线,数字(0 到 9)。
最好 不要使用下划线加大写字母的标示符,因为 Lua 的保留字也是这样的。
Lua 不允许使用特殊字符如 @, $, 和 % 来定义标示符,这与 Java 中的标识符有一定的区别。
Lua 是一个区分大小写的编程语言。
当然,Lua 中也有保留关键词。保留关键字不能作为常量或变量或其他用户自定义标示符。比如:if
、else
等等。
全局变量
在默认情况下,变量总是认为是全局的。
全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是 nil
。
1 | print(b) -- nil |
如果你想删除一个全局变量,只需要将变量赋值为 nil
。也就是说,当且仅当一个变量不等于 nil
时,这个变量即存在。
在 Lua 中,所有的全局变量都被存放在一个大 table 中,这个 table 名为:_G
。
1 | A = 123 |
全局变量与局部变量
全局变量:全局变量在代码运行周期从头到尾,都不会被销毁,而且随处都可调用。当代码量增加,大量新建全局变量会导致内存激增,因此需要一种可以临时使用、并且可以自动销毁释放内存资源的变量。
局部变量:使用 local
创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。
1 | A = 1 -- 全局变量 |
单行注释与多行注释
1 | -- 单行注释 |
1、多行注释推荐使用 --[=[多行注释]=]
,这样可以避免遇到 table[table[idx]]
时就将多行注释结束。
2、多行注释加 -
可以取消注释中间代码以继续运行,单行注释没有此功能(相当于多行注释变成单行注释)。
2.3 数据类型
Lua 是动态类型语言,定义变量不需要定义类型,直接为变量赋值即可。
Lua 中有 8 个基本类型分别为:nil
、boolean
、number
、string
、userdata
、function
、thread
和 table
。
可以使用 type()
函数来获取给定变量或者值的类型,这个函数的返回值类型是字符串:
1 | print(type("Hello world")) --> string |
nil
nil
类型表示一种没有任何有效值,比如输出一个没有赋值的变量,就会输出 nil
。类似 Java 中的 null
。
如果给全局变量或 table 里的一个变量赋值为 nil
,等同将它们删掉:
1 | tab1 = {k1 = "v1", k2 = "v2", "v3"} |
由于 type()
函数的返回值类型是字符串,因此利用 type()
函数判断一个变量是否是 nil
时,应当添加双引号 ""
:
1 | print(type(X)) -- nil |
boolean
Lua 会将 false 和 nil 看作是 false,其他的都是 true,包括数字 0(这一点与 C 语言有点区别)。
1 | print(type(true)) -- boolean |
number
Lua 默认只有一种 number 类型,即:double(双精度)类型。以下几种写法都会被认定为 number 类型:
1 | print(type(2)) |
string
1、字符串由一对双引号或单引号来表示;
2、可以使用两个方括号来表示字符块;
3、对数字字符串进行运算时,会尝试将这个数字字符串转成一个数字;
4、字符串连接使用 ..
,而不是 +
;
5、使用 #
来计算字符串的长度,放在字符串前面(准确的说,计算的是字符串所占字节数)。
1 | stringBlock = [[ |
table
1、table 的创建是通过“构造表达式”来完成的,最简单构造表达式是 {}
,用来创建一个空表。也可以在表里添加一些数据,直接初始化表;
2、table 其实是一个“关联数组”(associative arrays),数组的索引可以是数字或者是字符串;
3、table 的默认索引从 1 开始,而不是 0;
4、table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil
。
1 | local table1 = {} |
使用索引获取 table 对应的值时,除了可以使用 []
,还可以使用 .
:
1 | local site = {} |
function
在 Lua 中,函数是一等公民。
1 | local function factorial(n) |
2.4 变量与赋值
变量
1、变量在使用前需要进行声明;
2、Lua 中有全局变量、局部变量和表中的域三种类型的变量;
3、Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local
显式声明为局部变量;
4、局部变量的作用域为从声明位置开始到所在语句块结束;
5、变量的默认值均为 nil
。
1 | a = 5 |
赋值
1、Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量;
2、遇到赋值语句 Lua 会先计算右边所有的值然后再执行赋值操作,因此可以很简单地进行变量交换;
3、当变量的个数大于值的个数时,按变量个数补足 nil
;
4、当变量的个数小于值的个数时,多余的值会被忽略;
5、Lua 对多个变量同时赋值,不会进行变量传递,仅做值传递。
1 | local a, b, c = 0, 1 |
Lua 中使用赋值的一些建议:
1、多值赋值经常用来交换变量,或将函数调用返回给变量,比如:
1 | a, b = b, a |
2、尽可能的使用局部变量。这样可以避免命名冲突,同时访问局部变量的速度比全局变量更快。
2.5 循环
Lua 中的循环和 Java 中的循环类似,但在流程控制语句中没有 continue
,取而代之的是 goto
语句。
while
1 | local a = 5 |
数值
for
循环
格式与说明:
1 | for var=exp1,exp2,exp3 do |
var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次“执行体”。exp3 是可选的,如果不指定,默认为 1。
for
的三个表达式在循环开始前一次性求值,以后不再进行求值。
1 | for i = 10, 1, -1 do |
泛型
for
循环(类似 Java 的 ForEach)
1 | local p = {"one", "two", "three"} |
repeat..until
循环 (类似 Java 的 do…while)
1 | local b = 5 |
当然循环之间也可以嵌套,无论是同种循环还是不同种循环。
循环控制语句
break
与 Java 中的使用方式一样,不再赘述。
循环控制语句 goto
语句允许将控制流程无条件地转到被标记的语句处。
1 | local c = 1 |
2.6 流程控制
流程控制就是 if
、else
和 ifelse
构成的一个或多个条件语句来设定(与 Java 一样)。
但在 Lua 中控制结构的表达式并不一定必须是 boolean
类型的值,Lua 认为 false
和 nil
为假,true
和非 nil
为真。
注意:Lua 中的数字 0 也是 true。
1 | -- if |
2.7 函数
Lua 中的函数类似与 Java 中的方法。
Lua 中函数的定义格式:
1 | optional_function_scope function function_name (arg1, arg2, arg3..., argn) |
optional_function_scope
:指定函数是全局函数还是局部函数,未设置该参数默认为全局函数,可以使用关键字 local
设置函数为局部函数。
function_name
:函数名称。
arg1, arg2, arg3..., argn
:函数参数,多个参数以逗号隔开,也可以没有参数。
function_body
:函数体,函数中需要执行的代码语句块。
result_params_comma_separated
:函数返回值,Lua 语言函数可以返回多个值,每个值以逗号隔开。
在 Lua 中可以将一个函数作为另一个函数的参数。Lua 的函数可以接受可变数目的参数,和 C 语言类似,在函数参数列表中使用三点 ...
表示函数有可变的参数。可以通过 select("#",...)
来获取可变参数的数量。函数可以有几个固定参数加上可变参数,但固定参数必须放在变长参数之前。
将一个函数作为另一个函数的参数
1 | local myprint = function (param) |
多返回值
1 | local function twoReturnValue(a, b) |
可变参数
1 | local function average(...) |
获取参数信息
通常在遍历变长参数的时候只需要使用 {…}
,然而变长参数可能会包含一些 nil
,那么就可以用 select
函数来访问变长参数,比如 select('#', …)
或者 select(n, …)
。
1、select('#', …)
返回可变参数的长度。
2、select(n, …)
用于返回从起点 n 开始到结束位置的所有参数列表。
调用 select
时,必须传入一个固定实参 selector
(选择开关)和一系列变长参数。如果 selector
为数字 n,那么 select
返回参数列表中从索引 n
开始到结束位置的所有参数列表,否则只能为字符串 #
,这样 select
就会返回变长参数的总数。
1 | local function func(...) |
2.8 运算符
算术运算符
Lua 支持常见的算术运算符,比如 + - * / %(加、减、乘、除、取余),除此之外还有:
操作符 | 描述 | 实例 |
---|---|---|
^ | 乘幂 | A^2 输出结果 100 |
- | 负号 | -A 输出结果 -10 |
// | 整除运算符(>=lua5.3) | 5//2 输出结果 2 |
1 | print(2^2) -- 4.0 |
关系运算符
Lua 中的关系运算符也与 Java 类似,只不过 不等于 是 ~=
,而不是 !=
。
1 | -- ~= 不等于 |
逻辑运算符
操作符 | 描述 | 实例 |
---|---|---|
and | 逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。 | (A and B) 为 false。 |
or | 逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。 | (A or B) 为 true。 |
not | 逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。 | not(A and B) 为 true。 |
1 | -- not 取反 |
其他运算符
操作符 | 描述 | 备注 |
---|---|---|
… | 连接两个字符串 | 如果连接的某个值是 nil,则会报错 |
# | 一元运算符,返回字符串或表的长度。 | 返回字符串或表的长度 |
针对字符串而言,#
输出的值是字符串所占的字节数。
1 | print("hello" .. "world") |
数字与字符串的转换
使用 ..
连接数字和字符串时,数字会自动转换为字符串;在一个数字字面量后使用 ..
时,必须加上空格以防止被解释错。
1 | print(str .. num) -- hello world123 |
当字符串和数字使用算术操作符连接时,字符串会被转成数字(当然这个字符串必须能转换成数字,否则报错),还可以使用 tonumber()
函数将字符串转换为数字:
1 | local numStr = "456" |
运算符优先级
从高到低的顺序:
1 | ^ |
获取表的长度
1 | local tab3 = {} |
使用 #
运算符计算 table 的长度时,返回值似乎并不是我们所期待的。
table t
的长度被定义成一个整数下标 n
。 它满足 t[n]
不是 nil 而 t[n+1]
为 nil; 此外,如果 t[1]
为 nil ,n
就可能是零。 对于常规的数组,里面从 1 到 n
放着一些非空的值的时候, 它的长度就精确的为 n
,即最后一个值的下标。 如果数组有一个“空洞” (就是说,nil 值被夹在非空值之间), 那么 #t
可能是指向任何一个是 nil 值的前一个位置的下标 (就是说,任何一个nil 值都有可能被当成数组的结束)。
更多内容参考:lua table 长度解析
其他拓展
1、and
和 or
两个运算数是 number
类型时,得到的结果是怎么样的呢?
两个 number 类型的数值进行 and 操作,返回在 and 右边那个数值。进行 or 操作,返回在 or 左边那个数值。因为 and 优先级比 or 高,所以 and 和 or 混合运算会先计算 and 的返回值,再计算 or 的值。
2、Lua 不支持三目运算,但是可以实现三目运算:
1 | local flag = true |
2.9 字符串
在此只介绍字符串的基本操作,关于字符串匹配模式的内容参考:Lua 字符串。
大小写转换
1 | -- 字符串全大写 |
字符串替换
1 | print(string.gsub("m_o_f_a_n", "_", "", 3)) -- mofa_n 3 |
查找子串
1 | print(string.find("m_o_f_a_n", "m_o", 1)) -- 1 3 |
反转字符串
1 | print(string.reverse("mofan")) -- nafom |
字符串格式化
1 | print(string.format("the value is: %d", 212)) |
整型数字与字符串的转换(ASCII)
1 | print(string.char(97, 98, 99, 100)) -- abcd |
获取字符串长度
1 | print(string.len("mofan")) -- 5 |
拷贝字符串
1 | print(string.rep("mofan", 2)) -- mofanmofan |
字符串迭代(正则)
1 | for word in string.gmatch("I am mofan", "%a+") do |
正则查找一次
1 | print(string.match("I am 20 years old", "%d+ %a+")) -- 20 years |
字符串截取
1 | --[[ |
字符串格式化
1 | local string1 = "Lua" |
2.10 数组
数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。
Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。
一维数组
1 | local array = {"My", "name", "is", "mofan"} |
多维数组
1 | array = {} |
访问数组
1 | for row = 1, maxRows do |
2.11 迭代器
迭代器(Iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。
在 Lua 中迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。
先看一个例子:
1 | -- 无状态的迭代器 |
为什么会打印出这样的结果?为什么这里的 for
循环和【2.5 循环】中介绍的 for
循环不太一样,有点像泛型 for
循环,但是 in
后面跟了三个值。
先看下泛型 for
循环的语法格式:
1 | for 变量列表 in 迭代函数, 状态变量, 控制变量 do |
泛型 for
循环的执行过程:
1、首先初始化 in
后面表达式的值,与多值赋值一样,如果表达式返回的结果个数不足三个自动以 nil 补充,多余的被忽略;
2、将 状态常量 和 控制变量 作为参数调用 迭代函数(注意:对于 for
结构来说,状态常量仅仅在初始化时获取他的值并传递给迭代函数);
3、将迭代函数返回的值赋给变量列表;
4、如果返回的第一个值为 nil
循环结束,否则执行循环体;
5、回到第二步再次调用迭代函数。
按照这样的执行过程,上述例子产生那样结果的原因就很简单了。
在【2.5 循环】中的泛型 for
循环中,使用了 Lua 提供的默认迭代函数 ipairs()
:
1 | local array = {"a", "b", "c"} |
查看 ipairs()
函数的说明,知道 ipairs()
函数将返回三个值,分别是:
1、迭代函数;
2、表 t;
3、数字 0
根据泛型 for
循环的执行过程,不难知道 ipairs()
是这样实现的:
1 | function iter (a, i) |
无状态的迭代器
Lua 的迭代器包含以下两种类型:
- 无状态的迭代器
- 多状态的迭代器
无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器 只利用 这两个值可以获取下一个元素。
这种无状态迭代器的典型的简单的例子是 ipairs()
,利用它遍历数组的每一个元素。迭代的状态包括被遍历的表 (循环过程中不会改变的状态常量) 和当前的索引下标(控制变量)。
再比如本节最开始举的例子就是一个无状态迭代器。
多状态的迭代器
很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,使用多状态的迭代器有两种方式:
1、最简单的方法是使用闭包;
2、将所有的状态信息封装到 table
内,将 table
作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table
内,所以迭代函数通常不需要第二个参数。
比如下图所示的多状态迭代器的实现就利用了闭包:
1 | array = {"one", "two"} |
在多状态的迭代器中,迭代的时候每次调用的是闭包函数,迭代函数只是开始的时候调用一次:
1 | local function eleiter(t) |
默认迭代函数
pairs()
与ipairs()
的差异
1、它俩都能遍历 table;
2、ipairs 仅仅遍历值,按照索引(略过非整数的索引)升序遍历,索引中断(包括遇到 nil
)停止遍历;
3、pairs 能遍历集合的所有元素。
1 | local function traversalTableWithIpairs(table) |
2.12 表
利用数据结构 table
可以创建不同的数据类型,如:数组、字典等。table
使用关联型数组,可以用任意类型的值来作数组的索引,但这个值不能是 nil
。table 的 size 不是固定的,可以根据自己需要进行扩容。
table
在 Lua 非常重要,可以通过 table 来实现模块(module)、包(package)和对象(Object)。例如 string.format
表示使用 format
索引来获取 string 这个 table 中对应的值。
可以像下面这样构建一个 table
:
1 | -- 构建一个表 |
当我们为 table
a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil
,b 同样能访问 table
的元素(不会影响 b)。如果没有指定的变量指向 a,Lua 的垃圾回收机制会清理相对应的内存。
1 | mytable = {} |
table 操作
连接:
1 | local numberTable = {"One", "Two", "Three"} |
插入和移除:
1 | numberTable = {"1", "2", "3"} |
排序:
1 | numberTable = {"B", "A", "D", "C"} |
自定义 table 操作
最大值:
1 | local function getMaxInTable(t) |
获取长度: 我们知道使用 #
符号求取 table
的长度时会因为索引中断导致无法正确获取到 table
的长度,为此我们编写一个函数来获取 table
的实际长度。
1 | local function getTableLength(t) |
去重:
1 | local function table_unique(t) |
table 作为函数的入参时,是地址传递而不是值传递
1 | -- table 作为入参是引用传递 |
3. 模块与包
在 Java 中,可以利用包与类对代码进行封装,然后再其他地方调用,利于代码的重用,以降低代码耦合度。
Lua 中的模块也有类似这样的功能(从 Lua 5.1 开始,Lua 加入了标准的模块管理机制)。
Lua 的模块是由变量、函数等已知元素组成的 table
,因此创建一个模块很简单,就是先创建一个 table
,然后把需要导出的常量、函数放入其中,最后返回这个 table
就行。
2.1 定义与加载模块
现有一文件夹名为 Module
,在此文件夹下定义文件 ModuleA.lua
,在这个文件中定义名为 A
的模块:
1 | -- 定义一个名为 A 的模块 |
然后在 Module
文件夹下创建 Module.lua
文件,加载模块 A
:
1 | -- 这样导入会报错 |
2.2 多次加载统一模块
在 Module
文件夹下创建 ModuleB.lua
文件,并定义名为 B
的模块:
1 | B = {} |
B
模块中存在 IIFE,那我们在 Module.lua
中加载 B
模块时,它会执行几次呢?
1 | local mb = require "Module.ModuleB" -- 导入就会执行一次 |
2.3 dofile 与 loadfile
使用 require
加载模块时,会在加载时就执行一次,如果使用 require
对同一模块多次加载,只会在第一次加载时执行一次。
每次加载模块时都执行一次
那么有没有什么方式使得每次加载时都执行一次呢?使用 dofile
加载文件即可。
在 Module
文件夹下创建 ModuleC.lua
文件,并定义名为 C
的模块:
1 | C = {} |
然后在 Module.lua
中利用 dofile
每次加载 C
模块时,C
模块中的 IIFE 都会执行一次:
1 | -- 使用 dofile 每次导入都会执行 |
延迟执行
那么有没有什么方式使得每次加载时不执行,在使用时才执行呢?使用 loadfile
加载文件即可。
在 Module
文件夹下创建 ModuleD.lua
文件,并定义名为 D
的模块:
1 | D = {} |
然后在 Module.lua
中利用 loadfile
每次加载 D
模块时,D
模块中的 IIFE 不会立即执行,而是在使用 D
模块时才执行:
1 | -- 使用 loadfile 导入文件时不执行,需要时才执行 |
有关 Lua 中模块的加载机制和 C 包相关内容可以参考菜鸟教程:Lua 模块与包
4. 元表
可以对使用 key 来访问 table 中指定的 value,但是无法对两个 table 进行操作,比如相加。因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。
例如:使用元表可以为 Lua 定义如何计算两个 table。
两个 table 相加时:
1、检查两者之一是否有元表;
2、再检查元表中是否有一个 __add
的字段,如果找到就调用对应的值。
__add
等即时字段,其对应的值(往往是一个函数或是 table)就是“元方法”。
如何设置或获取元表:
1、 setmetatable(table,metatable)
:对指定 table 设置元表。如果元表中存在 __metatable
键值,setmetatable
会失败。
2、 getmetatable(table)
:返回对象的元表。
1 | local mytable = {} -- 普通表 |
4.1 __index 元方法
当通过键来访问 table 的时候,如果这个键没有值,Lua 就会寻找该 table 的 metatable(假定有 metatable)中的 __index
键。
如果 __index
包含一个表 table,Lua 会在那个表 table 中查找相应的键。
如果 __index
包含一个函数,就会调用那个函数,table 和 key 会作为该函数的入参。
1 | mytable = setmetatable({key1 = "value1"}, { |
上述案例可以简写成:
1 | mytable = setmetatable({key1 = "value1"}, {__index = {key2 = "metatableValue"} }) |
还可以套娃式使用:
1 | mytable = setmetatable({key1 = "value1"}, {__index = setmetatable({key2 = "value2"}, {__index = {key3 = "value3"}})}) |
从套娃式使用可以知道 Lua 在查询一个表元素的规则是:
1、在表中查找,如果找到,返回该元素,找不到则继续;
2、判断该表是否有元表,如果没有元表,返回 nil
,有元表则继续。
3、判断元表有没有 __index
方法,如果 __index
方法为 nil
,则返回 nil
;如果 __index
方法是一个表,则重复 1、2、3;如果 __index
方法是一个函数,则返回该函数的返回值。
4.2 __newindex 元方法
当我们对一个 table 中不存在的字段进行赋值时,可以赋值成功,比如:
1 | tab = {} |
那如果想要监控对不存在字段赋值的动作应该怎么做呢?
可以使用 __newindex
元方法。__newindex
元方法用来对表更新,__index
则用来对表访问。
当给表的一个缺少的索引赋值时,Lua 会先查找 __newindex
元方法,如果存在就调用这个函数而不是直接进行赋值操作。
__newindex
的两个规则:
1、如果 __newindex
是一个函数,则在给 table 中不存在的字段赋值时,会调用这个函数,并且赋值不成功。
2、如果 __newindex
是一个 table,则在给 table 中不存在的字段赋值时,会直接给 __newindex
指向的 table 赋值。
__newindex
是一个函数时会传入 3 个参数:table 本身、字段名、想要赋予的值。
更多有关 __newindex
元方法的内容,可以参考:Lua中的元方法__newindex详解
1 | local supperMan = { |
4.3 两表相加
在本节开始我们说过,可以利用 key 去访问 table 中对应的 value 值,但是没有办法对两个 table 进行相加,但如果利用元表中的元方法则可以达到这个目的。这就是运算符重载,是不是在 Java 中没见过这种写法,是的,Java 没有运算符重载。😂
1 | local function table_maxn(t) -- 获取表中最大的键值 |
为实现两表相加使用了 __add
元方法,元表中还有其他的元方法对应不同的运算符:
模式 | 描述 |
---|---|
__add |
对应的运算符 + |
__sub |
对应的运算符 - |
__mul |
对应的运算符 * |
__div |
对应的运算符 / |
__mod |
对应的运算符 % |
__unm |
对应的运算符 - |
__concat |
对应的运算符 .. |
__eq |
对应的运算符 == |
__lt |
对应的运算符 < |
__le |
对应的运算符 <= |
4.4 __call 元方法
当 table 名字作为函数名字的形式被调用时,会调用 __call
元方法。
1 | mytable = setmetatable({10}, { |
4.5 __tostring 元方法
__tostring
元方法用于修改表的输出行为。这与 Java 中重写 toString()
方法类似。
1 | mytable = setmetatable({10, 20, 30}, { |
4.6 利用元方法构造只读表
1 | local function unmodifiable(t) |
5. 协同程序
5.1 基本语法
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又 与其它协同程序共享全局变量 和其它大部分东西。
线程和协同程序区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
基本语法
方法 | 描述 |
---|---|
coroutine.create() | 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用 |
coroutine.resume() | 重启 coroutine,和 create 配合使用 |
coroutine.yield() | 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果 |
coroutine.status() | 查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序 |
coroutine.wrap() | 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复 |
coroutine.running() | 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号 |
5.2 create 与 wrap
create
和 wrap
函数在功能上相似,但是它们的返回值是不同的:
1、create
返回的是一个协同程序,类型为 thread
,需要使用 resume
调用;
2、wrap
返回的是一个普通函数,类型是 function
,和普通函数一样的使用方式,并且不能使用 resume
调用。
1 | local co = coroutine.create( |
5.3 running
running
函数返回当前正在运行的协程加一个布尔量。 如果当前运行的协程是主线程,其为真。
1 | local co2 |
5.4 resume 与 yield
当执行 create
时,相当于在线程中注册了一个事件;
当执行 resume
触发事件时,前一步 create
的函数将被执行;
当遇到 yield
时就会挂起当前线程,等到下次 resume
再次触发事件。
1 | local function foo (a) |
resume
和 yield
的配合强大之处在于,resume
处于主程中,它将外部状态(数据)传入到协同程序内部;而 yield
则将内部的状态(数据)返回到主程中。
resume
的返回值
1 | co = coroutine.create(function (a) |
yield
的返回值
1 | local cor = coroutine.create(function(a) |
5.5 生产者与消费者
1 | local newProductor |
5.6 总结
1、coroutine.creat
方法只要建立了一个协程 ,这个协程的状态默认是 suspend
。使用 resume
方法启动后,会变成 running
状态;遇到 yield
时将状态又变为 suspend
;如果遇到 return
,那么将协程的状态改为 dead
。
2、只要调用 coroutine.resume
方法就会返回一个 boolean
值。
3、coroutine.resume
方法如果调用成功,就会返回 true
;如果有 yield
方法,同时返回 yield
括号里的参数;如果没有 yield
,那么继续运行直到协程结束;直到遇到 return
,将协程的状态改为 dead
,并同时返回 return
的值。
4、coroutine.resume
方法如果调用失败(调用状态为 dead
的协程会导致失败),会返回 false
,并且带上一句 cannot resume dead coroutine
。
6. 文件 I/O
Lua I/O 库用于读取和处理文件,分为简单模式(和 C 语言一样)和完全模式。
1、简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
2、完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法
一般来说,简单模式在做一些简单的文件操作时更合适。
打开文件操作语句如下:
1 | file = io.open (filename [, mode]) |
mode 的值有:
模式 | 描述 |
---|---|
r | 以只读方式打开文件,该文件必须存在。 |
w | 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 |
a | 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF 符保留) |
r+ | 以可读写方式打开文件,该文件必须存在。 |
w+ | 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 |
a+ | 与a类似,但此文件可读可写 |
b | 二进制模式,如果文件是二进制文件,可以加上 b |
+ | 号表示对文件既可以读也可以写 |
本节的所有代码都书写在 IO.lua
文件中,并且只操作 test.txt
文件,文件之间的位置关系如下:
1 | D:. |
Lua I/O 更多详细内容参考:Lua 学习之基础篇六<Lua IO 库>
6.1 使用简单模式操作文件
1 | -- 使用绝对路径,以只读的方式打开文件 |
上述使用的 io.read()
中没有带参数,参数可以是下表中的一个:
模式 | 描述 |
---|---|
*n | 读取一个数字并返回它。例:file.read(“*n”) |
*a | 从当前位置读取整个文件。例:file.read(“*a”) |
*l(默认) | 读取下一行,在文件尾 (EOF) 处返回 nil。例:file.read(“*l”) |
number | 返回一个指定字符个数的字符串,或在 EOF 时返回 nil。例:file.read(5) |
其他的 io 方法有:
- io.tmpfile(): 返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除
- io.type(file): 检测 obj 是否一个可用的文件句柄
- io.flush(): 向文件写入缓冲中的所有数据
- io.lines(optional file name): 返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回
nil
,但不关闭文件
6.2 使用完全模式操作文件
如果需要同时操作多个文件,就需要使用完全模式。
1 | file = io.open("Advanced/testfile/test.txt", "r") |
file:read()
的参数与 io.read()
的参数一致。
完全模式的其他方法有:
-
file:seek(optional whence, optional offset): 设置和获取当前文件位置,成功则返回最终的文件位置(按字节),失败则返回
nil
加错误信息。参数whence
值可以是:- set:从文件头开始
- cur:从当前位置开始(默认)
- end:从文件尾开始
offset 的默认值为 0。不带参数
file:seek()
则返回当前位置,file:seek("set")
则定位到文件头,file:seek("end")
则定位到文件尾并返回文件大小。 -
file:flush(): 向文件写入缓冲中的所有数据
-
io.lines(optional file name): 打开指定的文件 filename 为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回
nil
,并自动关闭文件。
1 | -- file:seek() |
使用 *n
作为 file:read()
方法的参数读取文件中的数字的时候,只有文件中第一个字符是数字(或者空格加数字)的情况下才能读取到并返回这个数字,否则将返回 nil
。
6.3 flush 与 setvbuf
参考链接:Lua io.flush()/file:setvbuf()
以使用 file:setvbuf()
设置缓冲区大小为 16,然后向文件中写入超过 16 个字符的内容为例:
1、未使用 file:flush()
时,不满 16 的字符是不会写入文件的。当程序退出缓冲区时会将不满 16 的字符继续写入,但如果这时程序出现了异常,这部分字符就丢失了。
2、使用 file:flush()
后,可以将缓冲区数据强制写入到文件或内存变量并清空缓冲区。
1 | -- io.flush |
7. 错误处理
7.1 语法错误与运行错误
任何程序语言中,都需要错误处理。错误类型有 语法错误 和 运行错误。
语法错误通常是由于对程序的组件(如运算符、表达式)使用不当引起的,是由程序员自身造成的。语法错误会产生编译错误。
运行错误是程序可以正常执行,代码可以编译成功,但是会输出报错信息。
7.2 assert 与 error
使用
assert
进行错误处理
1 | local function add(a,b) |
assert
首先检查第一个参数,若没问题,assert
不做任何事情;否则,assert
以第二个参数作为错误信息抛出。
使用 error 进行错误处理
1 | local function errorFun(a, b) |
error
函数的语法格式如下:
1 | error (message [, level]) |
error
函数终止正在执行的函数,并返回 message
的内容作为错误信息(error
函数永远都不会返回)
通常情况下,error
会附加一些错误位置的信息到 message
头部。
Level 参数可以指示获得错误的位置:
参数值 | 获得错误的位置 |
---|---|
1(默认) | 调用 error 位置(文件 + 行号) |
2 | 哪个调用 error 函数的函数 |
0 | 不添加错误位置信息 |
7.3 pcall 和 xpcall
Lua 中处理错误,可以使用函数 pcall(protected call)来包装需要执行的代码。
pcall
接收一个函数和要传递给后者的参数,并执行。执行结果有两种:
1、有错误,返回 false
和错误信息;
2、无错误,返回 true
和正确的返回值。
1 | if pcall(add, 1, 2) then |
xpcall
相比 pcall
可以传递一个错误处理函数,错误处理函数的参数为错误信息,且只能有一个参数。
1 | local function myerrorhandler(err) |
8. 面向对象
8.1 简单示例
作为一个 Javaer,没啥比 面向对象更熟悉的了。面向对象的三大基本特征是啥?继承、封装、多态,这没啥可说的。
一个对象由属性和方法组成,在 Lua 中可以使用 table 来描述对象的属性,使用 function 来表示方法。至于继承,可以通过元表 metatable 来模拟,但并不推荐用,只模拟最基本的对象大部分实现够用了。
说到这,多半已经知道 Lua 中如何面向对象了:
1 | -- 模拟调用对象的方法 |
8.2 一个完整的示例
元类:
1 | Shape = {area = 0} |
基础方法 new
:
1 | function Shape:new (o,side) |
基础类方法 printArea
:
1 | function Shape:printArea () |
创建对象并求出面积:
1 | local myshape = Shape:new(nil,10) |
在派生类 Square
中重写基类 Shape
中的方法:
1 | Square = Shape:new() |
在派生类 Rectangle
中重写基类 Shape
中的方法:
1 | Rectangle = Shape:new() |
使用 .
可以调用对象中的方法,使用 :
也可以调用对象中的方法,那它们有什么区别呢?
.
与 :
的区别在于使用 :
定义的函数隐含 self
参数,使用 :
调用函数会自动将调用方法的对象传入调用方法中的 self
参数。
8.3 示例的优化
上述示例中每次 new
新实例的时候都需要将第一个变量的值设为 nil
,很不方便。
可以把变量 o 放在函数里创建,免去麻烦:
1 | --创建一个类,表示四边形 |
9. 其他内容
本节用于补充本文中没提到的知识点,以链接的方式给出:
https://www.lua.org/home.html ↩︎