loong - Lua - 基础
1、注释
注释在Lua中用于添加说明和注释。单行注释以 – 开始,多行注释则使用 --[[…]]。
-- 这是一条单行注释
--[[
这是一个多行注释
可以跨越多行
]]
2、变量
变量在Lua中无需显示声明类型。使用local
关键字创建局部变量,全局变量直接声明。
-- 局部变量
local age = 30
-- 全局变量
name = "John"
3、基本数据类型
基本数据类型包括整数、浮点数、字符串、布尔值和
nil
,表是一种非常灵活的数据结构。
3.1 、nil (空)
--- nil
local n --> 不赋值,默认为nil
print(n) --> output:nil
n = 100
print(n) --> output:100
3.2 、boolean (布尔)
可选值ture/false,Lua中nil和false为假,其他所有值均为"真".比如 0 和 空字符串 就是"真"
local a = true
local b = 0
local c = nil
if a then
print("执行")
else
print("不执行")
end
if b then
print("执行")
else
print("不执行")
end
if c then
print("不执行")
else
print("执行")
end
3.3 、 numbser (数字)
number类型用于表示实数,和C/C++中的double类型很相似.可以使用数学函数 math.floor(向下取整) 和 math.ceil(向上取整) 进行取整操作
local order = 3.99
local score = 98.01
print(math.floor(order)) -->output:3
print(math.ceil(98.01)) -->output:99
3.4 、string (字符串)
我们把两个正的方括号(即[[)间插入 n 个等号定义为第 n 级正长括号。就是说,0 级正的长括号写作 [[ ,一级正的长括号写作 [=[,如此等等。反的长括号也作类似定义;举个例子,4 级反的长括号写作 ]====]。 一个长字符串可以由任何一级的正的长括号开始,而由第一个碰到的同级反的长括号结束。整个词法分析过程将不受分行限制,不处理任何转义符,并且忽略掉任何不同级别的长括号。这种方式描述的字符串可以包含任何东西,当然本级别的反长括号除外。例:[[abc\nbc]],里面的 “\n” 不会被转义。
另外,Lua 的字符串是不可改变的值,不能像在 c 语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符。
在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在Lua虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。
这意味着
- 创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销)
- 内容相同的 Lua 字符串不会占用多份存储空间;
- – 已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n)。
-- 表示字符串三种方式
local str1 = 'hello world'
local str2 = "hello Lua"
local str3 = [["add\name",'hello']]
local str4 = [=[string have a [[]].]=]
print(str1) -->output: hello world
print(str2) -->output: hello Lua
print(str3) -->output: "add\name",'hello'
print(str4) -->output: string have a [[]].
3.5 、table(表)
表是Lua的核心数据结构,使用花括号
{}
定义。表是可以包含键值对,键和值可以是任何数据类型。
-- Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,索引通常是字符串(string)或者 number 类型,但也可以是除 nil 以外的任意类型的值。
local corp = {
web = "www.google.com", --索引为字符串,key = "web",value = "www.google.com"
telephone = "12345678", --索引为字符串
staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
100876, --相当于 [1] = 100876,此时索引为数字;key = 1, value = 100876
100191, --相当于 [2] = 100191,此时索引为数字
[10] = 360, --直接把数字索引给出
["city"] = "Beijing" --索引为字符串
}
print(corp.web) -->output:www.google.com
print(corp["telephone"]) -->output:12345678
print(corp[2]) -->output:100191
print(corp["city"]) -->output:"Beijing"
print(corp.staff[1]) -->output:Jack
print(corp[10]) -->output:360
4、表达式
4.1、算数表达式
算数表达式列表
序号 | 算术运算符 | 说明 |
---|---|---|
1 | + | 加法 |
2 | - | 减法 |
3 | * | 乘法 |
4 | / | 除法 |
5 | ^ | 指数 |
6 | % | 取模 |
7 | // | 整除(>=lua5.3) |
8 | - | 负号 |
示例
print(1 + 2) -->打印 3
print(5 / 10) -->打印 0.5。 这是 Lua 不同于 C 语言的地方
print(5.0 / 10) -->打印 0.5。 浮点数相除的结果是浮点数
print(2 ^ 10) -->打印 1024。 求 2 的 10 次方
-- print(10 / 0) -->注意除数不能为0,计算的结果会出错
local num = 1357
print(num % 2) -->打印 1
print((num % 2) == 1) -->打印 true。 判断 num 是否为奇数
print((num % 5) == 0) -->打印 false。判断 num 是否能被 5 整数
4.2、 关系运算符
关系运算符列表
序号 | 关系运算符 | 说明 |
---|---|---|
1 | < | 小于 |
2 | > | 大于 |
3 | <= | 小于等于 |
4 | >= | 大于等于 |
5 | == | 等于 |
6 | ~= | 不等于 |
示例
print(1 < 2) -->打印 true
print(1 == 2) -->打印 false
print(1 ~= 2) -->打印 true
local a, b = true, false
print(a == b) -->打印 false
注意:Lua 语言中“不等于”运算符的写法为:~=
在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等。可以看下面的例子:
local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
print("a==b")
else
print("a~=b")
end
---output: a~=b
由于 Lua 字符串总是会被“内化”,即相同内容的字符串只会被保存一份,因此 Lua 字符串之间的相等性比较可以简化为其内部存储地址的比较。这意味着 Lua 字符串的相等性比较总是为 O(1)。 而在其他编程语言中,字符串的相等性比较则通常为 O(n),即需要逐个字节(或按若干个连续字节)进行比较。
4.3、逻辑运算符
逻辑运算符列表
序号 | 逻辑运算符 | 说明 |
---|---|---|
1 | and | 逻辑与 |
2 | or | 逻辑或 |
3 | not | 逻辑非 |
Lua 中的 and 和 or 是不同于 C 语言的。在 C 语言中,and 和 or 只得到两个值 1 和 0,其中 1 表示真,0 表示假。而 Lua 中 and 的执行过程是这样的:
-
a and b
:-
若 a 为 nil 或 false,b 为任意的真值,则返回 a (即 nil 或 false),与出现顺序无关;
-
若 a 为 nil,b 为 false,则哪个 先出现 就返回哪个(短路求值);
-
若 a,b 均为真值,则返回 后出现 的那个值。
-
-
a or b
:- 若 a 为 nil 或 false,b 为任意的真值,则返回 b (即真值),与出现顺序无关;
- 若 a 为 nil,b 为 false,则返回 后出现 的那个;
- 若 a,b 均为真值,则返回 先出现 的那个值(短路求值)。
示例
local c = nil
local d = 0
local e = 100
local f = false
----------- and -----------
print(c and d) -->output nil
print(d and c) -->output nil
print(c and e) -->output nil
print(e and c) -->output nil
print(c and f) -->output nil
print(f and c) -->output false
print(d and e) -->output 100
print(e and d) -->output 0
print(d and f) -->output false
print(f and d) -->output false
----------- or -----------
print(c or d) -->output 0
print(d or c) -->output 0
print(c or e) -->output 100
print(e or c) -->output 100
print(c or f) -->output false
print(f or c) -->output nil
print(d or e) -->output 0
print(e or d) -->output 100
print(d or f) -->output 0
print(f or d) -->output 0
----------- not -----------
print(not c) -->output true
print(not d) -->output false
print(not e) -->output false
print(not f) -->output true
注意:所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。
4.4、字符串连接符号
在 Lua 中连接两个字符串,可以使用操作符“…”(两个点)。如果其中任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数
string.format
连接字符串。
print("Hello " .. "World") -->output Hello World
print(0 .. 1) -->output 01
str1 = string.format("%s-%s","hello","world")
print(str1) -->output hello-world
str2 = string.format("%d-%s-%.2f",123,"world",1.21)
print(str2) -->output 123-world-1.21
由于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 … 来拼接最终结果),则性能损耗会非常大。 在这种情况下,推荐使用 table 和
table.concat()
来进行很多字符串的拼接
local pieces = {}
for i, elem in ipairs(my_list) do
pieces[i] = my_process(elem)
end
local res = table.concat(pieces)
4.5、一元运算符
用来返回字符串(string)或者表(table)的长度
字符串
对于字符串来说,其返回值是该字符串占有多少个字节。也就是当字符串中的一个字符占一个字节时,字符串的长度。
a = "Hello "
b = "World"
print("连接字符串 a 和 b ", a..b ) -- 连接字符串 a 和 b Hello World
print("b 字符串长度 ",#b ) -- b 字符串长度 5
print("字符串 Test 长度 ",#"Test" ) -- 字符串 Test 长度 4
print("菜鸟教程网址长度 ",#"www.runoob.com" ) -- 菜鸟教程网址长度 14
表
作用于表时,返回的实际上是表的一个边界(border);一个表t的边界border,是一个满足下列条件的非负数:(border ==0 or t[border] != nil) and (t[border+1]==nil or border ==math.maxinteger)
在lua5.4中,math.maxinteger=9223372036854775807.这个条件的意思是说,一个边界是表中的满足下一个索引是nil的,但是本身的索引是有效的正整数,即t[border]!=nil,但是t[border+1]=nil。再加上两种特殊情况。当t[1]=nil时为0,当math.maxinteger的索引值存在,即t[math.integer]!=nil时为math.maxinteger.
当一个表只有一个边界时,叫做序列(sequence)。比如表{ 1,2,3,4,5} 是一个序列,他只有一个边界5,而{1,2,nil,4 }不是序列,有两个边界2和4。在索引为3处的值为nil,也叫做一个洞(hole)。
#t返回的是表t的任意一个边界,具体的返回值跟表内部的表示细节有关,或者说依赖于表的非数值键(key)的内存地址有关。
#操作符的复杂度为O(logn),n是表的最大整数键。
--the version of lua is lua5.4.4
t={1,2,3,4,5}
print(#t) --output 5
t={1,2,nil,4}
print(#t) --output 4
t={nil,1,2}
print(t) --output 3
t={}
t[2]=1
t[3]=2
print(#t) --output 0
t={1,2,3}
print(#t) --output 3
t[7]=7
print(#t) --output 3
t[6]=6
print(#t) --output 7
4.6、优先级
Lua 操作符的优先级如下表所示(从高到低):
序号 | 操作符 | 优先级 |
---|---|---|
1 | ^ | 高 |
2 | not # -(负号) | ↓ |
3 | * / % | ↓ |
4 | + -(减法) | ↓ |
5 | … | ↓ |
6 | < > <= >= == ~= | ↓ |
7 | and | ↓ |
8 | or | 低 |
示例
local a, b = 1, 2
local x, y = 3, 4
local i = 10
local res = 0
res = a + i < b/2 + 1 -->等价于res = (a + i) < ((b/2) + 1)
res = 5 + x^2*8 -->等价于res = 5 + ((x^2) * 8)
res = a < y and y <=x -->等价于res = (a < y) and (y <= x)
若不确定某些操作符的优先级,就应显式地用括号来指定运算顺序。这样做还可以提高代码的可读性。
5、控制结构
5.1、条件结构
使用
if
、else
、和elseif
来实现条件分支。
1. 单个if分支型
if age <13 then
print("未成年")
end
2.两个分支if-else型
if age <13 then
print("未成年")
else
print("老年")
end
3.多个分支if-elseif-else型
if age <13 then
print("未成年")
elseif age >= 18 and age < 65 then
print("成年")
-- 支持多个elseif
else
print("老年")
end
嵌套型
if age <13 then
print("未成年")
elseif age >= 18 and age < 65 then
print("成年")
else
if age > 70 then
print("古来稀")
else
print("加油多活几年")
end
end
5.2、循环结构
5.2.1 while 型
Lua 跟其他常见语言一样,提供了 while 控制结构,语法上也没有什么特别的。但是没有提供 do-while 型的控制结构,但是提供了功能相当的
repeat
。while 型控制结构语法如下,当表达式值为假(即
false
或nil
)时结束循环。也可以使用break
语言提前跳出循环。
while 表达式 do
--body
end
-- 使用示例
x = 1
sum = 0
while x <= 5 do
sum = sum + x
x = x + 1
end
print(sum) -->output 15
Lua 并没有像许多其他语言那样提供类似 continue
这样的控制语句用来立即进入下一个循环迭代。因此,我们需要仔细地安排循环体里的分支,以避免这样的需求。
没有提供 continue
,却也提供了另外一个标准控制语句 break
,可以跳出当前循环。例如我们遍历 table,查找值为 11 的数组下标索引:
local t = {1, 3, 5, 8, 11, 18, 21}
local i
for i, v in ipairs(t) do
if 11 == v then
print("index[" .. i .. "] have right value[11]")
break
end
end
5.2.2 repeat 型
Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。简单点说,执行 repeat 循环体后,直到 until 的条件为真时才结束,而其他语言(如:C++ 语言)的 do-while 则是当条件为假时就结束循环。
repeat
print("执行逻辑")
until false -- 执行一次就退出
5.2.3 for 型
for 语句有两种形式:数字 for(numeric for)和范型 for(generic for)。
1. for 数字型
数字型for的语法如下:
for var = begin, finish, step do
--body
end
关于数字型 for 需要关注以下几点:
- 1、
var
从begin
变化到finish
,每次变化都以step
作为步长递增var
; - 2、
begin
、finish
、step
三个表达式只会在循环开始时执行一次; - 3、第三个表达式
step
是可选的,默认为1
; - 4、控制变量
var
的作用域仅在 for 循环内,若需要在外面控制,则需将值赋给一个新的变量; - 5、循环过程中不要改变控制变量的值,那样会带来不可预知的影响。
数字型示例
-- 示例一
for i = 1, 5 do
print(i)
end
-- output:
1
2
3
4
5
-- 示例二
for i = 1, 10, 2 do
print(i)
end
-- output:
1
3
5
7
9
如果不想给循环设置上限的话,可以使用常量 math.huge
:
for i = 1, math.huge do
if (0.3*i^3 - 20*i^2 - 500 >=0) then
print(i)
break
end
end
2. for 泛型
泛型 for 循环通过一个迭代器(iterator)函数来遍历所有值:
-- 打印数组 a 的所有值
local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
print("index:", i, " value:", v)
end
-- output:
index: 1 value: a
index: 2 value: b
index: 3 value: c
index: 4 value: d
Lua 的基础库提供了 ipairs()
,这是一个用于遍历数组的迭代器函数。在每次循环中,i
会被赋予一个索引值,同时 v
被赋予一个对应于该索引的数组元素值。
下面是另一个类似的示例,演示了如何遍历一个 table 中所有的 key
-- 打印table t中所有的key
for k in pairs(t) do
print(k)
end
从外观上看泛型 for 比较简单,但其实它是非常强大的。通过不同的迭代器,几乎可以遍历所有的东西,而且写出的代码极具可读性。
标准库提供了几种迭代器,包括:
- 用于迭代文件中每行的
io.lines()
; - 迭代 table 元素的
pairs()
; - 迭代数组元素的
ipairs()
; - 迭代字符串中单词的
string.gmatch()
等。
泛型 for 循环与数字型 for 循环有两个相同点:
- 循环变量是循环体的局部变量;
- 决不应该对循环变量作任何赋值。
对于泛型 for 的使用,再来看一个更具体的示例。假设有这样一个 table,它的内容是一周中每天的名称:
local days = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
}
现在要将一个名称转换成它在一周中的位置。为此,需要根据给定的名称来搜索这个 table。然而在 Lua 中,通常更有效的方法是创建一个“逆向 table”。
例如这个逆向 table 叫 revDays,它以一周中每天的名称作为索引,位置数字作为值:
local revDays = {
["Sunday"] = 1,
["Monday"] = 2,
["Tuesday"] = 3,
["Wednesday"] = 4,
["Thursday"] = 5,
["Friday"] = 6,
["Saturday"] = 7
}
接下来,要找出一个名称所对应的位置,只需用名字来索引这个逆向 table 即可:
local x = "Tuesday"
print(revDays[x]) -->3
当然,不必手动声明这个逆向 table,而是通过原来的 table 自动地构造出这个逆向 table:
local days = {
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
}
local revDays = {}
for k, v in pairs(days) do
revDays[v] = k
end
-- print value
for k,v in pairs(revDays) do
print("k:", k, " v:", v)
end
-- output:
k: Tuesday v: 2
k: Monday v: 1
k: Sunday v: 7
k: Thursday v: 4
k: Friday v: 5
k: Wednesday v: 3
k: Saturday v: 6
这个循环会为每个元素进行赋值,其中变量 k 为 key(1、2、…),变量 v 为 value(“Sunday”、“Monday”、…)。
值得一提的是,在 LuaJIT 2.1 中,ipairs()
内建函数是可以被 JIT 编译的,而 pairs()
则只能被解释执行。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历。事实上,即使未来 pairs()
可以被 JIT 编译,哈希表的遍历本身也不会有数组遍历那么高效,毕竟哈希表就不是为遍历而设计的数据结构。
5.2.4 break、return、goto
1、break
语句 break
用来终止 while
、repeat
和 for
三种循环的执行,并跳出当前循环体,继续执行当前循环之后的语句。下面举一个 while
循环中的 break
的例子来说明:
-- 计算最小的 x,使从 1 到 x 的所有数相加和大于 100
sum = 0
i = 1
while true do
sum = sum + i
if sum > 100 then
break
end
i = i + 1
end
print("The result is " .. i) -->output:The result is 14
在实际应用中,break
经常用于嵌套循环中。
2、return
return
主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。
return
只能写在语句块的最后,一旦执行了 return
语句,该语句之后的所有语句都不会再执行。若要写在函数中间,则只能写在一个显式的语句块内,参见示例代码:
local function add(x, y)
return x + y
--print("add: I will return the result " .. (x + y))
--因为前面有个 return,若不注释该语句,则会报错
end
local function is_positive(x)
if x > 0 then
return x .. " is positive"
else
return x .. " is non-positive"
end
--由于 return 只出现在前面显式的语句块,所以此语句不注释也不会报错,
--但是不会被执行,此处不会产生输出
print("function end!")
end
local sum = add(10, 20)
print("The sum is " .. sum) -->output:The sum is 30
local answer = is_positive(-10)
print(answer) -->output:-10 is non-positive
有时候,为了调试方便,我们可以在某个函数的中间提前 return
,以进行控制流的短路。此时我们可以将 return
放在一个 do ... end
代码块中,例如:
local function foo()
print("before")
do return end
print("after") -- 这一行语句永远不会执行到
end
3、goto
LuaJIT 一开始对标的是 Lua 5.1,但渐渐地也开始加入部分 Lua 5.2 甚至 Lua 5.3 的有用特性。goto
就是其中一个不得不提的例子。有了 goto
,我们可以实现 continue
的功能:
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print([[i'm end]])
end
-----------------结果---------------------
1 yes continue
i'm end
2 yes continue
i'm end
3 no continue
i'm end
在 GotoStatement 这个页面上,你能看到更多用 goto
玩转控制流的脑洞。
goto
的另外一项用途,就是简化错误处理的流程。有些时候你会发现,直接 goto 到函数末尾统一的错误处理过程,是更为清晰的写法。
local function process(input)
print("the input is", input)
if input < 2 then
goto failed
end
-- 更多处理流程和 goto err
print("processing...")
do return end
::failed::
print("handle error with input", input)
end
process(1)
process(3)
6、function(函数)
函数在Lua中使用
function
关键字定义,可以接受参数并返回值。
6.1、函数的定义
有名函数的定义本质上是匿名函数对变量的赋值
1. 全局函数
function foo()
end
-- 等价于
foo = function ()
end
2. 局部函数
local function foo()
end
-- 等价于
local foo = function()
end
由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。以下是示例代码
local function max(a, b) --定义函数 max,用来求两个数的最大值,并返回
local temp = nil --使用局部变量 temp,保存最大值
if(a > b) then
temp = a
else
temp = b
end
return temp --返回最大值
end
local m = max(-12, 20) --调用函数 max,找出 -12 和 20 中的最大值
print(m) --> output: 20
如果参数列表为空,必须使用
()
表明是函数调用。
local function func() --形参为空
print("no parameter")
end
func() --函数调用,圆扩号不能省
--> output:
no parameter
在定义函数时要注意几点:
- 利用名字来解释函数、变量的意图,使人通过名字就能看出来函数、变量的作用。
- 每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。
- 让代码自己说话,不需要注释最好。
由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如
function foo.bar(a, b, c)
-- body ...
end
此时我们是把一个函数类型的值赋给了 foo
表的 bar
字段。换言之,上面的定义等价于:
foo.bar = function (a, b, c)
print(a, b, c)
end
对于此种形式的函数定义,不能再使用 local
修饰符了,因为不存在定义新的局部变量了。
7、模块
Lua支持模块化编程,允许将相关功能封装在独立的模块中,并通过require
关键字加载他们。
8、字符串操作
Lua提供需要字符串处理函数,例如string.sub
用于截取子串,string.find
用于查找字符串中的子串等。
local text = "Lua programming"
local sub = string.sub(text,1,3)
print(sub)
-- 输出: "Lua"
9、错误处理
错误处理通常使用pcall
函数来包裹可能引发异常的代码块,以捕获并处理错误。这通常与assert
一起使用。
local success , result = pcall(function()
error("出错了")
end)
if success then
print("执行成功")
else
print("错误信息:" .. result)
end
6.2、函数的参数
1、按值传递
Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
local function swap(a, b) --定义函数 swap,在函数内部交换两个变量的值
local temp = a
a = b
b = temp
print(a, b)
end
local x = "hello"
local y = 20
print(x, y)
swap(x, y) --调用 swap 函数
print(x, y) --调用 swap 函数后,x 和 y 的值并没有交换
-->output
hello 20
20 hello
hello 20
在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:
- 若实参个数 大于 形参个数,从左向右,多余的实参被忽略;
- 若实参个数 小于 形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。
local function fun1(a, b) --两个形参,多余的实参被忽略掉
print(a, b)
end
local function fun2(a, b, c, d) --四个形参,没有被实参初始化的形参,用 nil 初始化
print(a, b, c, d)
end
local x = 1
local y = 2
local z = 3
fun1(x, y, z) -- z 被函数 fun1 忽略掉了,参数变成 x, y
fun2(x, y, z) -- 后面自动加上一个 nil,参数变成 x, y, z, nil
-->output
1 2
1 2 3 nil
2、变长参数
上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为 ...
, 表示该函数可以接收不同长度的参数。访问参数的时候也要使用 ...
。
local function func( ... ) -- 形参为 ... ,表示函数采用变长参数
local temp = {...} -- 访问的时候也要使用 ...
local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
-- 组内容使用 " " 拼接成字符串。
print(ans)
end
func(1, 2) -- 传递了两个参数
func(1, 2, 3, 4) -- 传递了四个参数
-->output
1 2
1 2 3 4
值得一提的是,LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。
3、具名参数
Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
local function change(arg) -- change 函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2
arg.height = arg.height * 2
return arg
end
local rectangle = { width = 20, height = 15 }
print("before change:", "width =", rectangle.width,
"height =", rectangle.height)
rectangle = change(rectangle)
print("after change:", "width =", rectangle.width,
"height =", rectangle.height)
-->output
before change: width = 20 height = 15
after change: width = 40 height = 30
4、引用传递
函数参数是 table 类型时,传递进来的是实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值。我们把上面改变长方形长和宽的例子修改一下。
function change(arg) --chang 函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2 --表 arg 不是表 rectangle 的拷贝,他们是同一个表
arg.height = arg.height * 2
end -- 没有return语句了
local rectangle = { width = 20, height = 15 }
print("before change:", "width = ", rectangle.width,
" height = ", rectangle.height)
change(rectangle)
print("after change:", "width = ", rectangle.width,
" height =", rectangle.height)
--> output
before change: width = 20 height = 15
after change: width = 40 height = 30
在常用基本类型中,除了 table 是 按址 传递类型外,其它的都是 按值 传递参数。
用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
6.3、函数的返回值
Lua 具有一项与众不同的特性,允许函数返回多个值。Lua 的库函数中,有一些就是返回多个值。
示例代码:使用库函数
string.find()
,在源字符串中查找目标字符串,若查找成功,则返回目标字符串在源字符串中的起始位置和结束位置的下标。
local s, e = string.find("hello world", "llo")
print(s, e) -->output 3 5
返回多个值时,值之间用“,”隔开。
示例代码:定义一个函数,实现两个变量交换值
local function swap(a, b) -- 定义函数 swap,实现两个变量交换值
return b, a -- 按相反顺序返回变量的值
end
local x = 1
local y = 20
x, y = swap(x, y) -- 调用 swap 函数
print(x, y) --> output 20 1
当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。
调整规则:
- 若返回值个数 大于 接收变量的个数,多余的返回值会被忽略掉;
- 若返回值个数 小于 参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。
示例代码:
function init() --init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
x = init()
print(x)
x, y, z = init()
print(x, y, z)
--output
1
1 lua nil
当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。
示例代码:
local function init() -- init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -->output 1 2 nil
local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -->output 2 1 lua
函数调用的实参列表也是一个列表表达式。考虑下面的例子:
local function init()
return 1, "lua"
end
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua
如果你确保只取函数返回值的第一个值,可以使用括号运算符,例如
local function init()
return 1, "lua"
end
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1
值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
6.4、全动态函数调用
调用回调函数,并把一个数组参数作为回调函数的参数。
local args = {...} or {}
method_name(unpack(args, 1, table.maxn(args)))
1、使用场景
如果你的实参 table
中确定没有 nil 空洞,则可以简化为
method_name(unpack(args))
- 你要调用的函数参数是未知的;
- 函数的实际参数的类型和数目也都是未知的。
伪代码
add_task(end_time, callback, params)
if os.time() >= endTime then
callback(unpack(params, 1, table.maxn(params)))
end
值得一提的是,unpack
内建函数还不能为 LuaJIT
所 JIT
编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。
2、小试牛刀
local function run(x, y)
print('run', x, y)
end
local function attack(targetId)
print('targetId', targetId)
end
local function do_action(method, ...)
local args = {...} or {}
method(unpack(args, 1, table.maxn(args)))
end
do_action(run, 1, 2) -- output: run 1 2
do_action(attack, 1111) -- output: targetId 1111
7、模块
从 Lua 5.1 语言添加了对模块和包的支持。一个 Lua 模块的数据结构是一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)。一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块。
可以使用内建函数require()
来加载和缓存模块。简单的说,一个代码模块就是一个程序库,可以通过require()
来加载。模块加载后的结果通常是一个 Lua table,这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量。require()
函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。
7.1、require()
函数
Lua 提供了一个名为 require()
的函数用来加载模块。要加载一个模块,只需要简单地调用 require("file")
就可以了,file 指模块所在的文件名。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。
在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。
相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。
把下面的代码保存在文件 my.lua 中
local _M = {}
local function get_name()
return "Lucy"
end
function _M.greeting()
print("hello " .. get_name())
end
return _M
把下面代码保存在文件 main.lua 中,然后执行 main.lua,调用上述模块。
local my_module = require("my")
my_module.greeting() -->output: hello Lucy
注:对于需要导出给外部使用的公共模块,出于安全考虑,要避免全局变量的出现。
我们可以使用 lj-releng 或 luacheck 工具完成全局变量的检测。至于如何做,到后面再讲。
另一个要注意的是,由于在 LuaJIT 中,require()
函数内不能进行上下文切换,所以不能够在模块的顶级上下文中调用 cosocket 一类的 API。
否则会报 attempt to yield across C-call boundary
错误。
8、标准库
Lua标准库包含丰富的功能,如文件操作、网络编程、正则表达式、时间处理等。可以通过内置的模块来使用这些功能,如io、socket等。
8.1、String库
Lua 字符串库包含很多强大的字符操作函数。字符串库中的所有函数都导出在模块 string 中。在 Lua 5.1 中,它还将这些函数导出作为 string 类型的方法。这样假设要返回一个字符串转换后的大写形式,可以写成
ans = string.upper(s)
, 也能写成ans = s:upper()
。为了避免与之前版本不兼容,此处使用前者。Lua 字符串总是由字节构成的。Lua 核心并不尝试理解具体的字符集编码(比如 GBK 和 UTF-8 这样的多字节字符编码)。
需要特别注意的一点是,Lua 字符串内部用来标识各个组成字节的 下标是从 1 开始的,这不同于像 C 和 Perl 这样的编程语言。
这在计算字符串位置的时候再也不用调整了,对于非专业的开发者来说可能也是一个好事情,string.sub(str, 3, 7) 直接表示从第三个字符开始到第七个字符(含)为止的子串。
string.byte(s [, i [, j ]])
返回字符 s[i]、s[i + 1]、s[i + 2]、······、s[j] 所对应的 ASCII 码。
i
的默认值为 1,即第一个字节,j
的默认值为 i 。
print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte("abc")) -- 缺少第二个和第三个参数,此时这两个参数都默认为 1
-->output
97 98 99
99
97
由于 string.byte
只返回整数,而并不像 string.sub
等函数那样(尝试)创建新的 Lua 字符串,
因此使用 string.byte
来进行字符串相关的扫描和分析是最为高效的,尤其是在被 LuaJIT 2 所 JIT 编译之后。
string.char (…)
接收 0 个或多个整数(整数范围:0~255),返回这些整数所对应的 ASCII 码字符组成的字符串。当参数为空时,默认是一个 0。
print(string.char(96, 97, 98))
print(string.char()) -- 参数为空,默认是一个0,
-- 你可以用string.byte(string.char())测试一下
print(string.char(65, 66))
--> output
`ab
AB
此函数特别适合从具体的字节构造出二进制字符串。这通常比使用 table.concat
函数和 ..
连接运算符更加高效。
string.upper(s)
接收一个字符串 s,返回一个把所有小写字母 变成了大写 字母的字符串。
print(string.upper("Hello Lua")) -->output HELLO LUA
string.lower(s)
接收一个字符串 s,返回一个把所有大写字母 变成了小写 字母的字符串。
print(string.lower("Hello Lua")) -->output hello lua
string.len(s)
接收一个字符串,返回它的长度。
print(string.len("hello lua")) -->output 9
使用此函数是 不推荐 的。应当总是使用 #
运算符来获取 Lua 字符串的长度。
由于 Lua 字符串的长度是专门存放的,并不需要像 C 字符串那样即时计算,因此获取字符串长度的操作总是 O(1)
的时间复杂度。
string.find(s, p [, init [, plain]])
在 s 字符串中第一次匹配 p 字符串。
- 若匹配成功,则返回 p 字符串在 s 字符串中出现的开始位置和结束位置;
- 若匹配失败,则返回 nil。
第三个参数 init 默认为 1,并且可以为负整数。
当 init 为负数时,表示从 s 字符串的 string.len(s) + init + 1
位置开始向后匹配字符串 p 。
第四个参数默认为 false,当其为 true 时,只会把 p 看成一个字符串对待。
local find = string.find
print(find("abc cba", "ab"))
print(find("abc cba", "ab", 2)) -- 从索引为2的位置开始匹配字符串:ab
print(find("abc cba", "ba", -1)) -- 从索引为7的位置开始匹配字符串:ba
print(find("abc cba", "ba", -3)) -- 从索引为5的位置开始匹配字符串:ba
print(find("abc cba", "(%a+)", 1)) -- 从索引为1处匹配最长连续且只含字母的字符串
print(find("abc cba", "(%a+)", 1, true)) --从索引为1的位置开始匹配字符串:(%a+)
-->output
1 2
nil
nil
6 7
1 3 abc
nil
对于 LuaJIT 这里有个性能优化点,string.find 方法,当只有字符串查找匹配时,是可以被 JIT 编译器优化的,有关 JIT 可以编译优化清单,大家可以参考 http://wiki.luajit.org/NYI,性能提升是非常明显的,通常是 100 倍量级。
这里有个的例子,大家可以参考 https://groups.google.com/forum/m/#!topic/openresty-en/rwS88FGRsUI。
string.format(formatstring, …)
按照格式化参数 formatstring,返回后面 ...
内容的格式化版本。编写格式化字符串的规则与标准 C 语言中 printf 函数的规则基本相同:
- 它由常规文本和指示组成,这些指示控制了每个参数应放到格式化结果的什么位置,及如何放入它们。
- 一个指示由字符
%
加上一个字母组成,这些字母指定了如何格式化参数,例如:d
用于十进制数、x
用于十六进制数、o
用于八进制数、f
用于浮点数、s
用于字符串等。
- 在字符
%
和字母之间可以再指定一些其他选项,用于控制格式的细节。
示例代码
print(string.format("%.4f", 3.1415926)) -- 保留 4 位小数
print(string.format("%d %x %o", 31, 31, 31)) -- 十进制数 31 转换成不同进制
d = 29; m = 7; y = 2015 -- 一行包含几个语句,用;分开
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))
-->output
3.1416
31 1f 37
today is: 29/07/2015
string.match(s, p [, init])
在字符串 s 中匹配(模式)字符串 p。
- 若匹配成功,则返回目标字符串中与模式匹配的子串;
- 否则返回 nil。
第三个参数 init 默认为 1,并且可以为负整数。
当 init 为负数时,表示从 s 字符串的 string.len(s) + init + 1
位置开始向后匹配字符串 p。
示例代码
print(string.match("hello lua", "lua"))
print(string.match("lua lua", "lua", 2)) --匹配后面那个lua
print(string.match("lua lua", "hello"))
print(string.match("today is 27/7/2015", "%d+/%d+/%d+"))
-->output
lua
lua
nil
27/7/2015
string.match
目前并不能被 JIT 编译,应 尽量 使用 ngx_lua 模块提供的 ngx.re.match
等接口。
string.gmatch(s, p)
返回一个迭代器函数,通过这个迭代器函数可以遍历到在字符串 s 中出现模式串 p 的所有地方。
s = "hello world from Lua"
for w in string.gmatch(s, "%a+") do --匹配最长连续且只含字母的字符串
print(w)
end
-->output
hello
world
from
Lua
t = {}
s = "from=world, to=Lua"
for k, v in string.gmatch(s, "(%a+)=(%a+)") do --匹配两个最长连续且只含字母的
t[k] = v --字符串,它们之间用等号连接
end
for k, v in pairs(t) do
print (k,v)
end
-->output
to Lua
from world
此函数目前并不能被 LuaJIT 所 JIT 编译,而只能被解释执行。应 尽量 使用 ngx_lua 模块提供的 ngx.re.gmatch
等接口。
string.rep(s, n)
返回字符串 s 的 n 次拷贝。
print(string.rep("abc", 3)) --拷贝3次"abc"
-->output abcabcabc
string.sub(s, i [, j])
返回字符串 s 中,索引 i 到索引 j 之间的子字符串。
- 当 j 缺省时,默认为 -1,也就是字符串 s 的最后位置。
- i 可以为负数。当索引 i 在字符串 s 的位置在索引 j 的后面时,将返回一个空字符串。
print(string.sub("Hello Lua", 4, 7))
print(string.sub("Hello Lua", 2))
print(string.sub("Hello Lua", 2, 1)) --看到返回什么了吗
print(string.sub("Hello Lua", -3, -1))
-->output
lo L
ello Lua
Lua
如果你只是想对字符串中的 单个字节 进行检查,使用 string.char
函数通常会更为高效。
string.gsub(s, p, r [, n])
将目标字符串 s 中所有的子串 p 替换成字符串 r。
- 可选参数 n,表示限制替换次数。
- 返回值有两个,第一个是被替换后的字符串,第二个是替换了多少次。
print(string.gsub("Lua Lua Lua", "Lua", "hello"))
print(string.gsub("Lua Lua Lua", "Lua", "hello", 2)) --指明第四个参数
-->output
hello hello hello 3
hello hello Lua 2
此函数不能为 LuaJIT 所 JIT 编译,而只能被解释执行。一般我们推荐使用 ngx_lua 模块提供的 ngx.re.gsub
函数。
string.reverse (s)
接收一个字符串 s,返回这个字符串的反转。
print(string.reverse("Hello Lua")) --> output: auL olleH
8.2、Table库
table 库是由一些辅助函数构成的,这些函数将 table 作为数组来操作。
下标从 1 开始
在 Lua 中,数组下标从 1 开始计数。
官方解释:Lua lists have a base index of 1 because it was thought to be most friendly for non-programmers, as it makes indices correspond to ordinal element positions.
确实,对于我们数数来说,总是从 1 开始数的,而从 0 开始对于描述偏移量这样的东西有利。而 Lua 最初设计是一种类似 XML 的数据描述语言,所以索引(index)反应的是数据在里面的位置,而不是偏移量。
在初始化一个数组的时候,若不显式地用 键值对 方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。
local color={first="red", "blue", third="green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
从其他语言过来的开发者会觉得比较坑的一点是,当我们把 table 当作栈或者队列使用的时候,容易犯错:
-
追加到 table 的末尾用的是
s[#s+1] = something
,而不是s[#s] = something
,而且如果这个 something 是一个 nil 的话,会导致这一次压栈(或者入队列)没有存入任何东西,#s
的值没有变。 示例如下:-- 情况 1: 使用 `#str+1`, 要添加的值为“真值”或 false str = {'a', 'b', 'c'} str[#str+1] = 'd' print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (正确的结果) str_len:4 1 a 2 b 3 c 4 d -- 注:false 也是可以正常添加的。 -- 情况 2: 使用 `#str+1`, 要添加的值为 nil str = {'a', 'b', 'c'} str[#str+1] = nil print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (正确的结果) str_len:3 1 a 2 b 3 c -- str 的元素个数没有变化 -- 情况 3: 使用 `#str`, 要添加的值为“真值”或 false str = {'a', 'b', 'c'} str[#str] = 'd' print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (不期望的结果) str_len:3 1 a 2 b 3 d -- str 的元素个数没有变化,但是最后一个元素被覆盖了,并不是期望的添加一个新元素 -- 情况 4: 使用 `#str`, 要添加的值为 nil str = {'a', 'b', 'c'} str[#str] = nil print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (不期望的结果) str_len:2 1 a 2 b -- str 的元素少了一个,不光没有添加反而删除了一个
-
如果
s = { 1, 2, 3, 4, 5, 6 }
,你令s[4] = nil
,#s
会令你“匪夷所思”地变成 3。 示例如下:s = { 1, 2, 3, 4, 5, 6 } s[4] = nil print('s_len:' .. #s) -- 打印出 s 的元素个数 for i,v in ipairs(s) do print(i,v) -- 打印 s 的元素 end -- output: s_len:3 1 1 2 2 3 3
table.getn 获取长度
取长度操作符写作一元操作 #
。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)。
对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 #t
可能是指向任何一个是 nil 值的前一个位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。这也就说明对于有“空洞”的情况,table 的长度存在一定的 不可确定性。
local tblTest1 = { 1, a = 2, 3 }
print("Test1 " .. table.getn(tblTest1))
local tblTest2 = { 1, nil }
print("Test2 " .. table.getn(tblTest2))
local tblTest3 = { 1, nil, 2 }
print("Test3 " .. table.getn(tblTest3))
local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. table.getn(tblTest4))
local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. table.getn(tblTest5))
local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. table.getn(tblTest6))
我们使用 Lua 5.1 和 LuaJIT 2.1 分别执行这个用例,结果如下:
# lua test.lua
Test1 2
Test2 1
Test3 3
Test4 1
Test5 3
Test6 1
# luajit test.lua
Test1 2
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1
这一段的输出结果,就是这么 匪夷所思。请问,你以后还敢在 Lua 的 table 中用 nil 值吗?如果你继续往后面加 nil,你可能会发现点什么。你可能认为你发现的是个规律。但是,你千万不要认为这是个规律,因为这是错误的。
不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。
table.concat (table [, sep [, i [, j ] ] ])
对于元素是 string 或者 number 类型的表 table,返回
table[i]..sep..table[i+1] ··· sep..table[j]
连接成的字符串。填充字符串 sep 默认为空白字符串。起始索引位置 i 默认为 1,结束索引位置 j 默认是 table 的长度。如果 i 大于 j,返回一个空字符串。
local a = {1, 3, 5, "hello" }
print(table.concat(a)) -- output: 135hello
print(table.concat(a, "|")) -- output: 1|3|5|hello
print(table.concat(a, " ", 4, 2)) -- output:
print(table.concat(a, " ", 2, 4)) -- output: 3 5 hello
当需要循环拼接字符时,推荐使用
table.concat
。因为若使用..
在循环中拼接字符,会产生大量的中间字符串。如果拼接的字符串很大,经过多次循环拼接后,其内存开销急剧增大,也会同时触发多次GC
,甚至会导致 Lua 虚拟机内存不足。
local chunk, eof = ngx.arg[1], ngx.arg[2]
if not ngx.ctx.buffer then
ngx.ctx.buffer = ""
end
if eof then
local body = body_filter.transform_json_body(match_t, ngx.ctx.buffer) --calc
ngx.arg[1] = body
else
ngx.ctx.buffer = ngx.ctx.buffer .. chunk
ngx.arg[1] = nil
end
问题现象:当响应 body 较大时,luajit 虚拟机会概率性报错内存不足
not enough memory
,同时 openresty 占用的内存居高不下。
local chunk, eof = ngx.arg[1], ngx.arg[2]
if not ngx.ctx.buffer_table then
ngx.ctx.buffer_table = {}
end
if eof then
local buffer = table.concat(ngx.ctx.buffer_table) -- 使用 table.concat 拼接
local body = body_filter.transform_json_body(match_t, buffer)
ngx.arg[1] = body
else
ngx.arg[1] = nil
table.insert(ngx.ctx.buffer_table, chunk)
end
table.insert (table, [pos ,] value)
在(数组型)表 table 的 pos 索引位置插入 value,其它元素向后移动到空的地方。pos 的默认值是表的长度加一,即默认是插在表的最后。
local a = {1, 8} --a[1] = 1,a[2] = 8
table.insert(a, 1, 3) --在表索引为 1 处插入 3
print(a[1], a[2], a[3])
table.insert(a, 10) --在表的最后插入 10
print(a[1], a[2], a[3], a[4])
-->output
3 1 8
3 1 8 10
table.maxn (table)
返回(数组型)表 table 的最大索引编号;如果此表没有正的索引编号,返回 0。
当长度省略时,此函数通常需要
O(n)
的时间复杂度来计算 table 的末尾。因此用这个函数省略索引位置的调用形式来作 table 元素的末尾追加,是高代价操作。
local a = {}
a[-1] = 10
print(table.maxn(a))
a[5] = 10
print(table.maxn(a))
-->output
0
5
此函数的行为不同于 #
运算符,因为 #
可以返回数组中任意一个 nil 空洞或最后一个 nil 之前的元素索引。当然,该函数的开销相比 #
运算符也会更大一些。
table.remove (table [, pos])
在表 table 中删除索引为 pos(pos 只能是 number 型)的元素,并返回这个被删除的元素,它后面所有元素的索引值都会减一。pos 的默认值是表的长度,即默认是删除表的最后一个元素。
local a = { 1, 2, 3, 4}
print(table.remove(a, 1)) --删除索引为 1 的元素
print(a[1], a[2], a[3], a[4])
print(table.remove(a)) --删除最后一个元素
print(a[1], a[2], a[3], a[4])
-->output
1
2 3 4 nil
4
2 3 nil nil
table.sort (table [, comp])
按照给定的比较函数 comp 给表 table 排序,也就是从 table[1] 到 table[n],这里 n 表示 table 的长度。
比较函数有两个参数,如果希望第一个参数排在第二个的前面,就应该返回 true,否则返回 false。
如果比较函数 comp 没有给出,默认从小到大排序。
local function compare(x, y) --从大到小排序
return x > y --如果第一个参数大于第二个就返回 true,否则返回 false
end
local a = { 1, 7, 3, 4, 25}
table.sort(a) --默认从小到大排序
print(a[1], a[2], a[3], a[4], a[5])
table.sort(a, compare) --使用比较函数进行排序
print(a[1], a[2], a[3], a[4], a[5])
-->output
1 3 4 7 25
25 7 4 3 1
table 其他非常有用的函数
LuaJIT 2.1 新增加的 table.new
和 table.clear
函数是非常有用的。前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。
具体可以参考一下 OpenResty 捆绑的 lua-resty-*
库,里面有些实例可以作为参考。
8.3 、时间日期函数
在 Lua 中,函数
time
、date
和difftime
提供了所有的日期和时间功能。
(推荐)基于缓存的 ngx_lua 时间接口
事实上,在 Nginx/Openresty 中,会经常使用到获取时间操作,通常一次请求最少有几十次获取时间操作,当单核心 RPS/QPS 达到 10K 以上时,获取时间操作往往会达到 200K+量级的调用,是一个非常高频的调用。所以 Nginx 会将时间和日期进行缓存,并非每次调用或每次请求获取时间和日期。
推荐 使用 ngx_lua 模块提供的带缓存的时间接口,如 ngx.today
, ngx.time
, ngx.utctime
,ngx.localtime
, ngx.now
, ngx.http_time
,以及 ngx.cookie_time
等。
ngx.today()
语法:
str = ngx.today()`
该接口从 Nginx 缓存的时间中获取时间,返回当前的时间和日期,其格式为yyyy-mm-dd
(与 Lua 的日期库不同,不涉及系统调用)。
ngx.time()
语法:secs = ngx.time()
该接口从 Nginx 缓存的时间中获取时间,返回当前时间戳的历时秒数(与 Lua 的日期库不同,不涉及系统调用)。
ngx.now()
语法:secs = ngx.now()
该接口从 Nginx 缓存的时间中获取时间,以秒为单位(包括小数部分的毫秒)返回从当前时间戳开始的浮点数(与 Lua 的日期库不同,不涉及系统调用)。
ngx.time() 和 ngx.now() 辨析:ngx.time() 获取到的是秒级时间,ngx.now() 获取到的是毫秒级时间。
ngx.localtime()
语法:str = ngx.localtime()
返回 Nginx 缓存时间的当前时间戳(格式为 yyy-mm-dd hh:mm:ss
)(与 Lua 的日期库不同,不涉及系统调用)。
ngx.utctime()
语法:str = ngx.utctime()
返回 Nginx 缓存时间的当前 UTC 时间戳(格式为 yyyy-mm-dd hh:mm:ss
)(与 Lua 的日期库不同,不涉及系统调用)。
ngx.update_time()
语法:ngx.update_time()
强制更新 Nginx 当前时间缓存。这个调用涉及到一个系统调用,因此有一些开销,所以不要滥用。
获取时间示例代码
ngx.log(ngx.INFO, ngx.today())
ngx.log(ngx.INFO, ngx.time())
ngx.log(ngx.INFO, ngx.now())
ngx.log(ngx.INFO, ngx.localtime())
ngx.log(ngx.INFO, ngx.utctime())
ngx.update_time()
ngx.log(ngx.INFO, ngx.today())
ngx.log(ngx.INFO, ngx.time())
ngx.log(ngx.INFO, ngx.now())
ngx.log(ngx.INFO, ngx.localtime())
ngx.log(ngx.INFO, ngx.utctime())
-->output
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31
2020/12/31 15:37:27 [error] 15851#0: *2153324: 1609400247
2020/12/31 15:37:27 [error] 15851#0: *2153324: 1609400247.704 --**
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31 15:37:27
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31 07:37:27
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31
2020/12/31 15:37:27 [error] 15851#0: *2153324: 1609400247
2020/12/31 15:37:27 [error] 15851#0: *2153324: 1609400247.705 --缓存时间有变化
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31 15:37:27
2020/12/31 15:37:27 [error] 15851#0: *2153324: 2020-12-31 07:37:27
(不推荐) Lua 自带的日期和时间函数
在 OpenResty 的世界里,不推荐 使用这里的标准时间函数,因为这些函数通常会引发不止一个昂贵的系统调用,同时无法为 LuaJIT JIT 编译,对性能造成较大影响。
所以下面的部分函数,简单了解一下即可。
os.time ([table])
- 如果不使用参数 table 调用
time
函数,它会返回 当前 的时间和日期(它表示从某一时刻到现在的秒数)。 - 如果用 table 参数,它会返回一个数字,表示该 table 中所描述的日期和时间(它表示从某一时刻到 table 中描述日期和时间的秒数)。
table 的字段如下:
字段名称 | 取值范围 |
---|---|
year | 四位数字 |
month | 1–12 |
day | 1–31 |
hour | 0–23 |
min | 0–59 |
sec | 0–61 |
isdst | boolean(true 表示夏令时) |
对于 time
函数,如果参数为 table,那么 table 中 必须 含有 year、month、day 字段。其他字段缺省时,默认为中午(12:00:00)。
示例代码:(地点为北京)
print(os.time()) -->output 1438243393
a = { year = 1970, month = 1, day = 1, hour = 8, min = 1 }
print(os.time(a)) -->output 60
os.difftime (t2, t1)
返回 t1 到 t2 的时间差,单位为秒。
local day1 = { year = 2015, month = 7, day = 30 }
local t1 = os.time(day1)
local day2 = { year = 2015, month = 7, day = 31 }
local t2 = os.time(day2)
print(os.difftime(t2, t1)) -->output 86400
os.date ([format [, time]])
把一个表示日期和时间的数值,转换成更高级的表现形式。
- 第一个参数 format 是一个格式化字符串,描述了要返回的时间形式。
- 第二个参数 time 就是日期和时间的数字表示,缺省时默认为当前的时间。
使用格式字符 “*t”,创建一个时间 table。
示例代码:
local tab1 = os.date("*t") --返回一个描述当前日期和时间的表
local ans1 = "{"
for k, v in pairs(tab1) do --把 tab1 转换成一个字符串
ans1 = string.format("%s %s = %s,", ans1, k, tostring(v))
end
ans1 = ans1 .. "}"
print("tab1 = ", ans1)
local tab2 = os.date("*t", 360) --返回一个描述日期和时间数为 360 秒的表
local ans2 = "{"
for k, v in pairs(tab2) do --把 tab2 转换成一个字符串
ans2 = string.format("%s %s = %s,", ans2, k, tostring(v))
end
ans2 = ans2 .. "}"
print("tab2 = ", ans2)
-->output
tab1 = { hour = 17, min = 28, wday = 5, day = 30, month = 7, year = 2015, sec = 10, yday = 211, isdst = false,}
tab2 = { hour = 8, min = 6, wday = 5, day = 1, month = 1, year = 1970, sec = 0, yday = 1, isdst = false,}
该表中除了使用到了 time
函数参数 table 的字段外,这还提供了星期(wday,星期天为 1)和一年中的第几天(yday,一月一日为 1)。
除了使用 “*t” 格式字符串外,如果使用带标记(见下表)的特殊字符串,os.date
函数会将相应的标记位以时间信息进行填充,得到一个包含时间的字符串。
表如下:
格式字符 | 含义 |
---|---|
%a | 一星期中天数的简写(例如:Wed) |
%A | 一星期中天数的全称(例如:Wednesday) |
%b | 月份的简写(例如:Sep) |
%B | 月份的全称(例如:September) |
%c | 日期和时间(例如:07/30/15 16:57:24) |
%d | 一个月中的第几天 [01 ~ 31] |
%H | 24 小时制中的小时数 [00 ~ 23] |
%I | 12 小时制中的小时数 [01 ~ 12] |
%j | 一年中的第几天 [001 ~ 366] |
%M | 分钟数 [00 ~ 59] |
%m | 月份数 [01 ~ 12] |
%p | “上午(am)”或“下午(pm)” |
%S | 秒数 [00 ~ 59] |
%w | 一星期中的第几天 [1 ~ 7 = 星期天 ~ 星期六] |
%x | 日期(例如:07/30/15) |
%X | 时间(例如:16:57:24) |
%y | 两位数的年份 [00 ~ 99] |
%Y | 完整的年份(例如:2015) |
%% | 字符’%’ |
示例代码:
print(os.date("today is %A, in %B"))
print(os.date("now is %x %X"))
-->output
today is Thursday, in July
now is 07/30/15 17:39:22
8.4、 Numerals库
数学库:Lua 数学库由一组标准的数学函数构成。数学库的引入丰富了 Lua 编程语言的功能,同时也方便了程序的编写。常用数学函数见下表:
函数名 | 函数功能 |
---|---|
math.rad(x) | 角度x转换成弧度 |
math.deg(x) | 弧度x转换成角度 |
math.max(x, …) | 返回参数中值最大的那个数,参数必须是number型 |
math.min(x, …) | 返回参数中值最小的那个数,参数必须是number型 |
math.random ([m [, n]]) | 不传入参数时,返回 一个在区间[0,1)内均匀分布的伪随机实数; 只使用一个整数参数m时,返回一个在区间[1, m]内均匀分布的伪随机整数; 使用两个整数参数时,返回一个在区间[m, n]内均匀分布的伪随机整数 |
math.randomseed (x) | 为伪随机数生成器设置一个种子x,相同的种子将会生成相同的数字序列 |
math.abs(x) | 返回x的绝对值 |
math.fmod(x, y) | 返回 x对y取余数 |
math.pow(x, y) | 返回x的y次方 |
math.sqrt(x) | 返回x的算术平方根 |
math.exp(x) | 返回自然数e的x次方 |
math.log(x) | 返回x的自然对数 |
math.log10(x) | 返回以10为底,x的对数 |
math.floor(x) | 返回最大且不大于x的整数(即向下取整,舍弃小数部分) |
math.ceil(x) | 返回最小且不小于x的整数(即向上取整,取比它大的最小整数) |
math.pi | 圆周率 |
math.sin(x) | 求弧度x的正弦值 |
math.cos(x) | 求弧度x的余弦值 |
math.tan(x) | 求弧度x的正切值 |
math.asin(x) | 求x的反正弦值 |
math.acos(x) | 求x的反余弦值 |
math.atan(x) | 求x的反正切值 |
示例代码:
print(math.pi) -->output 3.1415926535898
print(math.rad(180)) -->output 3.1415926535898
print(math.deg(math.pi)) -->output 180
print(math.sin(1)) -->output 0.8414709848079
print(math.cos(math.pi)) -->output -1
print(math.tan(math.pi / 4)) -->output 1
print(math.atan(1)) -->output 0.78539816339745
print(math.asin(0)) -->output 0
print(math.max(-1, 2, 0, 3.6, 9.1)) -->output 9.1
print(math.min(-1, 2, 0, 3.6, 9.1)) -->output -1
print(math.fmod(10.1, 3)) -->output 1.1
print(math.sqrt(360)) -->output 18.97366596101
print(math.exp(1)) -->output 2.718281828459
print(math.log(10)) -->output 2.302585092994
print(math.log10(10)) -->output 1
print(math.floor(3.1415)) -->output 3
print(math.ceil(7.998)) -->output 8
另外使用 math.random()
函数获得伪随机数时,如果不使用 math.randomseed()
设置伪随机数生成种子或者设置相同的伪随机数生成种子,那么得得到的伪随机数序列是一样的。
示例代码:
math.randomseed (100) --把种子设置为100
print(math.random()) -->output 0.0012512588885159
print(math.random(100)) -->output 57
print(math.random(100, 360)) -->output 150
稍等片刻,再次运行上面的代码。
math.randomseed (100) --把种子设置为100
print(math.random()) -->output 0.0012512588885159
print(math.random(100)) -->output 57
print(math.random(100, 360)) -->output 150
两次运行的结果一样。为了避免每次程序启动时得到的都是相同的伪随机数序列,通常是使用当前时间作为种子。
修改上例中的代码:
math.randomseed (os.time()) --把100换成os.time()
print(math.random()) -->output 0.88369396038697
print(math.random(100)) -->output 66
print(math.random(100, 360)) -->output 228
稍等片刻,再次运行上面的代码。
math.randomseed (os.time()) --把100换成os.time()
print(math.random()) -->output 0.88946195867794
print(math.random(100)) -->output 68
print(math.random(100, 360)) -->output 129
8.5、文件操作
Lua I/O 库提供两种不同的方式处理文件:
- 隐式文件描述
- 显式文件描述。
隐式文件描述
设置一个默认的输入或输出文件,然后在这个文件上进行所有的输入或输出操作。所有的操作函数由 io 表提供。
打开已经存在的
test1.txt
文件,并读取里面的内容
file = io.input("test1.txt") -- 使用 io.input() 函数打开文件
repeat
line = io.read() -- 逐行读取内容,文件结束时返回 nil
if nil == line then
break
end
print(line)
until(false)
io.close(file) -- 关闭文件
--> output
my test file
hello
lua
在
test1.txt
文件的最后添加一行 “hello world”
file = io.open("test1.txt", "a+") -- 使用 io.open() 函数,以添加模式打开文件
io.output(file) -- 使用 io.output() 函数,设置默认输出文件
io.write("\nhello world") -- 使用 io.write() 函数,把内容写到文件
io.close(file)
在相应目录下打开 test1.txt
文件,查看文件内容发生的变化。
显式文件描述
使用 file:XXX()
函数方式进行操作,其中 file
为 io.open()
返回的文件句柄。
打开已经存在的
test2.txt
文件,并读取里面的内容
file = io.open("test2.txt", "r") -- 使用 io.open() 函数,以只读模式打开文件
for line in file:lines() do -- 使用 file:lines() 函数逐行读取文件
print(line)
end
file:close()
-->output
my test2
hello lua
在
test2.txt
文件的最后添加一行 “hello world”
file = io.open("test2.txt", "a") -- 使用 io.open() 函数,以添加模式打开文件
file:write("\nhello world") -- 使用 file:write() 函数,在文件末尾追加内容
file:close()
在相应目录下打开 test2.txt
文件,查看文件内容发生的变化。
文件操作函数
io.open(filename [, mode])
按指定的模式(mode),打开一个文件名为 filename
的文件,成功则返回文件句柄,失败则返回 nil 加错误信息。
模式如下表所示:
模式 | 含义 | 文件不存在时 |
---|---|---|
“r” | 读模式(默认) | 返回 nil 加错误信息 |
“w” | 写模式 | 创建文件 |
“a” | 添加模式 | 创建文件 |
“r+” | 更新模式,保存之前的数据 | 返回 nil 加错误信息 |
“w+” | 更新模式,清除之前的数据 | 创建文件 |
“a+” | 添加更新模式,保存之前的数据,在文件尾进行添加 | 创建文件 |
模式字符串后面可以有一个 ‘b’,用于在某些系统中打开二进制文件。
请注意 “w” 和 “wb” 的区别:
- “w” 表示文本文件(使用 “w”,其属性要看所在的平台)。
- Linux 的文件系统认为 0x0A 为文本文件的换行符,Windows 的文件系统认为 0x0D0A 为文本文件的换行符。
- 为了兼容其他文件系统(如从 Linux 拷贝来的文件),Windows 的文件系统在写文件时,会在文件中 0x0A 的前面加上 0x0D。
- “wb” 表示二进制文件。
- 文件系统会按纯粹的二进制格式进行写操作,因此也就不存在格式转换的问题。
Linux 文件系统下 “w” 和 “wb” 没有区别。
file:close()
关闭文件。
注意:当文件句柄被垃圾收集后,文件将自动关闭。句柄将变为一个不可预知的值。
io.close([file])
关闭文件。
此函数和
file:close()
的作用相同。没有参数 file 时,关闭默认输出文件。
file:flush()
把写入缓冲区的所有数据写入到文件 file 中。
io.flush()
相当于 file:flush()
,把写入缓冲区的所有数据写入到默认输出文件。
io.input([file])
参数 file
的格式如下:
- 当使用一个 文件名 调用时,打开这个文件(以文本模式),并设置文件句柄为默认输入文件;
- 当使用一个 文件句柄 调用时,设置此文件句柄为默认输入文件;
- 当不使用参数调用时,返回默认输入文件句柄。
file:lines()
返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil
,但不关闭文件。
io.lines([filename])
参数 filename
的格式如下:
- 打开指定的文件
filename
为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回nil
,并自动关闭文件。 - 若不带参数时
io.lines()
等价于io.input():lines()
读取默认输入设备的内容,结束时不关闭文件。
io.output([file])
类似于 io.input
,但操作在默认输出文件上。
file:read(…)
按指定的格式读取一个文件。
按每个格式将返回一个字符串或数字,如果不能正确读取则返回 nil
,若没有指定格式,则默认按行进行读取。
格式如下表所示:
格式 | 含义 |
---|---|
“*n” | 读取一个数字 |
“*a” | 从当前位置读取整个文件。若当前位置为文件尾,则返回空字符串。 |
“*l” | 读取下一行的内容。若为文件尾,则返回 nil。(默认) |
number | 读取指定字节数的字符。如果 number 为 0,则返回空字符串;若为文件尾,则返回 nil。 |
io.read(…)
相当于 io.input():read
io.type(obj)
检测 obj 是否是一个可用的文件句柄:
- 如果 obj 是一个打开的文件句柄,则返回 “file”;
- 如果 obj 是一个已关闭的文件句柄,则返回 “closed file”;
- 如果 obj 不是一个文件句柄,则返回
nil
。
file:write(…)
把每一个参数的值写入文件。参数必须为字符串或数字,若要输出其它值,则需通过 tostring
或 string.format
进行转换。
io.write(…)
相当于 io.output():write
。
file:seek([whence] [, offset])
设置和获取当前文件位置,成功则返回最终的文件位置(按字节,相对于文件开头),失败则返回 nil 加错误信息。
缺省时,whence
默认为 “cur”,offset
默认为 0 。
参数 whence
的含义如下表所示:
whence | 含义 |
---|---|
“set” | 文件开始 |
“cur” | 文件当前位置(默认) |
“end” | 文件结束 |
file:setvbuf(mode [, size])
设置输出文件的缓冲模式。
模式入下表所示:
模式 | 含义 |
---|---|
“no” | 没有缓冲,即直接输出。 |
“full” | 全缓冲,即当缓冲满后才进行输出操作(也可调用 flush 马上输出)。 |
“line” | 以行为单位,进行输出。 |
最后两种模式,size
可以指定缓冲的大小(按字节),忽略 size
将自动调整为最佳的大小。