在之前的博客《Flutter 热更新及动态UI生成》一文中,通过编写LuaDardo虚拟机,大致介绍了在Dart语言之上开发Lua虚拟机给Flutter提供动态能力的方案,但Lua语言流行并不算广泛,许多人对小巧精湛的Lua语言缺少了解,认为将Lua替换为JavaScript语言更好。为此,我特别整理了两篇Lua语言的快速上手指南,相信充分学习了解后,也会感觉到在特定需求场景下,小巧简洁的Lua将更具“胶水”优势。
基础语法篇
注释
- 单行注释
- 多行注释
-- 单行注释,使用两个减号
--[[
多行注释
多行注释
多行注释
]]
数据类型
Lua 中有 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({})) --> table
print(type(io.stdin)) --> userdata
变量
Lua 变量有三种类型:
- 全局变量
默认情况下,Lua中所有的变量都是全局变量 - 局部变量
使用local
显式声明在函数内的变量,以及函数的参数,都是局部变量。在函数外即使用local
去声明,它的作用域也是当前的整个文件,这相当于一个全局变量。 - 表中的域
注意,变量的默认值均为 nil
。Lua语言不区分未初始化变量和被赋值为nil
的变量,因此全局变量无须声明即可使用。在Lua中,应尽可能使用局部变量,这有两个好处:
- 避免命名冲突
- 访问局部变量的速度比全局变量更快
a = 5 -- 全局变量
local b = 5 -- 局部变量
function joke()
c = 5 -- 全局变量
local d = 6 -- 局部变量
end
a, b, c = 0, 2, 7 -- Lua支持多变量赋值
流程控制
代码块
在其他语言中,代码块会使用一对花括号"{“和”}"括起来的,在Lua中是使用关键字括起来
do
print("Hello") -- 使用do end包裹一个代码块
end
逻辑分支语句
if
语句格式
if(布尔表达式) then
--[ 在布尔表达式为 true 时执行的语句 --]
end
代码示例
if( a < 20 ) then
-- if条件为 true 时执行
print("a 小于 20" );
end
--[ if...else语句 --]
if( a < 20 ) then
print("a 小于 20" )
else
print("a 大于 20" )
end
if...elseif...else
多重判断
a = 100
if( a == 10 ) then
print("a 的值为 10" )
elseif( a == 20 ) then
print("a 的值为 20" )
elseif( a == 30 ) then
print("a 的值为 30" )
else
print("没有匹配 a 的值" )
end
注意,Lua中的if
语句也可以进行嵌套,但是建议优先考虑使用if...elseif...else
来组合这些条件判断。Lua中没有switch
语句。
循环语句
主要有三种循环
while循环
-- while循环
a=10
while( a < 20 ) do
print("a 的值为:", a)
a = a+1
end
for循环
for循环有两种
- 数值for循环
如下,var从exp1变化到exp2,每次变化以exp3为步长递增var。exp3是可选的,如果不指定,默认为1
for var=exp1,exp2,exp3 do
<执行体>
end
- 泛型for循环
通过一个迭代器函数来遍历所有值,类似于Java的foreach循环
for i,v in ipairs(a) do
<执行体>
end
代码示例
-- 数值型for循环,等价于C语言:for(int i=0,i<=9,i++)
for i = 0,9,1 do
print(i)
end
-- 遍历一个数组
days = {"Suanday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
for i,v in ipairs(days) do
print(i, v)
end
需要注意,除了ipairs
函数外,还有另一迭代器函数pairs
也可用于遍历。
这两个函数的区别在于:ipairs
仅仅遍历值,按照索引升序遍历,索引中断停止遍历。即不能返回 nil,只能返回数字 0,如果遇到 nil 则退出。它只能遍历到集合中出现的第一个不是整数的 key。pairs
能遍历集合的所有元素。即 pairs
可以遍历集合中所有的 key,并且除了迭代器本身以及遍历表本身还可以返回 nil。
local tab = {
[2] = "test2",
[6] = "test3",
[4] = "test1"
}
for k, v in pairs(tab) do -- 使用ipairs则无法正确遍历,因为索引不是正确有序的
print(k, v)
end
repeat循环
相当于其他语言的do…while循环,它是条件后行,即循环的条件语句在当前循环结束后去判断,因此循环体中语句至少要执行一次
repeat
statements
while( condition )
代码示例
a = 10
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )
循环控制
Lua中支持循环控制语句break
、return
和goto
,但要明确三者的区别
-
break
和return
语句都可用于跳出循环,但break
用于跳出最内层循环,而return
用于返回函数的执行结果或简单地结束函数的运行 - 类似于C语言的
goto
语句,用于将当前程序跳转到相应的标签处继续执行
Lua中没有continue
语句,这里我们使用goto
模仿一个continue
语句
while true do
if true then
-- 跳转到标签continue处
goto continue
end
-- some code
::continue::
end
标签名可以是任意有效的标识符。标签的语法格式:标签名称前后紧跟两个冒号,例如::continue::
。
注意,在使用goto
跳转时,Lua语言设置了一些限制条件。
- 首先,标签遵循常见的可见性规则,因此不能直接跳转到一个代码块中的标签(因为代码块中的标签对外不可见)
- 其次,
goto
不能跳转到函数外(注意第一条规则已经排除了跳转进一个函数的可能性) - 最后,
goto
不能跳转到局部变量的作用域
函数
一个Lua程序既可以调用Lua语言编写的函数,也可以调用C语言(或者宿主程序使用的其他任意语言)编写的函数。一般来说,我们选择使用C语言编写的函数来实现对性能要求更高,或不容易直接通过Lua语言进行操作的操作系统机制等。例如,Lua语言标准库中所有的函数就都是使用C语言编写的。不过,无论一个函数是用Lua语言编写的还是用C语言编写的,在调用它们时都没有任何区别。
声明一个函数的格式:
optional function 函数名(形参列表)
-- 函数体
end
其中optional
是可选的,用于指明函数是全局函数还是局部函数,默认为全局函数;使用关键字 local则为局部函数。
-- 声明一个求最大值的函数
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end
print(max(10,4))
Lua函数支持多值返回
function getAddr()
return "127.0.0.1",8080
end
ip, port = getAddr()
print(ip,port)
Lua函数还支持可变参,但有其他固定参数时,可变参必须放置到参数列表的最后
-- 参数列表中使用三个点表示可变参
function average(...)
local sum = 0
local arg={...}
for i,v in ipairs(arg) do
sum = sum + v
end
return sum/#arg -- #用于获取表长度
end
print("平均值为",average(10,5,3,4))
特别注意:
- 调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua语言会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的个数。
- 函数调用时需要一对圆括号,但当函数只有一个参数且该参数是字符串常量或表构造器时,括号是可省略的
print "hello,world" -- print("hello,world")
func{a=1,b=2} -- func({a=1,b=2})
type{} -- type({})
匿名函数与闭包
-- 匿名函数
add = function (a,b)
return a+b
end
print(add(10,8))
我们可以在函数中定义函数
-- 在函数中创建一个新的函数sub
function create()
function sub(x,y)
return y-x
end
end
create()
-- 由于Lua中默认变量是全局变量,因此sub是全局变量,外部可使用
print(sub(10,8))
-- 添加local即变为局部变量
function create()
local function sub(x,y)
return y-x
end
end
结合上述匿名函数示例,我们可以将代码写得更像函数式编程
function create()
local sub = function(x,y)
return y-x
end
-- do something
end
使用示例
-- 声明一个函数,接受函数类型变量做形参
function calc(a,b,callback)
print(callback(a,b))
end
-- 调用calc,传入一个做加法的匿名函数
calc(10,8,function (a,b)
return a+b
end)
-- 传入一个做减法的匿名函数
calc(10,8,function (a,b)
return a-b
end)
运算符
Lua提供了以下几种运算符类型:
- 算术运算符
+
、-
、*
、/
、%
、^
同C语言,但多出了一种负号运算符,例如:-7
- 关系运算符
同C语言,但请注意,Lua的不等于使用~=
,而不是!=
- 逻辑运算符
and
、or
、not
表示与、或和非 - 其他运算符
运算符 | 描述 | 实例 |
| 连接两个字符串 |
|
| 返回字符串或表的长度 |
|
运算符优先级
优先级从高到低
字符串
Lua 语言中字符串三种表示方式:
- 单引号括起的一串字符
- 双引号括起的一串字符
-
[[
和]]
括起的一串字符,可以包含多行
str1 = "this is a string"
str2 = 'this is a string'
str3 = [[
this is a string
this is a string
this is a string
]]
注意:字符串也可以用长括号括起来。长括号由两个方括号[,]
组成,其间有零个或多个等号=
。当等号的数量为N时,它被称为 “N级长括号”。以下是长括号的例子:
-
[[
开始0级的长括号 -
]]
闭合第0级的长括号 -
[=[
开始第1级的长括号 -
]=]
闭合第1级的长括号 -
[===[
开始第3层的长括号 -
]===]
闭合第3层的长括号
在长符串中,从同一水平的开括号到闭括号的部分是字符串。 与使用双引号或单引号的形式的字符串不同,长字符串可以包含换行符而无需特殊的转义符号。 请注意,当一个长字符串开始的括号后紧接一个换行符时, 这个换行符不会放在字符串内。
以下示例将相同的字符串分配给变量a:
a = 'abc\n123'
a = "abc\n123"
a = [[abc
123]]
a = [==[
abc
123]==]
字符串操作函数
string.upper(arg)
、string.lower(arg)
字符串全部转大写、小写string.char(arg)
和string.byte(arg[,int])
char 将整型编码转成字符并连接, byte 转换字符为整型编码
print(string.char(97,98,99,100))
string.len(arg)
计算字符串长度(计算ASCII字符串,不能用于计算中文)string.format(...)
类似于C语言的printf
,使用占位符格式化字符串
print(string.format("the value is:%d",4))
string.reverse(arg)
字符串反转string.gsub(mainString,findString,replaceString,num)
在字符串中替换。mainString
为要替换的字符串,findString
为被替换的字符,replaceString
要替换的新字符,num 替换次数(忽略,则全部替换)
string.gsub("aaaa","a","z",3) --> zzza 3
string.find(str, substr, [init, [end]])
在一个指定的目标字符串中搜索指定的内容(第三个参数为索引,可忽略),返回其具体位置。不存在则返回nil
print(string.find("Hello Lua", "Lua", 1))
string.sub(string, i,[j])
从一个字符串中截取子串,返回的是一个新字符串,而不是修改原字符串。i、j为范围索引,左闭右开
--[[
字符转换
]]
-- 转换第一个字符
print(string.byte("Lua"))
-- 转换第三个字符
print(string.byte("Lua",3))
-- 转换末尾第一个字符
print(string.byte("Lua",-1))
-- 转换末尾第二个字符
print(string.byte("Lua",-2))
-- ASCII 码转换为字符
print(string.char(97))
--[[
字符串转换
]]
print(tonumber("18")) -- 字符串转数值
print(tostring(20)) -- 数值转字符串
--[[
字符串拼接
]]
str1 = "www."
str2 = "bczl"
str3 = ".xyz"
-- 使用 .. 进行字符串连接
print("拼接字符串",str1..str2..str3)
-- 字符串于整数拼接,返回的仍是字符串
num = "12"..1
print(num)
特别注意:
string.len()
以及#
用于计算ASCII字符串长度,当传入Unicode字符串时,计算得到的是字节数而不是字符数。当我们需要处理中文等Unicode字符串时,应使用utf8标准库中的函数。
-- lua5.3新增utf8标准库
print(utf8.len("这是中文"))
-- 遍历每个字符的编码
for i, c in utf8.codes("编程之路从0到1") do
-- char函数将编码转为字符
print(i,utf8.char(c))
end
表
表是Lua中唯一的数据结构。表本质上是一种辅助数组(associative array),这种数组不仅可以使用数值作为索引,也可以使用字符串或其他任意类型的值作为索引(nil除外),但要注意,表是不固定大小的,会自动扩容。
使用构造器表达式(constructor expression)创建表
-- 初始化表
mytable = {}
-- 指定值
mytable[1]= "Lua"
mytable["wow"] = "before"
-- 获取值
print("索引为 1 的元素是 ", mytable[1])
print("索引为 wow 的元素是 ", mytable["wow"])
同一个表中存储的值可以具有不同的类型索引。当把表当作结构体使用时,还可以把索引当作成员名称使用,例如:a.name
等价于a["name"]
。
a = {}
a["name"] = "zhangsan"
-- 等价于a["name"]
print(a.name)
a.age = 19
print(a["age"])
对Lua语言而言,这两种形式是等价且可以自由混用的;不过,对于阅读代码的人而言,这两种形式可能代表了不同的意图。a.name
的点分形式表达出此表是被当作结构体使用的,a["name"]
的字符串索引形式则表达出此表被当作类似字典的数据结构使用。
注意:a.x
和a[x]
常常被混淆。实际上,a.x
代表的是a[“x”],即由字符串"x"索引的表;而a[x]
则是指由变量x对应的值索引的表,若不注意此问题,会导致诡异的Bug!
a = {}
x = "y"
a[x] = 10
print(a[x]) -- 等价 a["y"]
print(a.x) -- 等价 a["x"] 打印 nil
print(a.y) -- 等价 a["y"]
此外,a[2.0]
和a[2]
是等价的,浮点数作表索引时,任何能够被转换为整型的浮点数都会被转换成整型数,不能被转换的,如a[2.1]
则不会发生转换。
表构造器有三种字面量初始化方式
-- 1. 列表式构造。创建的是数组(一种特殊的表,索引是整数,类似其他语言的列表)
fruits = {"banana","orange","apple"}
-- 2. 记录式构造。索引可以是其他值,类似字典
a = {x=1,y=2,z=3}
-- 3. 通用构造器。
op = {["+"]="add",["-"]="sub"}
总结:
- Lua 没有其他语言的列表这种数据结构,索引为正整数的表,即用来表示列表功能。
{"a","b","c"} --> 等价于 {[1]="a",[2]="b",[3]="c"}
- **Lua 中序列的索引不是从0而是从1开始,这和我们的编程常识不同,需要特别注意!**数组的第一个元素是
a[1]
- 列表式构造和记录式构造可以混合使用
-- 混合两种表构造器
a = {x=1,y=2,"lua",n=100,"dart","java"}
print(a[1]) -- lua
print(a[2]) -- dart
print(a[3]) -- java
- 前两种构造器都不能使用负数做索引初始化表元素,也不能使用不符合规范的标识符作为索引!对于这类需求,需要使用第三种通用构造器。
-- +86做索引错误!
-- {+86="phone"}
{["+86"]="phone"} -- 正确
用表模拟数组(列表)
array = {}
-- 获取数组长度
print(#array) -- 0
-- 初始化数组
for i = 1, 10 do
array[i] = i*2
end
-- 再次获取数组长度
print(#array) -- 10
由于未初始化的元素均为nil
,所以可以利用nil
值来标记数组或列表的结束。Lua中把所有元素都不为nil
的数组称为序列,并提供了获取序列长度的操作符#
。长度操作符也为操作序列提供了几种写法:
print(a[#a]) -- 打印序列a的最后一个元素
a[#a] = nil -- 删除序列的最后一个元素
a[#a + 1] = v -- 把v的值加入到序列的末尾
对于中间存在空洞(nil
值)的列表而言,序列长度操作符是不可靠的,它只能用于序列。更准确地说,序列是由指定的n个正数数值类型的键所组成集合{1,...,n}
形成的表(请注意值为nil
的键实际不在表中)。特别地,不包含数值类型键的表就是长度为零的序列。
对于Lua语言而言,一个为nil
的字段和一个不存在的元素没有区别:
a = {10,20,nil,nil} -- 等价于 {10,20}
print(#a)
但是很多列表是通过逐个添加元素创建出来的。任何按照这种方式构造出来的带有空洞的列表,其最后一定存在为nil
的值,只是nil
可能存在几个元素中间,因此存在空洞的列表的行为是Lua语言中最具争议的内容之一。
表操作
- table.concat(table, separator, start, end)
接收一个字符串数组并返回字符串连接后的结果。元素间以指定分隔符separator
隔开。除了table
外,其他参数都是可选的。separator
默认是空字符,start
默认为1,end
默认为数组的长度 - table.insert(table, position, value)
将元素value
插入到数组的指定位置。position
是可选的,默认为数组末尾 - table.remove(table, position)
删除并返回数组指定位置上的元素。删除后所有元素前移,以填补空隙。若不指定位置,则会删除数组的最后一个元素 - table.sort(table, compare)
对给定的数组进行升序排序,其中compare
为可选参数,可以是一个外部函数用来自定义排序标准。排序函数的标准是接收两个参数并返回一个布尔型的值,若返回值为true
则表示升序,反之则为降序 - table.move (a1, f, e, t [,a2])
将表a中从索引f~e的元素(包含f和e位置的元素)移动到位置t上。注意,目标范围可以与源范围重叠。计算机领域移动(move)通常指将一个值从一个地方拷贝(copy)到另一个地方。当带有可选参数a2时,该函数将第一个表中的元素复制到第二个表中,相当于克隆操作。
local arr = {"alpha", "beta"}
table.insert(arr,1, "delta")
table.insert(arr,2, "epsilon")
table.insert(arr, "zeta")
print(table.concat(arr, ","))
table.remove(arr,2)
table.remove(arr)
-- 对数组排序
table.sort(arr)
print(table.concat(arr, ","))
local a1 = {"a","b","c","d"}
local a2 = {}
table.move(a1 ,1,#a1,4)
print(table.concat(a1, ",")) --> a,b,c,a,b,c,d
table.move(a1 ,1,#a1,1,a2)
print(table.concat(a2, ",")) --> a,b,c,a,b,c,d