封面来源:由博主个人绘制,如需使用请联系博主。
参考链接:菜鸟教程 Lua 教程
本文所用代码仓库:lua-study
本文目标用户:有一门或多门语言基础的人群,其中以 C++ 为首,Java 和 JavaScript 次之。
1. Lua 的简介与安装
1.1 Lua 的简介
什么是 Lua?
Lua 是一种轻量小巧的 脚本 语言。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。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 2 D:\environment\gcc-lua-install\tdm-gcc-10.3.0.exe
D:\environment\gcc-lua-install\lua-5.4.4.tar.gz
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @echo off
:: ========================
:: file build.cmd
:: ========================
setlocal
:: you may change the following variable 's value
:: to suit the downloaded version
set lua_version = 5.4.4
set work_dir =%~dp0
:: Removes trailing backslash
:: to enhance readability in the following steps
set work_dir =%work_dir:~ 0 , -1 %
set lua_install_dir =%work_dir%\ lua
set compiler_bin_dir =%work_dir%\ tdm - gcc \ bin
set lua_build_dir =%work_dir%\ lua -%lua_version%
set path =%compiler_bin_dir%;%path%
cd / D %lua_build_dir%
mingw32 - make PLAT = mingw
echo.
echo **** COMPILATION TERMINATED ****
echo.
echo **** BUILDING BINARY DISTRIBUTION ****
echo.
:: create a clean "binary" installation
mkdir %lua_install_dir%
mkdir %lua_install_dir%\ doc
mkdir %lua_install_dir%\ bin
mkdir %lua_install_dir%\ include
copy %lua_build_dir%\ doc \*.* %lua_install_dir%\ doc \*.*
copy %lua_build_dir%\ src \*.exe %lua_install_dir%\ bin \*.*
copy %lua_build_dir%\ src \*.dll %lua_install_dir%\ bin \*.*
copy %lua_build_dir%\ src \luaconf.h %lua_install_dir%\ include \*.*
copy %lua_build_dir%\ src \lua.h %lua_install_dir%\ include \*.*
copy %lua_build_dir%\ src \lualib.h %lua_install_dir%\ include \*.*
copy %lua_build_dir%\ src \lauxlib.h %lua_install_dir%\ include \*.*
copy %lua_build_dir%\ src \lua.hpp %lua_install_dir%\ include \*.*
echo.
echo **** BINARY DISTRIBUTION BUILT ****
echo.
%lua_install_dir%\ bin \lua. exe - e "print [[Hello!]];print[[Simple Lua test successful!!!]]"
echo.
pause
如果下载的 Lua 版本不是 5.4.4,请修改上述第 8 行;同时请注意第 15 行和第 19 行的配置,如果完全按照本文安装步骤进行则无需关注,只关注版本即可。
10、当前 gcc-lua-install 文件夹下的内容如下:
1 2 3 4 5 D:\environment\gcc-lua-install\lua-5.4.4
D:\environment\gcc-lua-install\tdm-gcc
D:\environment\gcc-lua-install\build.bat
D:\environment\gcc-lua-install\tdm-gcc-10.3.0.exe
D:\environment\gcc-lua-install\lua-5.4.4.tar.gz
11、双击运行 build.bat 文件,运行成功后,会在 gcc-lua-install 文件夹下生成 lua 文件夹,lua 文件夹中的内容如下:
1 2 3 D:\environment\gcc-lua-install\lua\bin
D:\environment\gcc-lua-install\lua\doc
D:\environment\gcc-lua-install\lua\include
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 2 3 4 5 6 7 8 9 10 11 "terminal.integrated.profiles.windows" : {
"Command Prompt" : {
"path" : "C: \\ Windows \\ System32 \\ cmd.exe" ,
"args" : [ "-NoExit" , "/K" , "chcp 65001" ]
},
"PowerShell" : {
"source" : "PowerShell" ,
"args" : [ "-NoExit" , "/C" , "chcp 65001" ]
}
},
"terminal.integrated.defaultProfile.windows" : "Command Prompt"
找不到 settings.json 文件?可以点击【设置】页面右上角的转换按钮打开 settings.json 文件:
更多内容可以参考给出的参考链接指向的文章。
2. Lua 基础语法
2.1 Hello World
1 2 3 print ( "hello world" )
print "hello world"
使用 Lua 输出字符串字面量时,可以不用 () 包裹。
2.2 标识符与全局变量
Lua 的标识符
Lua 标示符用于定义一个变量或函数来获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上 0 个或多个字母,下划线,数字(0 到 9)。
最好 不要使用下划线加大写字母的标示符 ,因为 Lua 的保留字也是这样的。
Lua 不允许使用特殊字符如 @ , $ , 和 % 来定义标示符,这与 Java 中的标识符有一定的区别。
Lua 是一个区分大小写的编程语言。
当然,Lua 中也有保留关键词。保留关键字不能作为常量或变量或其他用户自定义标示符。比如:if、else 等等。
全局变量
在默认情况下,变量总是认为是全局的。
全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是 nil。
1 2 3 4 print ( b ) -- nil
a = 1
print ( "a = " , a ) -- a = 1
如果你想删除一个全局变量,只需要将变量赋值为 nil。也就是说,当且仅当一个变量不等于 nil 时,这个变量即存在。
在 Lua 中,所有的全局变量都被存放在一个大 table 中,这个 table 名为:_G。
1 2 3 4 5 6 7 8 9 A = 123
print ( "使用 _G 输出全局变量 A 的值 " , _G .A) -- 123
_G .B = 234 -- 相当于定义了一个全局变量
print ( "获取使用 _G 定义的全局变量 " , _G .B) -- 234
_G [ "def" ] = 456
print ( "获取全局变量 def 对应的值 " , def , _G [ "def" ], _G .def) -- 456 456 456
-- 甚至还可以
_G . print ( "hello" ) -- 相当于直接调用 print("hello")
_G [ "print" ]( "world" ) -- 相当于直接调用 print("world")
全局变量与局部变量
全局变量:全局变量在代码运行周期从头到尾,都不会被销毁,而且随处都可调用。当代码量增加,大量新建全局变量会导致内存激增,因此需要一种可以临时使用、并且可以自动销毁释放内存资源的变量。
局部变量:使用 local 创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。
1 2 A = 1 -- 全局变量
local a = 1 -- 局部变量
单行注释与多行注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 单行注释
--[[
多行注释
]]
--[=[多行注释]=]
--[[
print("多行注释")
--]]
---[[
print ( "取消多行注释" )
--]]
1、多行注释推荐使用 --[=[多行注释]=],这样可以避免遇到 table[table[idx]] 时就将多行注释结束。
2、多行注释加 - 可以取消注释中间代码以继续运行,单行注释没有此功能(相当于多行注释变成单行注释)。
2.3 数据类型
Lua 是动态类型语言,定义变量不需要定义类型,直接为变量赋值即可。
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
可以使用 type() 函数来获取给定变量或者值的类型,这个函数的返回值类型是字符串:
1 2 3 4 5 6 7 8 print ( type ( "Hello world" )) --> string
print ( type ( 10.4 * 3 )) --> number
print ( type ( print )) --> function
print ( type ( type )) --> function
print ( type ( true )) --> boolean
print ( type ( nil )) --> nil
print ( type ( type ( X ))) --> string
print ( type ( X )) --> nil
nil
nil 类型表示一种没有任何有效值,比如输出一个没有赋值的变量,就会输出 nil。类似 Java 中的 null。
如果给全局变量或 table 里的一个变量赋值为 nil,等同将它们删掉:
1 2 3 4 5 6 7 8 tab1 = { k1 = "v1" , k2 = "v2" , "v3" }
for k , v in pairs ( tab1 ) do
print ( k .. " - " .. v )
end
tab1 .k1 = nil -- 删除 k1
for k , v in pairs ( tab1 ) do
print ( k .. " - " .. v )
end
由于 type() 函数的返回值类型是字符串,因此利用 type() 函数判断一个变量是否是 nil 时,应当添加双引号 "":
1 2 3 4 print ( type ( X )) -- nil
-- type(X) 返回的是 "nil" 字符串
print ( type ( X ) == nil ) -- false
print ( type ( X ) == "nil" ) -- true
boolean
Lua 会将 false 和 nil 看作是 false,其他的都是 true,包括数字 0(这一点与 C 语言有点区别)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 print ( type ( true )) -- boolean
print ( type ( false )) -- boolean
print ( type ( nil )) -- nil
if false or nil then
print ( "至少有一个是 true" )
else
print ( "false 和 nil 都为 false" ) -- 打印这句
end
if 0 then
print ( "数字 0 是 true" ) -- 打印这句
else
print ( "数字 0 为 false" )
end
number
Lua 默认只有一种 number 类型,即:double(双精度)类型。以下几种写法都会被认定为 number 类型:
1 2 3 4 5 6 print ( type ( 2 ))
print ( type ( 2.2 ))
print ( type ( 0.2 ))
print ( type ( 2e + 1 ))
print ( type ( 0.2e-1 ))
print ( type ( 7.8263692594256e-06 ))
string
1、字符串由一对双引号或单引号来表示;
2、可以使用两个方括号来表示字符块;
3、对数字字符串进行运算时,会尝试将这个数字字符串转成一个数字;
4、字符串连接使用 ..,而不是 +;
5、使用 # 来计算字符串的长度,放在字符串前面(准确的说,计算的是字符串所占字节数)。
1 2 3 4 5 6 7 8 9 10 stringBlock = [[
1111
2222
]]
print ( stringBlock )
print ( "1" + 2 ) -- 3
print ( "1" + "3" ) -- 4
print ( "1 + 3" ) -- 1 + 3
print ( "a" .. "b" ) -- ab
print ( 123 .. 456 ) -- 123456
table
1、table 的创建是通过“构造表达式”来完成的,最简单构造表达式是 {},用来创建一个空表。也可以在表里添加一些数据,直接初始化表;
2、table 其实是一个“关联数组”(associative arrays),数组的索引可以是数字或者是字符串;
3、table 的默认索引从 1 开始,而不是 0;
4、table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 local table1 = {}
local table2 = { "apple" , "pear" , "banana" }
table1 [ "key" ] = "value"
local key = 10
table1 [ key ] = 20
table1 [ key ] = table1 [ key ] + 20
for key , value in pairs ( table1 ) do
print ( key .. ": " .. value )
-- 10: 40
-- key: value
end
for k , v in pairs ( table2 ) do
print ( "key: " .. k )
-- key: 1
-- key: 2
-- key: 3
end
local table3 = {}
for i = 1 , 10 do
table3 [ i ] = i
end
table3 [ "key" ] = "val"
print ( table3 [ "key" ]) -- val
print ( table3 [ "none" ]) -- nil
使用索引获取 table 对应的值时,除了可以使用 [],还可以使用 .:
1 2 3 4 local site = {}
site [ "k" ] = "mofan"
print ( site [ "k" ]) -- mofan
print ( site .k) -- mofan
function
在 Lua 中,函数是一等公民。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 local function factorial ( n )
if n == 0 then
return 1
else
return n * factorial ( n - 1 )
end
end
print ( factorial ( 5 )) -- 120
local factorial1 = factorial -- 函数作为局部变量
print ( factorial1 ( 5 )) -- 120
local function testFun ( tab , fun )
for k , v in pairs ( tab ) do
print ( fun ( k , v ))
end
end
local tab = { k1 = "v1" , k2 = "v2" }
testFun ( tab , function ( k , v ) -- 匿名函数作为入参
return k .. "= " .. v
-- k1= v1
-- k2= v2
end )
2.4 变量与赋值
变量
1、变量在使用前需要进行声明;
2、Lua 中有全局变量、局部变量和表中的域三种类型的变量;
3、Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量;
4、局部变量的作用域为从声明位置开始到所在语句块结束;
5、变量的默认值均为 nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 a = 5
local b = 5
local function joke ()
C = 5
local d = 6
end
print ( C , d ) -- nil nil
joke ()
print ( C , d ) -- 5 nil
do
local a = 6
b = 6
print ( a , b ) -- 6 6
end
print ( a , b ) -- 5 6
赋值
1、Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量;
2、遇到赋值语句 Lua 会先计算右边所有的值然后再执行赋值操作,因此可以很简单地进行变量交换;
3、当变量的个数大于值的个数时,按变量个数补足 nil;
4、当变量的个数小于值的个数时,多余的值会被忽略;
5、Lua 对多个变量同时赋值,不会进行变量传递,仅做值传递。
1 2 3 4 5 6 7 8 local a , b , c = 0 , 1
print ( a , b , c ) -- 0 1 nil
a , b = a + 1 , b + 1 , b + 2
print ( a , b ) -- 1 2
a , b , c = 0
print ( a , b , c ) -- 0 nil nil
Lua 中使用赋值的一些建议:
1、多值赋值经常用来交换变量,或将函数调用返回给变量,比如:
1 2 a , b = b , a
a , b = f () -- f()返回两个值,第一个赋给 a,第二个赋给 b
2、尽可能的使用局部变量。这样可以避免命名冲突,同时访问局部变量的速度比全局变量更快。
2.5 循环
Lua 中的循环和 Java 中的循环类似,但在流程控制语句中没有 continue,取而代之的是 goto 语句。
while
1 2 3 4 5 local a = 5
while a < 7 do
print ( a )
a = a + 1
end
数值 for 循环
格式与说明:
1 2 3 for var = exp1 , exp2 , exp3 do
-- <执行体>
end
var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次“执行体”。exp3 是可选的,如果不指定,默认为 1。
for 的三个表达式在循环开始前一次性求值,以后不再进行求值。
1 2 3 4 5 6 7 8 9 10 11 12 for i = 10 , 1 , - 1 do
print ( i )
end
local function f ( x )
print ( "function" )
return x * 2
end
for i = 1 , f ( 5 ) do
print ( i )
end
泛型 for 循环(类似 Java 的 ForEach)
1 2 3 4 local p = { "one" , "two" , "three" }
for index , value in ipairs ( p ) do
print ( index , value )
end
repeat..until 循环 (类似 Java 的 do…while)
1 2 3 4 5 local b = 5
repeat
print ( "b = " .. b )
b = b + 1
until ( b > 7 ) -- 括号可以省略
当然循环之间也可以嵌套,无论是同种循环还是不同种循环。
循环控制语句
break 与 Java 中的使用方式一样,不再赘述。
循环控制语句 goto 语句允许将控制流程无条件地转到被标记的语句处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 local c = 1
:: label :: print ( "--- goto label ---" )
c = c + 1
if c < 3 then
goto label -- c 小于 3 的时候跳转到标签 label
end
--[[
打印结果:
--- goto label ---
--- goto label ---
]]
print ( "---------------- " )
local i = 0
:: s1 :: do
print ( i )
i = i + 1
end
if i > 3 then
os.exit () -- i 大于 3 时退出
end
goto s1
--[[
打印结果:
0
1
2
3
]]
2.6 流程控制
流程控制就是 if、else 和 ifelse 构成的一个或多个条件语句来设定(与 Java 一样)。
但在 Lua 中控制结构的表达式并不一定必须是 boolean 类型的值,Lua 认为 false 和 nil 为假,true 和非 nil 为真。
注意:Lua 中的数字 0 也是 true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- if
if 0 then
print ( "0 也是 true" )
end
print ( "---------------- " )
-- if..else
local a = 100
if a < 100 then
print ( "a 小于 100" )
else
print ( "a 不小于 100" )
end
print ( "---------------- " )
-- if..elseif..else
local b = 100
if b > 1000 then
print ( "b 大于 1000" )
elseif b > 100 then
print ( "b 大于 100" )
else
print ( "b 大于 10" )
end
2.7 函数
Lua 中的函数类似与 Java 中的方法。
Lua 中函数的定义格式:
1 2 3 4 optional_function_scope function function_name ( arg1 , arg2 , arg3 ..., argn )
function_body
return result_params_comma_separated
end
optional_function_scope:指定函数是全局函数还是局部函数,未设置该参数默认为全局函数,可以使用关键字 local 设置函数为局部函数。
function_name:函数名称。
arg1, arg2, arg3..., argn:函数参数,多个参数以逗号隔开,也可以没有参数。
function_body:函数体,函数中需要执行的代码语句块。
result_params_comma_separated:函数返回值,Lua 语言函数可以返回多个值,每个值以逗号隔开。
在 Lua 中可以将一个函数作为另一个函数的参数。Lua 的函数可以接受可变数目的参数,和 C 语言类似,在函数参数列表中使用三点 ... 表示函数有可变的参数。可以通过 select("#",...) 来获取可变参数的数量。函数可以有几个固定参数加上可变参数,但固定参数必须放在变长参数之前。
将一个函数作为另一个函数的参数
1 2 3 4 5 6 7 8 9 10 local myprint = function ( param )
print ( "自定义打印函数 - ## " , param , "##" )
end
local function add ( n1 , n2 , printFunc )
local result = n1 + n2
printFunc ( result )
end
myprint ( 10 )
add ( 1 , 2 , myprint )
多返回值
1 2 3 4 5 6 local function twoReturnValue ( a , b )
return a + 1 , b + 2
end
local r1 , r2 = twoReturnValue ( 1 , 2 )
print ( "return1 = " .. r1 .. ", return2 = " .. r2 )
可变参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 local function average (...)
local result = 0 ;
local arg = { ... }
for index , value in ipairs ( arg ) do
result = result + value
end
-- 两种方式获取参数参数的个数
print ( "传入 " .. # arg .. "个数" )
print ( "传入 " .. select ( "#" , ... ) .. "个数" )
return result / # arg
end
print ( "平均值为 " .. average ( 1 , 2 , 3 , 4 , 5 ))
print ( "---------------- " )
local function fwrite ( fmt , ...)
return io.write ( string.format ( fmt , ... ))
end
fwrite ( "mofan \n " ) -- mofan
fwrite ( "%d%d \n " , 1 , 2 ) -- 12
获取参数信息
通常在遍历变长参数的时候只需要使用 {…},然而变长参数可能会包含一些 nil,那么就可以用 select 函数来访问变长参数,比如 select('#', …) 或者 select(n, …)。
1、select('#', …) 返回可变参数的长度。
2、select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表。
调用 select 时,必须传入一个固定实参 selector(选择开关)和一系列变长参数。如果 selector 为数字 n,那么 select 返回参数列表中从索引 n 开始到结束位置的所有参数列表,否则只能为字符串 #,这样 select 就会返回变长参数的总数。
1 2 3 4 5 6 7 local function func (...)
-- 从第三个位置开始,变量 var 对应右边变量列表的第一个参数
local var = select ( 3 , ... )
print ( var )
-- 打印第三个位置后所有列表参数
print ( select ( 3 , ... ))
end
2.8 运算符
算术运算符
Lua 支持常见的算术运算符,比如 + - * / %(加、减、乘、除、取余),除此之外还有:
操作符
描述
实例
^
乘幂
A^2 输出结果 100
-
负号
-A 输出结果 -10
//
整除运算符(>=lua5.3)
5//2 输出结果 2
1 2 3 4 print ( 2 ^ 2 ) -- 4.0
print (- 2 ) -- -2
print ( 5 // 2 ) -- 2
print ( 5 / 2 ) -- 2.5
关系运算符
Lua 中的关系运算符也与 Java 类似,只不过 不等于 是 ~=,而不是 !=。
1 2 -- ~= 不等于
print ( 1 ~= 2 ) -- true
逻辑运算符
操作符
描述
实例
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 2 -- not 取反
print (not( 1 and nil )) -- true
其他运算符
操作符
描述
备注
…
连接两个字符串
如果连接的某个值是 nil,则会报错
#
一元运算符,返回字符串或表的长度。
返回字符串或表的长度
针对字符串而言,# 输出的值是字符串所占的字节数。
1 2 3 4 5 6 7 8 9 10 11 12 13 print ( "hello" .. "world" )
local str = "hello world"
local tab = { "v1" , "v2" }
print ( "str length is " .. # str ) -- 11
print ( "table size is " .. # tab ) -- 2
local tab2 = { k1 = "v1" , k2 = "v2" }
print ( tab2 .k1) -- v1
print ( "tab2 size is " .. # tab2 ) -- 0
-- 针对字符串而言,# 输出的值是字符串所占的字节数
print (# "Hello World" ) -- 11
print (# "你好世界" ) -- 12
print (# "默烦" ) -- 6
数字与字符串的转换
使用 .. 连接数字和字符串时,数字会自动转换为字符串;在一个数字字面量后使用 .. 时,必须加上空格以防止被解释错。
1 2 print ( str .. num ) -- hello world123
print ( 10 .. 20 ) -- 1020
当字符串和数字使用算术操作符连接时,字符串会被转成数字(当然这个字符串必须能转换成数字,否则报错),还可以使用 tonumber() 函数将字符串转换为数字:
1 2 3 local numStr = "456"
print ( "字符串转数字 " , ( tonumber ( numStr ) + num )) -- 579
print ( "字符串转数字 " , ( num + numStr )) -- 579
运算符优先级
从高到低的顺序:
1 2 3 4 5 6 7 8 ^
not - (unary)
* / %
+ -
..
< > <= >= ~= ==
and
or
获取表的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 local tab3 = {}
tab3 [ 1 ] = "1"
tab3 [ 2 ] = "2"
tab3 [ 4 ] = "4"
print ( "tab3 的长度 " , # tab3 ) -- 4
local tab4 = {}
tab4 [ 1 ]= "1"
tab4 [ 2 ]= "2"
tab4 [ 5 ]= "5"
print ( "tab4 的长度 " , # tab4 ) -- 2
local tab5 = { 1 , 2 , nil , nil , 5 }
print ( "tab5 的长度 " , # tab5 ) -- 5
tab5 [ 6 ] = 1
print ( "tab5 的长度 " , # tab5 ) -- 2
使用 # 运算符计算 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 2 local flag = true
print ( flag and "mofan" or "默烦" ) -- mofan
2.9 字符串
在此只介绍字符串的基本操作,关于字符串匹配模式的内容参考:Lua 字符串 。
大小写转换
1 2 3 4 5 -- 字符串全大写
print ( string.upper ( "mofan" ))
-- 字符串全小写
print ( string.lower ( "MOFAN" ))
字符串替换
1 2 print ( string.gsub ( "m_o_f_a_n" , "_" , "" , 3 )) -- mofa_n 3
print ( string.gsub ( "m_o_f_a_n" , "_" , "" )) -- mofan 4
查找子串
1 2 3 print ( string.find ( "m_o_f_a_n" , "m_o" , 1 )) -- 1 3
print ( string.find ( "m_o_f_a_n" , "_m_o" , 1 )) -- nil
print ( string.find ( "m_o_f_a_n" , "_m_o" , 2 )) -- nil
反转字符串
1 print ( string.reverse ( "mofan" )) -- nafom
字符串格式化
1 print ( string.format ( "the value is: %d" , 212 ))
整型数字与字符串的转换(ASCII)
1 2 3 print ( string.char ( 97 , 98 , 99 , 100 )) -- abcd
print ( string.byte ( "abcd" , 4 )) -- 100
print ( string.byte ( "abcd" )) -- 97
获取字符串长度
1 print ( string.len ( "mofan" )) -- 5
拷贝字符串
1 2 print ( string.rep ( "mofan" , 2 )) -- mofanmofan
print ( string.rep ( "mofan" , 2 , ", " )) -- mofan, mofan
字符串迭代(正则)
1 2 3 4 5 6 for word in string.gmatch ( "I am mofan" , "%a+" ) do
print ( word )
-- I
-- am
-- mofan
end
正则查找一次
1 2 3 print ( string.match ( "I am 20 years old" , "%d+ %a+" )) -- 20 years
print ( string.format ( "%d, %q" , string.match ( "I am 20 years old" , "(%d+) (%a+)" ))) -- 20, "years"
print ( string.match ( "I am 20 years old" , "%d+ %a+" , 15 )) -- nil
字符串截取
1 2 3 4 5 6 7 8 9 10 11 12 13 --[[
string.sub(s, i [, j])
s:要截取的字符串。
i:截取开始位置。
j:截取结束位置,默认为 -1,最后一个字符。
]]
print ( string.sub ( "mofan" , 1 , 2 )) -- mo
print ( string.sub ( "mofan" , 5 , 7 )) -- n
print ( string.sub ( "mofan" , 1 , - 2 )) -- mofa
print ( string.sub ( "mofan" , 6 )) -- 空字符串
print ( string.sub ( "mofan" , 100 )) -- 空字符串
print ( string.sub ( "mofan" , - 5 )) -- mofan
print ( string.sub ( "mofan" , - 100 )) -- mofan
字符串格式化
1 2 3 4 5 6 7 8 9 10 11 local string1 = "Lua"
local string2 = "Tutorial"
local number1 = 10
local number2 = 20
-- 基本字符串格式化
print ( string.format ( "基本格式化 %s %s" , string1 , string2 )) -- 基本格式化 Lua Tutorial
-- 日期格式化
local date = 2 ; local month = 1 ; local year = 2014
print ( string.format ( "日期格式化 %02d/%02d/%03d" , date , month , year )) -- 日期格式化 02/01/2014
-- 十进制格式化
print ( string.format ( "%.4f" , 1 / 3 )) -- 0.3333
2.10 数组
数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。
Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。
一维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 local array = { "My" , "name" , "is" , "mofan" }
for i = 0 , 2 do
print ( array [ i ])
-- nil
-- My
-- name
end
array = {}
for i = - 2 , 2 do
array [ i ] = i * 2
end
for i = - 2 , 2 do
print ( array [ i ])
-- -4
-- -2
-- 0
-- 2
-- 4
end
for i = 1 , 5 do
print ( array [ i ])
-- 2
-- 4
-- nil
-- nil
-- nil
end
多维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 array = {}
for i = 1 , 3 do
array [ i ] = {}
for j = 1 , 3 do
array [ i ][ j ] = i * j
end
end
for i = 1 , 3 do
for j = 1 , 3 do
print ( array [ i ][ j ])
end
end
print ( "---------------- " )
array = {}
local maxRows = 3
local maxColumns = 3
for row = 1 , maxRows do
for col = 1 , maxColumns do
array [ row * maxColumns + col ] = row * col
end
end
访问数组
1 2 3 4 5 for row = 1 , maxRows do
for col = 1 , maxColumns do
print ( array [ row * maxColumns + col ])
end
end
2.11 迭代器
迭代器(Iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。
在 Lua 中迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。
先看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 无状态的迭代器
local function square ( iteratorMaxCount , currentNumber )
if currentNumber < iteratorMaxCount
then
currentNumber = currentNumber + 1
return currentNumber , currentNumber * currentNumber
end
end
for i , n in square , 3 , 0
do
print ( i , n )
-- 1 1
-- 2 4
-- 3 9
end
为什么会打印出这样的结果?为什么这里的 for 循环和【2.5 循环】中介绍的 for 循环不太一样,有点像泛型 for 循环,但是 in 后面跟了三个值。
先看下泛型 for 循环的语法格式:
1 2 3 for 变量列表 in 迭代函数, 状态变量, 控制变量 do
循环体
end
泛型 for 循环的执行过程:
1、首先初始化 in 后面表达式的值,与多值赋值一样,如果表达式返回的结果个数不足三个自动以 nil 补充,多余的被忽略;
2、将 状态常量 和 控制变量 作为参数调用 迭代函数 (注意:对于 for 结构来说,状态常量仅仅在初始化时获取他的值并传递给迭代函数 );
3、将迭代函数返回的值赋给变量列表;
4、如果返回的第一个值为 nil 循环结束,否则执行循环体;
5、回到第二步再次调用迭代函数。
按照这样的执行过程,上述例子产生那样结果的原因就很简单了。
在【2.5 循环】中的泛型 for 循环中,使用了 Lua 提供的默认迭代函数 ipairs():
1 2 3 4 local array = { "a" , "b" , "c" }
for i , v in ipairs ( array ) do
print ( i .. ": " .. v )
end
查看 ipairs() 函数的说明,知道 ipairs() 函数将返回三个值,分别是:
1、迭代函数;
2、表 t;
3、数字 0
根据泛型 for 循环的执行过程,不难知道 ipairs() 是这样实现的:
1 2 3 4 5 6 7 8 9 10 11 function iter ( a , i )
i = i + 1
local v = a [ i ]
if v then
return i , v
end
end
function ipairs ( a )
return iter , a , 0
end
无状态的迭代器
Lua 的迭代器包含以下两种类型:
无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器 只利用 这两个值可以获取下一个元素。
这种无状态迭代器的典型的简单的例子是 ipairs(),利用它遍历数组的每一个元素。迭代的状态包括被遍历的表 (循环过程中不会改变的状态常量) 和当前的索引下标(控制变量)。
再比如本节最开始举的例子就是一个无状态迭代器。
多状态的迭代器
很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,使用多状态的迭代器有两种方式:
1、最简单的方法是使用闭包;
2、将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。
比如下图所示的多状态迭代器的实现就利用了闭包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 array = { "one" , "two" }
local function elementIterator ( collection )
local index = 0
local count = # collection
-- 闭包函数
return function ()
index = index + 1
if index <= count
then
-- 返回迭代器的当前元素
return collection [ index ]
end
end
end
for element in elementIterator ( array )
do
print ( element )
end
在多状态的迭代器中,迭代的时候每次调用的是闭包函数,迭代函数只是开始的时候调用一次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 local function eleiter ( t )
local index = 0
print ( 'in eleiter function' ) --> 每次调用迭代函数都说一句:in eleiter function
return function ()
print ( 'I am here.' ) --> 每次调用闭包函数都说一句:I am here
index = index + 1
return t [ index ]
end
end
local t = { 'one' , 'two' }
for ele in eleiter ( t ) do
print ( ele )
end
默认迭代函数 pairs() 与 ipairs() 的差异
1、它俩都能遍历 table;
2、ipairs 仅仅遍历值,按照索引(略过非整数的索引)升序遍历,索引中断(包括遇到 nil)停止遍历;
3、pairs 能遍历集合的所有元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 local function traversalTableWithIpairs ( table )
for k , v in ipairs ( table ) do
print ( k , v )
end
print ( "---------------- " )
end
local function traversalTableWithPairs ( table )
for k , v in pairs ( table ) do
print ( k , v )
end
print ( "---------------- " )
end
local table = {
[ 1 ] = "one" ,
[ 4 ] = "four" ,
[ 6 ] = "six"
}
-- 1 one 在 key 为 2 处断开
traversalTableWithIpairs ( table )
table = {
[ 2 ] = "two" ,
[ 4 ] = "four" ,
[ 6 ] = "six"
}
-- 什么都没输出,因为 key 为 1 时,value 为 nil
traversalTableWithIpairs ( table )
-- 输出所有 key value
traversalTableWithPairs ( table )
local anotherTable = {
"alpha" ,
"beta" ,
[ 3 ] = "and" ,
[ 3.5 ] = "or" ,
[ "two" ] = "not"
}
-- 输出前三项,因为其余两项 key 不是整数
traversalTableWithIpairs ( anotherTable )
-- 输出所有 key value
traversalTableWithPairs ( anotherTable )
2.12 表
利用数据结构 table 可以创建不同的数据类型,如:数组、字典等。table 使用关联型数组,可以用任意类型的值来作数组的索引,但这个值不能是 nil。table 的 size 不是固定的,可以根据自己需要进行扩容。
table 在 Lua 非常重要,可以通过 table 来实现模块(module)、包(package)和对象(Object)。例如 string.format 表示使用 format 索引来获取 string 这个 table 中对应的值。
可以像下面这样构建一个 table:
1 2 3 4 5 6 -- 构建一个表
local mytable = {}
mytable [ 1 ] = "One"
mytable = nil -- 删除引用。Lua 的垃圾回收机制会释放内存
当我们为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil,b 同样能访问 table 的元素(不会影响 b)。如果没有指定的变量指向 a,Lua 的垃圾回收机制会清理相对应的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 mytable = {}
print ( "mytable 的类型是 " .. type ( mytable )) -- table
mytable [ 1 ] = "One"
mytable [ "mofan" ] = "修改前"
print ( "索引为 1 的元素是 " .. mytable [ 1 ]) -- One
print ( "索引为 mofan 的元素是 " .. mytable [ "mofan" ]) -- 修改前
-- anotherTable 和 mytable 指向同一个 table
local anotherTable = mytable
print ( "anotherTable 中索引为 1 的元素是 " .. anotherTable [ 1 ]) -- One
print ( "anotherTable 中索引为 mofan 的元素是 " .. anotherTable [ "mofan" ]) -- 修改前
anotherTable [ "mofan" ] = "修改后"
print ( "anotherTable 中索引为 mofan 的元素是 " .. anotherTable [ "mofan" ]) -- 修改后
-- 释放变量
anotherTable = nil
print ( "anotherTable 是 " , anotherTable ) -- nil。使用 .. 连接将报错,因为 .. 是用来连接两个字符串的
-- mytable 不受影响
print ( "mytable 索引为 mofan 的元素是 " .. mytable [ "mofan" ]) -- 修改后
mytable = nil
print ( "mytable 是 " , mytable ) -- nil
table 操作
连接:
1 2 3 4 local numberTable = { "One" , "Two" , "Three" }
print ( "连接 table 中所有元素 " , table.concat ( numberTable )) -- OneTwoThree
print ( "使用指定连接符进行连接 " , table.concat ( numberTable , "," )) -- One,Two,Three
print ( "对指定位置的元素进行连接" , table.concat ( numberTable , "," , 2 , 3 )) -- Two,Three
插入和移除:
1 2 3 4 5 6 7 8 9 10 11 12 numberTable = { "1" , "2" , "3" }
table.insert ( numberTable , "4" ) -- 在末尾插入
print ( "索引为 4 的元素是 " , numberTable [ 4 ]) -- 4
table.insert ( numberTable , 2 , "Two" ) -- 在索引为 2 的位置插入元素
print ( "索引为 1 的元素是 " , numberTable [ 1 ]) -- 1
print ( "索引为 2 的元素是 " , numberTable [ 2 ]) -- Two
print ( "索引为 3 的元素是 " , numberTable [ 3 ]) -- 2
print ( "索引为 5 的元素是 " , numberTable [ 5 ]) -- 4
table.remove ( numberTable , 5 )
print ( "索引为 5 的元素是 " , numberTable [ 5 ]) -- nil
排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 numberTable = { "B" , "A" , "D" , "C" }
print "排序前"
for index , value in ipairs ( numberTable ) do
print ( index , value )
end
print "排序后"
table.sort ( numberTable )
for index , value in ipairs ( numberTable ) do
print ( index , value )
end
print "自定义排序规则"
table.sort ( numberTable , function ( a , b )
return a > b
end )
for index , value in ipairs ( numberTable ) do
print ( index , value ) -- 降序排序
end
自定义 table 操作
最大值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local function getMaxInTable ( t )
local mn = nil
for index , value in ipairs ( t ) do
if mn == nil then
mn = value
end
if mn < value then
mn = value
end
end
return mn
end
local testTab = {[ 1 ] = 1 , [ 2 ] = 2 , [ 3 ] = 3 , [ 4 ] = 6 }
print ( "testTab 中的最大值是 " , getMaxInTable ( testTab )) -- 6
print ( "testTab 的长度为 " , # testTab ) -- 4
获取长度: 我们知道使用 # 符号求取 table 的长度时会因为索引中断导致无法正确获取到 table 的长度,为此我们编写一个函数来获取 table 的实际长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 local function getTableLength ( t )
local length = 0
for index , value in pairs ( t ) do -- 使用 pairs 而不是 ipairs
length = length + 1
end
return length
end
local tab3 = {}
tab3 [ 1 ] = "1"
tab3 [ 2 ] = "2"
tab3 [ 4 ] = "4"
print ( "tab3 的长度 " , getTableLength ( tab3 )) -- 3
local tab4 = {}
tab4 [ 1 ]= "1"
tab4 [ 2 ]= "2"
tab4 [ 5 ]= "5"
print ( "tab4 的长度 " , getTableLength ( tab4 )) -- 3
local tab5 = { 1 , 2 , nil , nil , 5 }
print ( "tab5 的长度 " , getTableLength ( tab5 )) -- 3
tab5 [ 6 ] = 1
print ( "tab5 的长度 " , getTableLength ( tab5 )) -- 4
去重:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local function table_unique ( t )
local check = {};
local n = {};
for key , value in pairs ( t ) do
if not check [ value ] then
n [ key ] = value
check [ value ] = value
end
end
return n
end
local notDistinctTable = { 1 , 2 , 3 , 4 , 20 , 6 , 7 , 7 , 15 , 28 };
for key , value in pairs ( table_unique ( notDistinctTable )) do
print ( "value is " , value )
end
table 作为函数的入参时,是地址传递而不是值传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- table 作为入参是引用传递
local function fn1 ( t )
t [ "1" ] = "3"
end
local t = {[ "1" ] = "1" , [ "2" ] = "2" }
print "调用前"
for key , value in pairs ( t ) do
print ( key , value )
-- 2 2
-- 1 1
end
fn1 ( t )
print "调用后"
for key , value in pairs ( t ) do
print ( key , value )
-- 2 2
-- 1 3
end
3. 模块与包
在 Java 中,可以利用包与类对代码进行封装,然后再其他地方调用,利于代码的重用,以降低代码耦合度。
Lua 中的模块也有类似这样的功能(从 Lua 5.1 开始,Lua 加入了标准的模块管理机制)。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是先创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。
2.1 定义与加载模块
现有一文件夹名为 Module,在此文件夹下定义文件 ModuleA.lua,在这个文件中定义名为 A 的模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 -- 定义一个名为 A 的模块
A = {}
A .constant = "I am a Constant"
function A . funA ()
io.write "funA in the module A \n "
end
-- 不能从外部访问模块里的这个私有函数
local function funB ()
print "I am a private function"
end
-- 可以通过公有函数调用私有函数
function A . funC ()
funB ()
end
return A ;
然后在 Module 文件夹下创建 Module.lua 文件,加载模块 A:
1 2 3 4 5 6 7 8 9 10 11 12 13 -- 这样导入会报错
-- local ma = require(A)
-- 导入 Module 下的 ModuleA Lua 文件
require "Module.ModuleA"
print ( A .constant) -- A 是在 ModuleA.lua 中返回的 table
-- 取一个别名
local ma = require ( "Module.ModuleA" )
ma . funA ();
print ( ma .constant)
ma . funC ()
2.2 多次加载统一模块
在 Module 文件夹下创建 ModuleB.lua 文件,并定义名为 B 的模块:
1 2 3 4 5 6 7 8 B = {}
( function ()
print "---------------- "
print "I am a function in Module B"
end )()
return B
B 模块中存在 IIFE,那我们在 Module.lua 中加载 B 模块时,它会执行几次呢?
1 2 local mb = require "Module.ModuleB" -- 导入就会执行一次
local mbb = require "Module.ModuleB" -- 同一文件导入多次,也只执行一次
2.3 dofile 与 loadfile
使用 require 加载模块时,会在加载时就执行一次,如果使用 require 对同一模块多次加载,只会在第一次加载时执行一次。
每次加载模块时都执行一次
那么有没有什么方式使得每次加载时都执行一次呢?使用 dofile 加载文件即可。
在 Module 文件夹下创建 ModuleC.lua 文件,并定义名为 C 的模块:
1 2 3 4 5 6 7 8 9 10 11 12 C = {}
( function ()
print "---------------- "
print "I am a function in Module C"
end )()
function C . funA ()
print "I am funC in Module C"
end
return C
然后在 Module.lua 中利用 dofile 每次加载 C 模块时,C 模块中的 IIFE 都会执行一次:
1 2 3 4 -- 使用 dofile 每次导入都会执行
local mc = dofile "Module/ModuleC.lua"
mc . funA ()
local mcc = dofile "Module/ModuleC.lua"
延迟执行
那么有没有什么方式使得每次加载时不执行,在使用时才执行呢?使用 loadfile 加载文件即可。
在 Module 文件夹下创建 ModuleD.lua 文件,并定义名为 D 的模块:
1 2 3 4 5 6 7 8 9 10 11 12 D = {}
( function ()
print "---------------- "
print "I am a function in Module D"
end )()
function D . funA ()
print "I am funA in Module D"
end
return D
然后在 Module.lua 中利用 loadfile 每次加载 D 模块时,D 模块中的 IIFE 不会立即执行,而是在使用 D 模块时才执行:
1 2 3 4 5 6 -- 使用 loadfile 导入文件时不执行,需要时才执行
local md = loadfile "Module/ModuleD.lua"
local mdd = loadfile "Module/ModuleD.lua"
md () -- 执行
md (). funA () -- 执行 D 模块中的 funA
有关 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 2 3 4 5 6 7 8 9 10 11 12 13 14 local mytable = {} -- 普通表
local metatable1 = {} -- 元表 1 号
local metatable2 = {} -- 元表 2 号
setmetatable ( mytable , metatable1 ) -- 设置元表
local a = getmetatable ( mytable ) -- 获取元表
print ( a )
mytable .__metatable = nil -- 移除普通表的元表
mytable = setmetatable ({}, metatable2 ) -- 设置元表 2 号
a = getmetatable ( mytable ) -- 获取新设置的元表
print ( a ) -- 比较两次打印结果,结果不一样,证明新设置成功
4.1 __index 元方法
当通过键来访问 table 的时候,如果这个键没有值,Lua 就会寻找该 table 的 metatable(假定有 metatable)中的 __index 键。
如果 __index 包含一个表 table,Lua 会在那个表 table 中查找相应的键。
如果 __index 包含一个函数,就会调用那个函数,table 和 key 会作为该函数的入参。
1 2 3 4 5 6 7 8 9 10 11 12 mytable = setmetatable ({ key1 = "value1" }, {
__index = function ( tab , key )
if key == "key2" then
return "metatableValue"
else
return nil
end
end
})
print ( mytable .key1) -- value1
print ( mytable .key2) -- metatableValue
上述案例可以简写成:
1 2 3 mytable = setmetatable ({ key1 = "value1" }, { __index = { key2 = "metatableValue" } })
print ( mytable .key1) -- value1
print ( mytable .key2) -- metatableValue
还可以套娃式使用:
1 2 3 4 mytable = setmetatable ({ key1 = "value1" }, { __index = setmetatable ({ key2 = "value2" }, { __index = { key3 = "value3" }})})
print ( mytable .key1) -- value1
print ( mytable .key2) -- value2
print ( mytable .key3) -- value3
从套娃式使用可以知道 Lua 在查询一个表元素的规则是:
1、在表中查找,如果找到,返回该元素,找不到则继续;
2、判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
3、判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。
4.2 __newindex 元方法
当我们对一个 table 中不存在的字段进行赋值时,可以赋值成功,比如:
1 2 3 tab = {}
tab .key1 = "value1"
print ( key1 ) -- value1
那如果想要监控对不存在字段赋值的动作应该怎么做呢?
可以使用 __newindex 元方法。__newindex 元方法用来对表更新,__index 则用来对表访问。
当给表的一个缺少的索引赋值时,Lua 会先查找 __newindex 元方法,如果存在就调用这个函数而不是直接进行赋值操作。
__newindex 的两个规则:
1、如果 __newindex 是一个函数,则在给 table 中不存在的字段赋值时,会调用这个函数,并且赋值不成功。
2、如果 __newindex 是一个 table,则在给 table 中不存在的字段赋值时,会直接给 __newindex 指向的 table 赋值。
__newindex 是一个函数时会传入 3 个参数:table 本身、字段名、想要赋予的值。
更多有关 __newindex 元方法的内容,可以参考:Lua中的元方法__newindex详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 local supperMan = {
name = "超人" ,
money = "Incalculable" ,
sayHello = function ()
print "I am Kent"
end
}
local man = { name = "none" }
local newIndexTab = {
__index = supperMan ,
__newindex = function ( tab , key , value )
print ( key .. " 字段不存在,请勿赋值!" )
end
}
setmetatable ( man , newIndexTab )
man . sayHello = function ()
print "Hello everyone"
end
man . sayHello ()
-- sayHello 字段不存在,请勿赋值!(访问 sayHello 字段时,字段不存在,执行 __newindex)
-- I am Kent (访问 sayHello 字段时,字段不存在,也会执行 __index 对应的函数或去其指向的 table 中寻找对应的字段值)
man .name = "mofan"
print ( man .name) -- mofan 字段存在,直接修改,不执行 __newindex
newIndexTab = { name = "new index tab name" }
man = setmetatable ({}, { __newindex = newIndexTab })
print ( "newIndexTab 赋值前 name 的值 " , newIndexTab .name) -- new index tab name
man .name = "mofan"
print ( "newIndexTab 赋值后 name 的值 " , newIndexTab .name) -- mofan
print ( "man 中 name 的值 " , man .name) -- nil
4.3 两表相加
在本节开始我们说过,可以利用 key 去访问 table 中对应的 value 值,但是没有办法对两个 table 进行相加,但如果利用元表中的元方法则可以达到这个目的。这就是运算符重载,是不是在 Java 中没见过这种写法,是的,Java 没有运算符重载。😂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 local function table_maxn ( t ) -- 获取表中最大的键值
local mn = 0
for key , value in pairs ( t ) do
if mn < key then
mn = key
end
end
return mn
end
mytable = setmetatable ({ 1 , 2 , 3 }, {
__add = function ( mytable , newtable ) -- 使用 __add 元方法,重载 `+` 运算符
for i = 1 , table_maxn ( newtable ) do
table.insert ( mytable , table_maxn ( mytable ) + 1 , newtable [ i ])
end
return mytable
end
})
local secondtable = { 4 , 5 , 6 }
mytable = mytable + secondtable
for index , value in ipairs ( mytable ) do
print ( index , value )
end
为实现两表相加使用了 __add 元方法,元表中还有其他的元方法对应不同的运算符:
模式
描述
__add
对应的运算符 +
__sub
对应的运算符 -
__mul
对应的运算符 *
__div
对应的运算符 /
__mod
对应的运算符 %
__unm
对应的运算符 -
__concat
对应的运算符 ..
__eq
对应的运算符 ==
__lt
对应的运算符 <
__le
对应的运算符 <=
4.4 __call 元方法
当 table 名字作为函数名字的形式被调用时,会调用 __call 元方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 mytable = setmetatable ({ 10 }, {
__call = function ( mytable , newtable )
local sum = 0
for i = 1 , table_maxn ( mytable ) do
sum = sum + mytable [ i ]
end
if type ( newtable ) == "table" then
for i = 1 , table_maxn ( newtable ) do
sum = sum + newIndexTab [ i ]
end
elseif type ( newtable ) == "number" then
sum = sum + newtable
end
return sum -- 将两个表的值加在一起
end
})
newIndexTab = { 10 , 20 , 30 }
print ( mytable ( newIndexTab )) -- 70
print ( mytable ( 10 )) -- 20
4.5 __tostring 元方法
__tostring 元方法用于修改表的输出行为。这与 Java 中重写 toString() 方法类似。
1 2 3 4 5 6 7 8 9 10 11 mytable = setmetatable ({ 10 , 20 , 30 }, {
__tostring = function ( tab )
local str = {}
for index , value in ipairs ( tab ) do
table.insert ( str , string.format ( "%s -> %s" , index , value ))
end
return table.concat ( str , " \n " ) -- 必须返回字符串,否则报错
end
})
print ( mytable )
4.6 利用元方法构造只读表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local function unmodifiable ( t )
local proxy = {}
local mt = {
__index = t ,
__newindex = function ( t , k , v )
error "you can't modify this table"
end
}
setmetatable ( proxy , mt )
return proxy
end
mytable = unmodifiable ({ "One" , "Two" , "Three" })
print ( mytable [ 2 ])
mytable [ 4 ] = "Four" -- 报错
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 local co = coroutine.create (
function ( i )
print ( i );
end
)
print ( coroutine.status ( co )) -- suspended
print ( "type of co is " , type ( co )) -- thread
coroutine.resume ( co , 1 ) -- 1
print ( coroutine.status ( co )) -- dead
print ( "---------------- " )
co = coroutine.wrap (
function ( i )
print ( i );
end
)
co ( 1 )
print ( "type of co is " , type ( co )) -- function
5.3 running
running 函数返回当前正在运行的协程加一个布尔量。 如果当前运行的协程是主线程,其为真。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 local co2
co2 = coroutine.create (
function ()
for i = 1 , 10 do
print ( i )
if i == 3 then
print ( coroutine.status ( co2 )) -- running
print ( coroutine.running ()) -- thread:XXXXXX false
end
coroutine.yield ()
end
end
)
coroutine.resume ( co2 ) --1
coroutine.resume ( co2 ) --2
coroutine.resume ( co2 ) --3
print ( coroutine.status ( co2 )) -- suspended
print ( coroutine.running ()) -- thread:XXXXXX true
5.4 resume 与 yield
当执行 create 时,相当于在线程中注册了一个事件;
当执行 resume 触发事件时,前一步 create 的函数将被执行;
当遇到 yield 时就会挂起当前线程,等到下次 resume 再次触发事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 local function foo ( a )
print ( "foo 函数输出" , a )
return coroutine.yield ( 2 * a ) -- 返回 2*a 的值
end
co = coroutine.create ( function ( a , b )
print ( "第一次协同程序执行输出" , a , b ) -- co-body 1 10
local r = foo ( a + 1 )
print ( "第二次协同程序执行输出" , r )
local s
r , s = coroutine.yield ( a + b , a - b ) -- a,b的值为第一次调用协同程序时传入
print ( "第三次协同程序执行输出" , r , s )
return b , "结束协同程序" -- b的值为第二次调用协同程序时传入
end )
print ( "main" , coroutine.resume ( co , 1 , 10 )) -- true, 4
print ( "---------------- " )
print ( "main" , coroutine.resume ( co , "r" )) -- true 11 -9
print ( "---------------- " )
print ( "main" , coroutine.resume ( co , "x" , "y" )) -- true 10 end
print ( "---------------- " )
print ( "main" , coroutine.resume ( co , "x" , "y" )) -- cannot resume dead coroutine
print ( "---------------- " )
resume 和 yield 的配合强大之处在于,resume 处于主程中,它将外部状态(数据)传入到协同程序内部;而 yield 则将内部的状态(数据)返回到主程中。
resume 的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 co = coroutine.create ( function ( a )
local r = coroutine.yield ( a + 1 ) -- yield 返回 a + 1 给调用它的 resume 函数,即 2
print ( "r = " , r ) -- r 的值是第二次 resume 传进来的 100
end )
local status , r = coroutine.resume ( co , 1 ) -- resume 返回两个值,一个是自身状态,一个是 yield 的值
print ( status ) -- true
print ( r ) -- 2
status = coroutine.resume ( co , 100 )
print ( status ) -- true
-- resume 调用失败,返回 false,并带上 “cannot resume dead coroutine”。
print ( coroutine.resume ( co , 10 )) -- false cannot resume dead coroutine
-- 调用失败并将其返回值赋值给其他变量
status , r = coroutine.resume ( co , 10 )
print ( status ) -- false
print ( r ) -- cannot resume dead coroutine
yield 的返回值
1 2 3 4 5 6 7 8 9 10 local cor = coroutine.create ( function ( a )
print ( "参数 a 值为 " , a ); -- 1
local b , c = coroutine.yield ( a + 3 ); -- 2 3。这里表示挂起协程,并且将a+1的值进行返回,并且指定下一次唤醒需要 b,c 两个参数。
print ( "参数 b,c值分别为 " , b , c ); -- b c 的值为当次 resume 唤醒协程时指定的值,即 2 3
return b * c ; --协程结束,并且返回 b*c 的值。
end );
print ( "第一次调用:" , coroutine.resume ( cor , 1 )); -- true 4
print ( "第二次调用:" , coroutine.resume ( cor , 2 , 3 )); -- true 6
print ( "第三次调用:" , coroutine.resume ( cor )); -- false cannot resume dead coroutine
5.5 生产者与消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 local newProductor
local function receive ()
local status , value = coroutine.resume ( newProductor )
return value
end
local function send ( x )
coroutine.yield ( x ) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
local function productor ()
local i = 0
while true do
i = i + 1
send ( i ) -- 将生产的物品发送给消费者
end
end
local function consumer ()
while true do
local i = receive () -- 从生产者那里得到物品
print ( i )
end
end
-- 启动程序
newProductor = coroutine.create ( productor )
consumer () -- 从 1 开始打印,每次输出加 1,直到人为终止程序
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 2 3 4 5 6 7 D:.
│
├───Advanced
│ IO.lua
│
└───testfile
test.txt
Lua I/O 更多详细内容参考:Lua 学习之基础篇六<Lua IO 库>
6.1 使用简单模式操作文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 使用绝对路径,以只读的方式打开文件
local file = io.open ( "D: \\ Code \\ Lua \\ Advanced \\ testfile \\ test.txt" , "r" )
-- 设置默认输入的文件(输入指的是输入到内存中)
io.input ( file )
-- 读取文件中的第一行
print ( io.read ())
-- 关闭文件
io.close ( file )
-- 使用相对路径以附加的方式打开文件(其中 Advanced 目录是我使用 VS Code 打开的 Lua 目录下的一个子目录)
file = io.open ( "Advanced/testfile/test.txt" , "a" )
-- 设置默认输出的文件(输出指的是输出到磁盘上)
io.output ( file )
-- 在文件最后一行添加内容(并不是在当前文件的最后一行的下一行添加,而是追加到最后一行末尾)
io.write ( " -- I am mofan" )
io.write ( " \n insert an extra row" ) -- 将内容插入到最后一行的下一行
-- 关闭文件
io.close ( file )
上述使用的 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 2 3 4 5 6 7 8 9 file = io.open ( "Advanced/testfile/test.txt" , "r" )
-- 输出文件第一行
print ( file : read ())
-- 关闭打开的文件
file : close ()
file = io.open ( "Advanced/testfile/test.txt" , "a" )
-- 将内容插入到最后一行的下一行
file : write ( " \n -- the last row" )
file : close ()
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 -- file:seek()
file = io.open ( "Advanced/testfile/test.txt" , "r" )
file : seek ( "end" , - 25 )
print ( file : read ( "*a" ))
file : seek ( "set" )
local num = file : read ( "*n" ) -- 获取数字
if num == nil then
local tab = {}
-- 将文件内容写到 tab 中
for line in io.lines ( "Advanced/testfile/test.txt" ) do
table.insert ( tab , line )
end
file : close ()
file = io.open ( "Advanced/testfile/test.txt" , "w" )
for index , value in ipairs ( tab ) do
-- 在第一行前面增加 123
if index == 1 then
file : write ( "123" .. value .. " \n " )
else
file : write ( value .. " \n " )
end
end
file : close ()
end
file = io.open ( "Advanced/testfile/test.txt" , "r" )
print ( file : read ( "*n" )) -- 123
file : close ()
使用 *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 2 3 4 5 6 7 8 9 -- io.flush
file = io.open ( "Advanced/testfile/test.txt" , "a" )
file : setvbuf ( "full" , 16 )
file : write ( "10 letters \n " )
file : write ( "10 letters \n " )
file : flush () -- 注释前后,控制台效果不一样
-- 暂停程序,模拟出错
os.execute ( "pause" )
file : close ()
7. 错误处理
7.1 语法错误与运行错误
任何程序语言中,都需要错误处理。错误类型有 语法错误 和 运行错误 。
语法错误通常是由于对程序的组件(如运算符、表达式)使用不当引起的,是由程序员自身造成的。语法错误会产生编译错误。
运行错误是程序可以正常执行,代码可以编译成功,但是会输出报错信息。
7.2 assert 与 error
使用 assert 进行错误处理
1 2 3 4 5 6 7 local function add ( a , b )
assert ( type ( a ) == "number" , "a 不是一个数字" )
assert ( type ( b ) == "number" , "b 不是一个数字" )
return a + b
end
-- add(10)
assert 首先检查第一个参数,若没问题,assert 不做任何事情;否则,assert 以第二个参数作为错误信息抛出。
使用 error 进行错误处理
1 2 3 4 5 6 local function errorFun ( a , b )
error ( "error handle" )
return a - b
end
-- print(errorFun(3, 1))
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 2 3 4 5 6 7 8 9 10 11 12 13 if pcall ( add , 1 , 2 ) then
print ( "no error" )
else
print "error"
end
local status , errorInfo = pcall ( errorFun , 3 , 1 )
print ( status ) -- false
print ( errorInfo ) -- 存在错误返回错误信息
local sum
status , sum = pcall ( add , 3 , 4 )
print ( status ) -- true
print ( sum ) -- 没有错误返回正确结果 7
xpcall 相比 pcall 可以传递一个错误处理函数,错误处理函数的参数为错误信息,且只能有一个参数。
1 2 3 4 5 6 7 local function myerrorhandler ( err )
print ( "ERROR:" , err )
end
-- ERROR: xxx error handle
status = xpcall ( errorFun , myerrorhandler , 3 , 1 )
print ( status ) -- false
8. 面向对象
8.1 简单示例
作为一个 Javaer,没啥比 面向对象更熟悉的了。面向对象的三大基本特征是啥?继承、封装、多态,这没啥可说的。
一个对象由属性和方法组成,在 Lua 中可以使用 table 来描述对象的属性,使用 function 来表示方法。至于继承,可以通过元表 metatable 来模拟,但并不推荐用,只模拟最基本的对象大部分实现够用了。
说到这,多半已经知道 Lua 中如何面向对象了:
1 2 3 4 5 6 7 8 9 10 -- 模拟调用对象的方法
Account = { balanve = 0 }
function Account . withdraw ( v )
Account .balanve = Account .balanve - v
end
Account . withdraw ( 10 )
print ( Account .balanve) -- -10
8.2 一个完整的示例
元类:
基础方法 new:
1 2 3 4 5 6 7 8 function Shape : new ( o , side )
o = o or {}
setmetatable ( o , self )
self .__index = self
side = side or 0
self .area = side * side ;
return o
end
基础类方法 printArea:
1 2 3 function Shape : printArea ()
print ( "面积为 " , self .area) -- 100
end
创建对象并求出面积:
1 2 local myshape = Shape : new ( nil , 10 )
myshape : printArea () -- 100
在派生类 Square 中重写基类 Shape 中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Square = Shape : new ()
-- 派生类方法 new
function Square : new ( o , side )
o = o or Shape : new ( o , side )
setmetatable ( o , self )
self .__index = self
return o
end
-- 派生类方法 printArea(重写基础类的函数)
function Square : printArea ()
print ( "正方形面积为 " , self .area)
end
-- 创建对象
local mysquare = Square : new ( nil , 10 )
mysquare : printArea () -- 100
在派生类 Rectangle 中重写基类 Shape 中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Rectangle = Shape : new ()
-- 派生类方法 new
function Rectangle : new ( o , length , breadth )
o = o or Shape : new ( o )
setmetatable ( o , self )
self .__index = self
self .area = length * breadth
return o
end
-- 派生类方法 printArea
function Rectangle : printArea ()
print ( "矩形面积为 " , self .area)
end
-- 创建对象
local myrectangle = Rectangle : new ( nil , 10 , 20 )
myrectangle : printArea () -- 200
使用 . 可以调用对象中的方法,使用 : 也可以调用对象中的方法,那它们有什么区别呢?
. 与 : 的区别在于使用 : 定义的函数隐含 self 参数,使用 : 调用函数会自动将调用方法的对象传入调用方法中的 self 参数。
8.3 示例的优化
上述示例中每次 new 新实例的时候都需要将第一个变量的值设为 nil,很不方便。
可以把变量 o 放在函数里创建,免去麻烦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 --创建一个类,表示四边形
local RectAngle = { } --声明类名和类成员变量
function RectAngle : new ( len , wid ) --声明新建实例的 new 方法
local o = {
--设定各个项的值
length = len or 0 ,
width = wid or 0 ,
area = len * wid
}
setmetatable ( o , { __index = self } ) --将自身的表映射到新 new 出来的表中
return o
end
function RectAngle : getInfo () --获取表内信息的方法
return self .length, self .width, self .area
end
local a = RectAngle : new ( 10 , 20 )
print ( a : getInfo ()) -- 10 20 200
local b = RectAngle : new ( 10 , 10 )
print ( b : getInfo ()) -- 10 10 100
print ( a : getInfo ()) -- 10 20 200
9. 其他内容
本节用于补充本文中没提到的知识点,以链接的方式给出: