loong - Lua - 基础详解

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、逻辑运算符

逻辑运算符列表

序号逻辑运算符说明
1and逻辑与
2or逻辑或
3not逻辑非

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^
2not   # -(负号)
3*   /   %
4+   -(减法)
5
6< > <=  >=  ==  ~=
7and
8or

示例

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、条件结构

使用ifelse、和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 型控制结构语法如下,当表达式值为假(即 falsenil)时结束循环。也可以使用 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、varbegin 变化到 finish,每次变化都以 step 作为步长递增 var
  • 2、beginfinishstep 三个表达式只会在循环开始时执行一次;
  • 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 比较简单,但其实它是非常强大的。通过不同的迭代器,几乎可以遍历所有的东西,而且写出的代码极具可读性。

标准库提供了几种迭代器,包括:

  1. 用于迭代文件中每行的 io.lines()
  2. 迭代 table 元素的 pairs()
  3. 迭代数组元素的 ipairs()
  4. 迭代字符串中单词的 string.gmatch() 等。

泛型 for 循环与数字型 for 循环有两个相同点:

  1. 循环变量是循环体的局部变量;
  2. 决不应该对循环变量作任何赋值。

对于泛型 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 用来终止 whilerepeatfor 三种循环的执行,并跳出当前循环体,继续执行当前循环之后的语句。下面举一个 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

在定义函数时要注意几点:

  1. 利用名字来解释函数、变量的意图,使人通过名字就能看出来函数、变量的作用。
  2. 每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。
  3. 让代码自己说话,不需要注释最好。

由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 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))
  1. 你要调用的函数参数是未知的;
  2. 函数的实际参数的类型和数目也都是未知的。

伪代码

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.newtable.clear 函数是非常有用的。前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。
具体可以参考一下 OpenResty 捆绑的 lua-resty-* 库,里面有些实例可以作为参考。

8.3 、时间日期函数

在 Lua 中,函数 timedatedifftime 提供了所有的日期和时间功能。

(推荐)基于缓存的 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四位数字
month1–12
day1–31
hour0–23
min0–59
sec0–61
isdstboolean(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]
%H24 小时制中的小时数 [00 ~ 23]
%I12 小时制中的小时数 [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() 函数方式进行操作,其中 fileio.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(…)

把每一个参数的值写入文件。参数必须为字符串或数字,若要输出其它值,则需通过 tostringstring.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 将自动调整为最佳的大小。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7-Moon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值