lua基础入门教程

我穿过思念齐眉的年代,手指苍白。是时光腰身的流年,像水一样蔓过四季。你仍像是没有归航的船

Posted by yishuifengxiao on 2022-07-04

参考手册地址 https://www.runoob.com/manual/lua53doc/

Lua 5.3 参考手册目录 https://www.runoob.com/manual/lua53doc/contents.html#contents

Lua 5.3 参考手册索引 https://www.runoob.com/manual/lua53doc/contents.html#index

一 基础环境准备

1.1 环境安装

1.1.1 linux环境安装

1
2
3
4
5
curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test
make install

如果安装的时候报错: lua.c:80:31: fatal error: readline/readline.h: No such file or directory

解决方法: 缺少libreadline-dev依赖包

centos 系统: yum install readline-devel
debian 系统: apt-get install libreadline-dev

1.1.2 Window 系统上安装 Lua

window下你可以使用一个叫”SciTE”的IDE环境来执行lua程序,下载地址为:

双击安装后即可在该环境下编写 Lua 程序并运行。

你也可以使用 Lua 官方推荐的方法使用 LuaDist:http://luadist.org/

接下来我们创建一个 helloWorld.lua:

1
print("Hello World!")

执行以下命令:
1
$ lua helloWorld

输出结果为:
1
Hello World!

1.2 Lua 基本语法

1.2.1 交互式编程

Lua 提供了交互式编程模式。我们可以在命令行中输入程序并立即查看效果。

Lua 交互式编程模式可以通过命令 lua -i 或 lua 来启用:

1
2
3
$ lua -i 
$ Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio
>

在命令行中,输入以下命令:

1
> print("Hello World!")

接着我们按下回车键,输出结果如下:

1
2
3
> print("Hello World!")
Hello World!
>

1.2.2 脚本式编程

我们可以将 Lua 程序代码保持到一个以 lua 结尾的文件,并执行,该模式称为脚本式编程,如我们将如下代码存储在名为 hello.lua 的脚本文件中:

1
2
print("Hello World!")
print("www.w3cschool.cn")

使用 lua 名执行以上脚本,输出结果为:

1
2
3
$ lua test.lua
Hello World!
www.w3cschool.cn

我们也可以将代码修改为如下形式来执行脚本(在开头添加:#!/usr/local/bin/lua):

1
2
3
4
#!/usr/local/bin/lua

print("Hello World!")
print("www.w3cschool.cn")

以上代码中,我们指定了 Lua 的解释器 /usr/local/bin directory。加上 # 号标记解释器会忽略它。接下来我们为脚本添加可执行权限,并执行:

1
2
3
./test.lua 
Hello World!
www.w3cschool.cn

1.2.3 注释

1.2.3.1 单行注释

两个减号是单行注释:

1
--

1.2.3.2 多行注释

1
2
3
4
--[[
多行注释
多行注释
--]]

1.2.4 标示符

Lua 标示符用于定义一个变量,函数获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上0个或多个字母,下划线,数字(0到9)。

最好不要使用下划线加大写字母的标示符,因为Lua的保留字也是这样的。

Lua 不允许使用特殊字符如 @, $, 和 % 来定义标示符。 Lua 是一个区分大小写的编程语言。因此在 Lua 中 W3c 与 w3c 是两个不同的标示符。以下列出了一些正确的标示符:

1
2
mohd         zara      abc     move_name    a_123
myname50 _temp j a23b9 retVal

1.2.5 关键词

以下列出了 Lua 的保留关键字。保留关键字不能作为常量或变量或其他用户自定义标示符:

and break do else
elseif end false for
function if in local
nil not or repeat
return then true until
while

一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量。


1.2.6 全局变量

在默认情况下,变量总是认为是全局的。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

1
2
3
4
5
6
> print(b)
nil
> b=10
> print(b)
10
>

如果你想删除一个全局变量,只需要将变量赋值为nil。

1
2
b = nil
print(b) --> nil

这样变量b就好像从没被使用过一样。换句话说, 当且仅当一个变量不等于nil时,这个变量即存在。

二 lua基础知识

2.1 Lua 数据类型

Lua是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua中有8个基本类型分别为:nil、boolean、number、string、userdata、function、thread和table。

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。

我们可以使用type函数测试给定变量或者值的类型:

1
2
3
4
5
6
7
print(type("Hello world"))      --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type(X))) --> string

2.1.1 nil(空)

nil 类型表示一种没有任何有效值,它只有一个值 — nil,例如打印一个没有赋值的变量,便会输出一个 nil 值:

1
2
3
> print(type(a))
nil
>

对于全局变量和 table,nil 还有一个”删除”作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉,执行下面代码就知:

1
2
3
4
5
6
7
8
9
tab1 = { key1 = "val1", key2 = "val2", "val3" }
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end

tab1.key1 = nil
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end

使用 nil 作比较时应该加上双引号:

1
2
3
4
5
6
7
> type(X)
nil
> type(X)==nil
false
> type(X)=="nil"
true
>

2.1.2 boolean(布尔)

boolean 类型只有两个可选值:true(真) 和 false(假),Lua 把 false 和 nil 看作是”假”,其他的都为”真”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print(type(true))
print(type(false))
print(type(nil))

if false or nil then
print("至少有一个是 true")
else
print("false 和 nil 都为 false")
end

if 0 then
print("数字 0 是 true")
else
print("数字 0 为 false")
end

以上代码执行结果如下:

1
2
3
4
$ lua test.lua 
boolean
boolean
nilfalse和nil都为false数字0是true

2.1.3 number(数字)

Lua 默认只有一种 number 类型 — double(双精度)类型(默认类型可以修改 luaconf.h 里的定义),以下几种写法都被看作是 number 类型:

1
2
3
4
5
6
print(type(2))
print(type(2.2))
print(type(0.2))
print(type(2e+1))
print(type(0.2e-1))
print(type(7.8263692594256e-06))

以上代码执行结果:

1
2
3
4
5
6
number
number
number
number
number
number

2.1.4 string(字符串)

字符串由一对双引号或单引号来表示。

1
2
string1 = "this is string1"
string2 = 'this is string2'

也可以用 2 个方括号 “[[]]” 来表示”一块”字符串。

1
2
3
4
5
6
7
8
9
html = [[
<html>
<head></head>
<body>
<a href="//www.w3cschool.cn/">w3cschoolW3Cschool教程</a>
</body>
</html>
]]
print(html)

以下代码执行结果为:

1
2
3
4
5
6
<html>
<head></head>
<body>
<a href="//www.w3cschool.cn/">w3cschoolW3Cschool教程</a>
</body>
</html>

在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> print("2" + 6)
8.0
> print("2" + "6")
8.0
> print("2 + 6")
2 + 6
> print("-2e2" * "6")
-1200.0
> print("error" + 1)
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
stdin:1: in main chunk
[C]: in ?
>

以上代码中”error” + 1执行报错了,字符串连接使用的是 .. ,如:

1
2
3
4
5
> print("a" .. 'b')
ab
> print(157 .. 428)
157428
>

使用 # 来计算字符串的长度,放在字符串前面,如下实例:

1
2
3
4
5
6
> len = "www.w3cschool.cn"
> print(#len)
16
> print(#"www.w3cschool.cn")
16
>

2.1.5 table(表)

在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。也可以在表里添加一些数据,直接初始化表:

1
2
3
4
5
-- 创建一个空的 table
local tbl1 = {}

-- 直接初始表
local tbl2 = {"apple", "pear", "orange", "grape"}

Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字或者是字符串。

1
2
3
4
5
6
7
8
9
-- table_test.lua 脚本文件
a = {}
a["key"] = "value"
key = 10
a[key] = 22
a[key] = a[key] + 11
for k, v in pairs(a) do
print(k .. " : " .. v)
end

脚本执行结果为:

1
2
3
$ lua table_test.lua 
key : value
10 : 33

不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认初始索引一般以 1 开始。

1
2
3
4
5
-- table_test2.lua 脚本文件
local tbl = {"apple", "pear", "orange", "grape"}
for key, val in pairs(tbl) do
print("Key", key)
end

脚本执行结果为:

1
2
3
4
5
$ lua table_test2.lua 
Key 1
Key 2
Key 3
Key 4

table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。

1
2
3
4
5
6
7
8
-- table_test3.lua 脚本文件
a3 = {}
for i = 1, 10 do
a3[i] = i
end
a3["key"] = "val"
print(a3["key"])
print(a3["none"])

脚本执行结果为:

1
2
3
$ lua table_test3.lua 
val
nil

2.1.6 function(函数)

在 Lua 中,函数是被看作是”第一类值(First-Class Value)”,函数可以存在变量里:

1
2
3
4
5
6
7
8
9
10
11
-- function_test.lua 脚本文件
function factorial1(n)
if n == 0 then
return 1
else
return n * factorial1(n - 1)
end
end
print(factorial1(5))
factorial2 = factorial1
print(factorial2(5))

脚本执行结果为:

1
2
3
$ lua function_test.lua 
120
120

function 可以以匿名函数(anonymous function)的方式通过参数传递:

1
2
3
4
5
6
7
8
9
10
-- function_test2.lua 脚本文件
function anonymous(tab, fun)
for k, v in pairs(tab) do
print(fun(k, v))
end
end
tab = { key1 = "val1", key2 = "val2" }
anonymous(tab, function(key, val)
return key .. " = " .. val
end)

脚本执行结果为:

1
2
3
$ lua function_test2.lua 
key1 = val1
key2 = val2

2.1.7 thread(线程)

在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。

线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。


2.1.8 userdata(自定义类型)

userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和 指针)存储到 Lua 变量中调用。

2.2 Lua 变量

变量在使用前,必须在代码中进行声明,即创建该变量。编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。

Lua 变量有三种类型:全局变量、局部变量、表中的域。

Lua 中的变量全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量。

局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- test.lua 文件脚本
a = 5 -- 全局变量
local b = 5 -- 局部变量

function joke()
c = 5 -- 全局变量
local d = 6 -- 局部变量
end

joke()
print(c,d) --> 5 nil

do
local a = 6 -- 局部变量
b = 6 -- 全局变量
print(a,b); --> 6 6
end

print(a,b) --> 5 6

执行以上实例输出结果为:

1
2
3
4
$ lua test.lua 
5 nil
6 6
5 6

2.2.1 赋值语句

赋值是改变一个变量的值和改变表域的最基本的方法。

1
2
a = "hello" .. "world"
t.n = t.n + 1

Lua可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。

1
a, b = 10, 2*x       <-->       a=10; b=2*x

遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:

1
2
x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i] -- swap 'a[i]' for 'a[i]'

当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:

1
2
a. 变量个数 > 值的个数             按变量个数补足nil
b. 变量个数 < 值的个数 多余的值会被忽略

例如:

1
2
3
4
5
6
7
8
a, b, c = 0, 1
print(a,b,c) --> 0 1 nil

a, b = a+1, b+1, b+2 -- value of b+2 is ignored
print(a,b) --> 1 2

a, b, c = 0
print(a,b,c) --> 0 nil nil

上面最后一个例子是一个常见的错误情况,注意:如果要对多个变量赋值必须依次对每个变量赋值。

1
2
a, b, c = 0, 0, 0
print(a,b,c) --> 0 0 0

多值赋值经常用来交换变量,或将函数调用返回给变量:

1
a, b = f()

f()返回两个值,第一个赋给a,第二个赋给b。

应该尽可能的使用局部变量,有两个好处:

  • \1. 避免命名冲突。
  • \2. 访问局部变量的速度比全局变量更快。

2.2.2 索引

对 table 的索引使用方括号 []。Lua 也提供了 . 操作。

1
2
3
t[i]
t.i -- 当索引为字符串类型时的一种简化写法
gettable_event(t,i) -- 采用索引访问本质上是一个类似这样的函数调用

例如:

1
2
3
4
5
6
> site = {}
> site["key"] = "www.w3cschool.cn"
> print(site["key"])
www.w3cschool.cn
> print(site.key)
www.w3cschool.cn

2.3 Lua 常用数据结构

Lua中的table不是一种简单的数据结构,它可以作为其它数据结构的基础。如数组、记录、线性表、队列和集合等,在Lua中都可以通过table来表示。

2.3.1 数组

在lua中通过整数下标访问表中的元素即可简单的实现数组。并且数组不必事先指定大小,大小可以随需要动态的增长。

1
2
3
4
5
6
7
8
a = {}
for i = 1,100 do
a[i] = 0
end
print("The length of array 'a' is " .. #a)

squares = {1, 4, 9, 16, 25}
print("The length of array 'a' is " .. #squares)

在Lua中习惯上数组的下表从1开始,Lua的标准库与此习惯保持一致,因此如果你的数组下标也是从1开始你就可以直接使用标准库的函数,否则就无法直接使用。

2.3.2 二维数组

Lua中主要有两种表示矩阵的方法,第一种是用数组的数组表示。也就是说一个表的元素是另一个表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local N = 3
local M = 3
mt = {}
for i = 1,N do
mt[i] = {}
for j = 1,M do
mt[i][j] = i * j
end
end

mt = {}
for i = 1, N do
for j = 1, M do
mt[(i - 1) * M + j] = i * j
end
end

2.3.3 链表

Lua中用tables很容易实现链表,每一个节点是一个table,指针是这个表的一个域,并且指向另一个节点(table)。例如,要实现一个只有两个域:值和指针的基本链表,代码如下:

1
2
3
4
5
6
7
8
9
10
list = nil
for i = 1, 10 do
list = { next = list ,value = i}
end

local l = list
while l do
--print(l.value)
l = l.next
end

2.3.4 队列与双向队列

虽然可以使用Lua的table库提供的insert和remove操作来实现队列,但这种方式实现的队列针对大数据量时效率太低,有效的方式是使用两个索引下标,一个表示第一个元素,另一个表示最后一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
List = {}

--创建
function List.new()
return {first = 0,last = -1}
end

--队列头插入
function List.pushFront(list,value)
local first = list.first - 1
list.first = first
list[first] = value
end

--队列尾插入
function List.popFront(list)
local first = list.first
if first > list.last then
error("List is empty")
end

local value = list[first]
list[first] = nil
list.first = first + 1
return value
end

function List.popBack(list)
local last = list.last
if list.first > last then
error("List is empty")
end
local value = list[last]
list[last] = nil
list.last = last - 1
return value
end

--测试代码
local testList = {first = 0,last = -1}
local tableTest = 12

List.pushFront(testList,tableTest)
print( List.popFront(testList))

2.3.5 栈

简单实现堆栈功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
local stackMng = {}
stackMng.__index = stackMng

function stackMng:new()
local temp = {}
setmetatable(temp,stackMng)
return temp
end

function stackMng:init()
self.stackList = {}
end

function stackMng:reset()
self:init()
end

function stackMng:clear()
self.stackList = {}
end

function stackMng:pop()
if #self.stackList == 0 then
return
end
if self.stackList[1] then
print(self.stackList[1])
end

return table.remove(self.stackList,1)
end

function stackMng:push(t)
table.insert(self.stackList,t)
end

function stackMng:Count()
return #self.stackList
end

--测试代码
object = stackMng:new()
object:init()
object:push(1)
object:pop()

2.3.6 集合

在Lua中用table实现集合是非常简单的,见如下代码:

1
2
3
4
5
6
7
8
reserved = {
["while"] = true, ["end"] = true,
["function"] = true, ["local"] = true,
}

for k,v in pairs(reserved) do
print(k,"->",v)
end

三 Lua 循环与流程判断

操作符 描述 实例
== 等于,检测两个值是否相等,相等返回 true,否则返回 false (A == B) 为 false。
~= 不等于,检测两个值是否相等,不相等返回 true,否则返回 false (A ~= B) 为 true。
> 大于,如果左边的值大于右边的值,返回 true,否则返回 false (A > B) 为 false。
< 小于,如果左边的值大于右边的值,返回 false,否则返回 true (A < B) 为 true。
>= 大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false (A >= B) 返回 false。
<= 小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false (A <= B) 返回 true。

~= 不等于,检测两个值是否相等,不相等返回 true,否则返回 false (A ~= B) 为 true


很多情况下我们需要做一些有规律性的重复操作,因此在程序中就需要重复执行某些语句。

一组被重复执行的语句称之为循环体,能否继续重复,决定循环的终止条件。

循环结构是在一定条件下反复执行某段程序的流程结构,被反复执行的程序被称为循环体。

循环语句是由循环体及循环的终止条件两部分组成的。

lua循环

Lua 语言提供了以下几种循环处理方式:

循环类型 描述
while 循环 在条件为 true 时,让程序重复地执行某些语句。执行语句前会先检查条件是否为 true。
for 循环 重复执行指定语句,重复次数可在 for 语句中控制。
Lua repeat…until 重复执行循环,直到 指定的条件为真时为止
循环嵌套 可以在循环内嵌套一个或多个循环语句(while、for、do..while)

循环控制语句

循环控制语句用于控制程序的流程, 以实现程序的各种结构方式。

Lua 支持以下循环控制语句:

控制语句 描述
break 语句 退出当前循环或语句,并开始脚本执行紧接着的语句。

3.1 while 循环

3.1.1 语法

Lua 编程语言中 while 循环语法:

1
2
3
4
while(condition)
do
statements
end

statements(循环体语句) 可以是一条或多条语句,condition(条件) 可以是任意表达式,在 condition(条件) 为 true 时执行循环体语句。

流程图如下:

Lua while 循环

在以上流程图中我们可以看出在condition(条件)为 false 时会跳过当前循环并开始脚本执行紧接着的语句。

注:该循环结构又被称之为当型循环,可以理解为

3.1.2 实例

以下实例循环输出 a 的值:

1
2
3
4
5
6
a=10
while( a < 20 )
do
print("a 的值为:", a)
a = a+1
end

执行以上代码,输出结果如下:

1
2
3
4
5
6
7
8
9
10
a 的值为:	10
a 的值为: 11
a 的值为: 12
a 的值为: 13
a 的值为: 14
a 的值为: 15
a 的值为: 16
a 的值为: 17
a 的值为: 18
a 的值为: 19

在循环体中如果条件永远为 true 循环语句就会永远执行下去,以下以 while 循环为例:

1
2
3
4
while( true )
do
print("循环将永远执行下去")
end

3.2 Lua for 循环

Lua 编程语言中 for 循环语句可以重复执行指定语句,重复次数可在 for 语句中控制。

Lua 编程语言中 for语句有两大类::

  • 数值for循环
  • 泛型for循环

3.2.1 数值for循环

3.2.1.1 语法格式

Lua 编程语言中数值for循环语法格式:

1
2
3
for var=exp1,exp2,exp3 do  
<执行体>
end

var从exp1变化到exp2,每次变化以exp3为步长递增var,并执行一次”执行体”。exp3是可选的,如果不指定,默认为1。

3.2.1.2 实例

1
2
3
4
5
6
7
for i=1,f(x) do
print(i)
end

for i=10,1,-1 do
print(i)
end

for的三个表达式在循环开始前一次性求值,以后不再进行求值。比如上面的f(x)只会在循环开始前执行一次,其结果用在后面的循环中。

验证如下:

1
2
3
4
5
6
7
#!/usr/local/bin/lua  
function f(x)
print("function")
return x*2
end
for i=1,f(5) do print(i)
end

以上实例输出结果为:

1
2
3
4
5
6
7
8
9
10
11
function
1
2
3
4
5
6
7
8
9
10

可以看到 函数f(x)只在循环开始前执行一次。


3.2.2 泛型for循环

3.2.2.1 语法

泛型for循环通过一个迭代器函数来遍历所有值,类似java中的foreach语句。

Lua 编程语言中泛型for循环语法格式:

1
2
3
4
--打印数组a的所有值  
for i,v in ipairs(a)
do print(v)
end

i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。

3.2.2.2 实例

循环数组 days:

1
2
3
#!/usr/local/bin/lua  
days = {"Suanday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
for i,v in ipairs(days) do print(v) end

以上实例输出结果为:

1
2
3
4
5
6
7
Suanday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

3.3 Lua repeat…until 循环

Lua 编程语言中 repeat…until 循环语句不同于 for 和 while循环,for 和 while循环的条件语句在当前循环执行开始时判断,而 repeat…until 循环的条件语句在当前循环结束后判断。

3.3.1 语法

Lua 编程语言中 repeat…until 循环语法格式:

1
2
3
repeat
statements
until( condition )

repeat…until 是条件后行,所以repeat…until 的循环体里面至少要运行一次。

statements(循环体语句) 可以是一条或多条语句,condition(条件) 可以是任意表达式,在 condition(条件) 为 false 时执行循环体语句。

condition(条件)为 true 时会跳过当前循环并开始脚本执行紧接着的语句。

Lua repeat…until 循环流程图如下:

Lua repeat...until 循环

注:该循环结构又被称为直到型循环,可以简单的理解为:直到condition为真才跳出循环

3.3.2 实例

1
2
3
4
5
6
7
--[ 变量定义 --]
a = 10
--[ 执行循环 --]
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )

执行以上代码,程序输出结果为:

1
2
3
4
5
6
a的值为:	10
a的值为: 11
a的值为: 12
a的值为: 13
a的值为: 14
a的值为: 15

3.4 Lua 循环嵌套

Lua 编程语言中允许循环中嵌入循环。以下实例演示了 Lua 循环嵌套的应用。

3.4.1 语法

Lua 编程语言中 for 循环嵌套语法格式:

1
2
3
4
5
6
7
8
for init,max/min value, increment
do
for init,max/min value, increment
do
statements
end
statements
end

Lua 编程语言中 while 循环嵌套语法格式:

1
2
3
4
5
6
7
8
while(condition)
do
while(condition)
do
statements
end
statements
end

Lua 编程语言中 repeat…until 循环嵌套语法格式:

1
2
3
4
5
6
repeat
statements
repeat
statements
until( condition )
until( condition )

除了以上同类型循环嵌套外,我们还可以使用不同的循环类型来嵌套,如 for 循环体中嵌套 while 循环。

3.4.2 实例

以下实例使用了for循环嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
j =2
for i=2,10 do
for j=2,(i/j) , 2 do
if(not(i%j))
then
break
end
if(j > (i/j))then
print("i 的值为:",i)
end
end
end

以上代码执行结果为:

1
2
3
i 的值为:	8
i 的值为: 9
i 的值为: 10

3.5 流程控制

控制结构的条件表达式结果可以是任何值,Lua认为false和nil为假,true和非nil为真。

要注意的是Lua中 0 为 true:

1
2
3
4
5
--[ 0 为 true ]
if(0)
then
print("0 为 true")
end

以上代码输出结果为:

1
0 为 true

Lua 提供了以下控制结构语句:

语句 描述
if 语句 if 语句 由一个布尔表达式作为条件判断,其后紧跟其他语句组成。
if…else 语句 if 语句 可以与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码。
if 嵌套语句 你可以在ifelse if中使用一个或多个 ifelse if 语句 。

3.5.1 if 语句

Lua if 语句 由一个布尔表达式作为条件判断,其后紧跟其他语句组成。

Lua if 语句语法格式如下:

1
2
3
4
if(布尔表达式)
then
--[ 在布尔表达式为 true 时执行的语句 --]
end

实例

以下实例用于判断变量 a 的值是否小于 20:

1
2
3
4
5
6
7
8
9
10
--[ 定义变量 --]
a = 10;

--[ 使用 if 语句 --]
if( a < 20 )
then
--[ if 条件为 true 时打印以下信息 --]
print("a 小于 20" );
end
print("a 的值为:", a);

以上代码执行结果如下:

1
2
a 小于 20
a 的值为: 10

3.5.2 if…else 语句

Lua if 语句可以与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码块。

Lua if…else 语句语法格式如下:

1
2
3
4
5
6
if(布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
else
--[ 布尔表达式为 false 时执行该语句块 --]
end

实例

以下实例用于判断变量 a 的值:

1
2
3
4
5
6
7
8
9
10
11
12
--[ 定义变量 --]
a = 100;
--[ 检查条件 --]
if( a < 20 )
then
--[ if 条件为 true 时执行该语句块 --]
print("a 小于 20" )
else
--[ if 条件为 false 时执行该语句块 --]
print("a 大于 20" )
end
print("a 的值为 :", a)

以上代码执行结果如下:

1
2
a 大于 20
a 的值为 : 100

3.5.3 if…elseif…else 语句

Lua if 语句可以与 elseif…else 语句搭配使用, 在 if 条件表达式为 false 时执行 elseif…else 语句代码块,用于检测多个条件语句。

Lua if…elseif…else 语句语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if( 布尔表达式 1)
then
--[ 在布尔表达式 1 为 true 时执行该语句块 --]

elseif( 布尔表达式 2)
then
--[ 在布尔表达式 2 为 true 时执行该语句块 --]

elseif( 布尔表达式 3)
then
--[ 在布尔表达式 3 为 true 时执行该语句块 --]
else
--[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end

实例

以下实例对变量 a 的值进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--[ 定义变量 --]
a = 100

--[ 检查布尔条件 --]
if( a == 10 )
then
--[ 如果条件为 true 打印以下信息 --]
print("a 的值为 10" )
elseif( a == 20 )
then
--[ if else if 条件为 true 时打印以下信息 --]
print("a 的值为 20" )
elseif( a == 30 )
then
--[ if else if condition 条件为 true 时打印以下信息 --]
print("a 的值为 30" )
else
--[ 以上条件语句没有一个为 true 时打印以下信息 --]
print("没有匹配 a 的值" )
end
print("a 的真实值为: ", a )

以上代码执行结果如下:

1
2
没有匹配 a 的值
a 的真实值为: 100

3.5.4 if 嵌套语句

if…else 语句

Lua if 语句允许嵌套, 这就意味着你可以在一个 if 或 else if 语句中插入其他的 if 或 else if 语句。

Lua if 嵌套语句语法格式如下

1
2
3
4
5
6
7
8
if( 布尔表达式 1)
then
--[ 布尔表达式 1 为 true 时执行该语句块 --]
if(布尔表达式 2)
then
--[ 布尔表达式 2 为 true 时执行该语句块 --]
end
end

实例

以下实例用于判断变量 a 和 b 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--[ 定义变量 --]
a = 100;
b = 200;

--[ 检查条件 --]
if( a == 100 )
then
--[ if 条件为 true 时执行以下 if 条件判断 --]
if( b == 200 )
then
--[ if 条件为 true 时执行该语句块 --]
print("a 的值为 100 b 的值为 200" );
end
end
print("a 的值为 :", a );
print("b 的值为 :", b );

以上代码执行结果如下:

1
2
3
a 的值为 100 b 的值为 200
a 的值为 : 100
b 的值为 : 200

四 Lua 函数

在Lua中,函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。

Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如print()函数可以将传入的参数打印在控制台上。

Lua 函数主要有两种用途:

  • 1.完成指定的任务,这种情况下函数作为调用语句使用;
  • 2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。

4.1 函数定义

Lua 编程语言函数定义格式如下:

1
2
3
4
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
  • optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数.未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local
  • function_name: 指定函数名称。
  • argument1, argument2, argument3…, argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。
  • function_body: 函数体,函数中需要执行的代码语句块。
  • result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。

4.2 实例

以下实例定义了函数 max(),参数为 num1, num2,用于比较两值的大小,并返回最大值:

1
2
3
4
5
6
7
8
9
10
11
12
13
--[[ 函数返回两个值的最大值 --]] function max(num1, num2)

if (num1 > num2) then
result = num1;
else
result = num2;
end

return result;
end
-- 调用函数
print("两值比较最大值为 ", max(10, 4))
print("两值比较最大值为 ", max(5, 6))

以上代码执行结果为:

1
2
两值比较最大值为     10
两值比较最大值为 6

Lua 中我们可以将函数作为参数传递给函数,如下实例:
1
2
3
4
5
6
7
8
9
10
11
12
myprint = function(param)
print("这是打印函数 - ##", param, "##")
end

function add(num1, num2, functionPrint)
result = num1 + num2
-- 调用传递的函数参数
functionPrint(result)
end
myprint(10)
-- myprint 函数作为参数传递
add(2, 5, myprint)

以上代码执行结果为:

1
2
这是打印函数 -   ##   10  ##
这是打印函数 - ## 7 ##

4.3 多返回值

Lua函数可以返回多个结果值,比如string.find,其返回匹配串”开始和结束的下标”(如果不存在匹配串返回nil)。

1
2
3
> s, e = string.find("www.w3cschool.cn", "w3cschool") 
> print(s, e)
5 13

Lua函数中,在return后列出要返回的值得列表即可返回多值,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function maximum(a)
local mi = 1 -- 最大值索引
local m = a[mi] -- 最大值
for i, val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m, mi
end

print(maximum({8, 10, 23, 12, 5}))

以上代码执行结果为:

1
23   3

4.4 可变参数

Lua函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(…) 表示函数有可变的参数。

Lua将函数的参数放在一个叫arg的表中,#arg 表示传入参数的个数。

例如,我们计算几个数的平均值:

1
2
3
4
5
6
7
8
9
10
11
function average(...)
result = 0
local arg = {...}
for i, v in ipairs(arg) do
result = result + v
end
print("总共传入 " .. #arg .. " 个数")
return result / #arg
end

print("平均值为", average(10, 5, 3, 4, 5, 6))

以上代码执行结果为:

1
2
总共传入 6 个数
平均值为 5.5

4.5 函数闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createCountdownTimer(second)
local ms = second * 1000 -- ms为countDown的Upvalue
local function countDown()
ms = ms - 1
return ms
end

return countDown
end

local timer1 = createCountdownTimer(1)

for i = 1, 3 do
print(timer1())
end

输出结果:

1
2
3
999
998
997

4.5.1 关于函数闭包描述

  • Upvalue

    一个函数所使用的定义在它的函数体之外的局部变量(external local variable)称为这个函数的upvalue。 在前面的代码中,函数countDown使用的定义在函数createCountdownTimer 中的局部变量ms就是countDown的upvalue,但ms对createCountdownTimer而 言只是一个局部变量,不是upvalue。 Upvalue是Lua不同于C/C++的特有属性,需要结合代码仔细体会。

  • 函数闭包

一个函数和它所使用的所有upvalue构成了一个函数闭包。

  • Lua函数闭包与C函数的比较

Lua函数闭包使函数具有保持它自己的状态的能力,从这个意义上说,可以 与带静态局部变量的C函数相类比。但二者有显著的不同:对Lua来说,函数 是一种基本数据类型——代表一种(可执行)对象,可以有自己的状态;但 是对带静态局部变量的C函数来说,它并不是C的一种数据类型,更不会产生 什么对象实例,它只是一个静态地址的符号名称。

4.6 Lua中的基本函数库

基本函数库 功能 参数
assert(v[,mess age]) 相当于C的断言 v:当表达式v为nil或false将触发错误,message:发生错误时返回的信息,默认为”assertion failed!”
collectgarbage (opt [, arg]) 是垃圾收集器的通用接口,用于操作垃圾收集器 opt:操作方法标志”Stop”: 停止垃圾收集器”Restart”: 重启垃圾收集器”Collect”: 执行一次全垃圾收集循环”Count”: 返回当前Lua中使用的内存量(以KB为单位)”Step”: 单步执行一个垃圾收集. 步长 “Size” 由参数arg指定 (大型的值需要多步才能完成),如果要准确指定步长,需要多次实验以达最优效果。如果步长完成一次收集循环,将返回True”Setpause”: 设置 arg/100 的值作为暂定收集的时长”Setstepmul”: 设置 arg/100 的值,作为步长的增幅(即新步长=旧步长*arg/100)
dofile (filename) 打开并且执行一个lua块,当忽略参数filename时,将执行标准输入设备(stdin)的内容。返回所有块的返回值。当发生错误时,dofile将错误反射给调用者 注:dofile不能在保护模式下运行
error (message [, level]) 终止正在执行的函数,并返回message的内容作为错误信息(error函数永远都不会返回) 通常情况下,error会附加一些错误位置的信息到message头部.Level参数指示获得错误的位置,Level=1[默认]:为调用error位置(文件+行号)Level=2:指出哪个调用error的函数的函数Level=0:不添加错误位置信息
_G全局环境表(全局变量) 记录全局环境的变量值的表 _G._G = _G
getfenv(f) 返回函数f的当前环境表 f可以为函数或调用栈的级别,级别1[默认]为当前的函数,级别0或其它值将返回全局环境_G
getmetatable(object) 返回指定对象的元表(若object的元表.metatable项有值,则返回object的元表.metatable的值),当object没有元表时将返回nil
ipairs (t) 返回三个值 迭代函数、表、0多用于穷举表的键名和键值对如:for i,v in ipairs(t) do end每次循环将索引赋级i,键值赋给v 注:本函数只能用于以数字索引访问的表如:t={“1”,”cash”}
load (func [, chunkname]) 装载一个块中的函数,每次调用func将返回一个连接前一结的字串,在块结尾处将返回nil当没有发生错误时,将返回一个编译完成的块作为函数,否则返回nil加上错误信息,此函数的环境为全局环境chunkname用于错误和调试信息
loadfile ([filename]) 与load类似,但装载的是文件或当没有指定filename时装载标准输入(stdin)的内容
loadstring (string [, chunkname]) 与load类似,但装载的内容是一个字串如:assert(loadstring(s))()
next (table [, index]) 允许程序遍历表中的每一个字段,返回下一索引和该索引的值。 table:要遍历的表index:要返回的索引的前一索中的号,当index为nil[]时,将返回第一个索引的值,当索引号为最后一个索引或表为空时将返回nil注:可以用next(t)来检测表是否为空(此函数只能用于以数字索引的表与ipairs相类似)
ipairs (t) 返回三个值 next函数、表、0多用于穷举表的键名和键值对如:for n,v in pairs(t) do end 每次循环将索引赋级i,键值赋给v注:本函数只能用于以键名索引访问的表如:t={id=”1”,name=”cash”}
pcall (f, arg1, ···) 在保护模式下调用函数(即发生的错误将不会反射给调用者)当调用函数成功能返回true,失败时将返回false加错误信息
print (···) 简单的以tostring方式格式化输出参数的内容
rawequal (v1, v2) 检测v1是否等于v2,此函数不会调用任何元表的方法
rawget (table, index) 获取表中指定索引的值,此函数不会调用任何元表的方法,成功返回相应的值,当索引不存在时返回nil 注:本函数只能用于以数字索引访问的表如:t={“1”,”cash”}
rawset (table, index, value) 设置表中指定索引的值,此函数不会调用任何元表的方法,此函数将返回table
select (index, ···) 当index为数字将返回所有index大于index的参数:如:select(2,”a”,”b”) 返回 “b”当index为”#”,则返回参数的总个数(不包括index)
setfenv (f, table) 设置函数f的环境表为table f可以为函数或调用栈的级别,级别1[默认]为当前的函数,级别0将设置当前线程的环境表
setmetatable (table, metatable) 指定的table设置元表metatable,如果metatable为nil则取消table的元表,当metatable有__metatable字段时,将触发错误 注:只能为LUA_TTABLE表类型指定元表
tonumber (e [, base]) 尝试将参数e转换为数字,当不能转换时返回nil base(2~36)指出参数e当前使用的进制,默认为10进制,如tonumber(11,2)=3
tostirng(e) 将参数e转换为字符串,此函数将会触发元表的__tostring事件
type(v) 返回参数的类型名(“nil”,”number”, “string”, “boolean”, “table”, “function”, “thread”, “userdata”)
unpack (list [, i [, j]]) 返回指定表的索引的值,i为起始索引,j为结束索引 注:本函数只能用于以数字索引访问的表,否则只会返回nil如:t={“1”,”cash”}
_VERSION 返回当前Lua的版本号”Lua 5.1”.
xpcall (f, err) 与pcall类似,在保护模式下调用函数(即发生的错误将不会反射给调用者)但可指定一个新的错误处理函数句柄当调用函数成功能返回true,失败时将返回false加err返回的结果

4.7 Lua中的数学库

Lua5.1中数学库的所有函数如下表:

math.pi 为圆周率常量 = 3.14159265358979323846

数学库 说明 例子 方法
abs 取绝对值 math.abs(-15) 15
acos 反余弦函数 math.acos(0.5) 1.04719755
asin 反正弦函数 math.asin(0.5) 0.52359877
atan2 x / y的反正切值 math.atan2(90.0, 45.0) 1.10714871
atan 反正切函数 math.atan(0.5) 0.463647609
ceil 不小于x的最大整数 math.ceil(5.8) 6
cosh 双曲线余弦函数 math.cosh(0.5) 1.276259652
cos 余弦函数 math.cos(0.5) 0.87758256
deg 弧度转角度 math.deg(math.pi) 180
exp 计算以e为底x次方值 math.exp(2) 2.718281828
floor 不大于x的最大整数 math.floor(5.6) 5
fmod (mod) 取模运算 math.mod(14, 5) 4
frexp 把双精度数val分解为数字部分(尾数)和以2为底的指数n,即val=x*2n math.frexp(10.0) 0.625 4
ldexp 计算value * 2的n次方 math.ldexp(10.0, 3) 80 = 10 * (2 ^3)
log10 计算以10为基数的对数 math.log10(100) 2
log 计算一个数字的自然对数 math.log(2.71) 0.9969
max 取得参数中最大值 math.max(2.71, 100, -98, 23) 100
min 取得参数中最小值 math.min(2.71, 100, -98, 23) -98
modf 把数分为整数和小数 math.modf(15.98) 15 98
pow 得到x的y次方 math.pow(2, 5) 32
rad 角度转弧度 math.rad(180) 3.14159265358
random 获取随机数 math.random(1, 100)math.random(100) 获取1-100的随机数
randomseed 设置随机数种子 math.randomseed(os.time()) 在使用math.random函数之前必须使用此函数设置随机数种子
sinh 双曲线正弦函数 math.sinh(0.5) 0.5210953
sin 正弦函数 math.sin(math.rad(30)) 0.5
sqrt 开平方函数 math.sqrt(16) 4
tanh 双曲线正切函数 math.tanh(0.5) 0.46211715
tan 正切函数 math.tan(0.5) 0.5463024

引用博客:http://www.cnblogs.com/whiteyun/archive/2009/08/10/1543040.html

五 常用函数

5.1 Lua 字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。

Lua 语言中字符串可以使用以下三种方式来表示:

  • 单引号间的一串字符。
  • 双引号间的一串字符。
  • [[]] 间的一串字符。

以上三种方式的字符串实例如下:

实例

1
2
3
4
5
6
7
string1 = "Lua"
print("\"字符串 1 是\"", string1)
string2 = 'w3cschool.cn'
print("字符串 2 是", string2)

string3 = [["Lua 教程"]]
print("字符串 3 是", string3)

以上代码执行输出结果为:

1
2
3
"字符串 1 是"    Lua
字符串 2 是 runoob.com
字符串 3"Lua 教程"

转义字符用于表示不能直接显示的字符,比如后退键,回车键,等。如在字符串转换双引号可以使用 “\””。

5.1.1 字符串操作

Lua 提供了很多的方法来支持字符串的操作:

序号 方法 & 用途
1 string.upper(argument): 字符串全部转为大写字母。
2 string.lower(argument): 字符串全部转为小写字母。
3 string.gsub(mainString,findString,replaceString,num)在字符串中替换。mainString 为要操作的字符串, findString 为被替换的字符,replaceString 要替换的字符,num 替换次数(可以忽略,则全部替换),如:> string.gsub("aaaa","a","z",3); zzza 3
4 string.find (str, substr, [init, [plain]]) 在一个指定的目标字符串 str 中搜索指定的内容 substr,如果找到了一个匹配的子串,就会返回这个子串的起始索引和结束索引,不存在则返回 nil。init 指定了搜索的起始位置,默认为 1,可以一个负数,表示从后往前数的字符个数。plain 表示是否以正则表达式匹配。以下实例查找字符串 “Lua” 的起始索引和结束索引位置:> string.find("Hello Lua user", "Lua", 1) 7 9
5 string.reverse(arg) 字符串反转> string.reverse("Lua") auL
6 string.format(…) 返回一个类似printf的格式化字符串> string.format("the value is:%d",4) the value is:4
7 string.char(arg) 和 string.byte(arg[,int]) char 将整型数字转成字符并连接, byte 转换字符为整数值(可以指定某个字符,默认第一个字符)。> string.char(97,98,99,100) abcd > string.byte("ABCD",4) 68 > string.byte("ABCD") 65 >
8 string.len(arg) 计算字符串长度。string.len("abc") 3
9 string.rep(string, n) 返回字符串string的n个拷贝> string.rep("abcd",2) abcdabcd
10 .. 链接两个字符串> print("www.runoob.".."com") www.runoob.com
11 string.gmatch(str, pattern) 返回一个迭代器函数,每一次调用这个函数,返回一个在字符串 str 找到的下一个符合 pattern 描述的子串。如果参数 pattern 描述的字符串没有找到,迭代函数返回nil。> for word in string.gmatch("Hello Lua user", "%a+") do print(word) end Hello Lua user
12 string.match(str, pattern, init) string.match()只寻找源字串str中的第一个配对. 参数init可选, 指定搜寻过程的起点, 默认为1。 在成功配对时, 函数将返回配对表达式中的所有捕获结果; 如果没有设置捕获标记, 则返回整个配对字符串. 当没有成功的配对时, 返回nil。> = string.match("I have 2 questions for you.", "%d+ %a+") 2 questions > = string.format("%d, %q", string.match("I have 2 questions for you.", "(%d+) (%a+)")) 2, "questions"

5.5.2 字符串截取

字符串截取使用 sub() 方法。

string.sub() 用于截取字符串,原型为:

1
string.sub(s, i [, j])

参数说明:

  • s:要截取的字符串。
  • i:截取开始位置。
  • j:截取结束位置,默认为 -1,最后一个字符。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 字符串
local sourcestr = "prefix--runoobgoogletaobao--suffix"
print("\n原始字符串", string.format("%q", sourcestr))

-- 截取部分,第4个到第15个
local first_sub = string.sub(sourcestr, 4, 15)
print("\n第一次截取", string.format("%q", first_sub))

-- 取字符串前缀,第1个到第8个
local second_sub = string.sub(sourcestr, 1, 8)
print("\n第二次截取", string.format("%q", second_sub))

-- 截取最后10个
local third_sub = string.sub(sourcestr, -10)
print("\n第三次截取", string.format("%q", third_sub))

-- 索引越界,输出原始字符串
local fourth_sub = string.sub(sourcestr, -100)
print("\n第四次截取", string.format("%q", fourth_sub))

以上代码执行结果为:

1
2
3
4
5
6
7
8
9
原始字符串    "prefix--runoobgoogletaobao--suffix"

第一次截取 "fix--runoobg"

第二次截取 "prefix--"

第三次截取 "ao--suffix"

第四次截取 "prefix--runoobgoogletaobao--suffix"

5.5.3 字符串大小写转换

实例

1
2
3
string1 = "Lua";
print(string.upper(string1))
print(string.lower(string1))

以上代码执行结果为:

1
2
LUA
lua

5.5.4 字符串查找与反转

实例

1
2
3
4
5
string = "Lua Tutorial"
-- 查找字符串
print(string.find(string, "Tutorial"))
reversedString = string.reverse(string)
print("新字符串为", reversedString)

以上代码执行结果为:

1
2
5    12
新字符串为 lairotuT auL

5.5.5 字符串格式化

Lua 提供了 string.format() 函数来生成具有特定格式的字符串, 函数的第一个参数是格式 , 之后是对应格式中每个代号的各种数据。

由于格式字符串的存在, 使得产生的长字符串可读性大大提高了。这个函数的格式很像 C 语言中的 printf()

以下实例演示了如何对字符串进行格式化操作:

格式字符串可能包含以下的转义码:

  • %c - 接受一个数字, 并将其转化为ASCII码表中对应的字符
  • %d, %i - 接受一个数字并将其转化为有符号的整数格式
  • %o - 接受一个数字并将其转化为八进制数格式
  • %u - 接受一个数字并将其转化为无符号整数格式
  • %x - 接受一个数字并将其转化为十六进制数格式, 使用小写字母
  • %X - 接受一个数字并将其转化为十六进制数格式, 使用大写字母
  • %e - 接受一个数字并将其转化为科学记数法格式, 使用小写字母e
  • %E - 接受一个数字并将其转化为科学记数法格式, 使用大写字母E
  • %f - 接受一个数字并将其转化为浮点数格式
  • %g(%G) - 接受一个数字并将其转化为%e(%E, 对应%G)及%f中较短的一种格式
  • %q - 接受一个字符串并将其转化为可安全被Lua编译器读入的格式
  • %s - 接受一个字符串并按照给定的参数格式化该字符串

为进一步细化格式, 可以在%号后添加参数. 参数将以如下的顺序读入:

  • (1) 符号: 一个+号表示其后的数字转义符将让正数显示正号. 默认情况下只有负数显示符号.
  • (2) 占位符: 一个0, 在后面指定了字串宽度时占位用. 不填时的默认占位符是空格.
  • (3) 对齐标识: 在指定了字串宽度时, 默认为右对齐, 增加-号可以改为左对齐.
  • (4) 宽度数值
  • (5) 小数位数/字串裁切: 在宽度数值后增加的小数部分n, 若后接f(浮点数转义符, 如%6.3f)则设定该浮点数的小数只保留n位, 若后接s(字符串转义符, 如%5.3s)则设定该字符串只显示前n位.

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
string1 = "Lua"
string2 = "Tutorial"
number1 = 10
number2 = 20
-- 基本字符串格式化
print(string.format("基本格式化 %s %s", string1, string2))
-- 日期格式化
date = 2;
month = 1;
year = 2014
print(string.format("日期格式化 %02d/%02d/%03d", date, month, year))
-- 十进制格式化
print(string.format("%.4f", 1 / 3))

以上代码执行结果为:

1
2
3
基本格式化 Lua Tutorial
日期格式化 02/01/2014
0.3333

其他实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string.format("%c", 83) -- 输出S
string.format("%+d", 17.0) -- 输出+17
string.format("%05d", 17) -- 输出00017
string.format("%o", 17) -- 输出21
string.format("%u", 3.14) -- 输出3
string.format("%x", 13) -- 输出d
string.format("%X", 13) -- 输出D
string.format("%e", 1000) -- 输出1.000000e+03
string.format("%E", 1000) -- 输出1.000000E+03
string.format("%6.3f", 13) -- 输出13.000
string.format("%q", "One\nTwo") -- 输出"One\
--   Two"
string.format("%s", "monkey") -- 输出monkey
string.format("%10s", "monkey") -- 输出 monkey
string.format("%5.3s", "monkey") -- 输出 mon

5.5.6 字符与整数相互转换

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 字符转换
-- 转换第一个字符
print(string.byte("Lua"))
-- 转换第三个字符
print(string.byte("Lua",3))
-- 转换末尾第一个字符
print(string.byte("Lua",-1))
-- 第二个字符
print(string.byte("Lua",2))
-- 转换末尾第二个字符
print(string.byte("Lua",-2))

-- 整数 ASCII 码转换为字符
print(string.char(97))

以上代码执行结果为:

1
2
3
4
5
6
76
97
97
117
117
a

5.5.7 其他常用函数

以下实例演示了其他字符串操作,如计算字符串长度,字符串连接,字符串复制等:

1
2
3
4
5
6
7
8
9
10
11
12
string1 = "www."
string2 = "runoob"
string3 = ".com"
-- 使用 .. 进行字符串连接
print("连接字符串", string1 .. string2 .. string3)

-- 字符串长度
print("字符串长度 ", string.len(string2))

-- 字符串复制 2 次
repeatedString = string.rep(string2, 2)
print(repeatedString)

以上代码执行结果为:

1
2
3
连接字符串    www.runoob.com
字符串长度 6
runoobrunoob

5.5.8 匹配模式

Lua 中的匹配模式直接用常规的字符串来描述。 它用于模式匹配函数 string.find, string.gmatch, string.gsub, string.match

你还可以在模式串中使用字符类。

字符类指可以匹配一个特定字符集合内任何字符的模式项。比如,字符类 %d 匹配任意数字。所以你可以使用模式串 %d%d/%d%d/%d%d%d%d 搜索 dd/mm/yyyy 格式的日期:

5.5.8.1 实例

1
2
3
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date))) -- > 30/05/1999

下面的表列出了Lua支持的所有字符类:d

单个字符(除 ^$()%.[]*+-? 外): 与该字符自身配对

  • .(点): 与任何字符配对

  • %a: 与任何字母配对

  • %c: 与任何控制符配对(例如\n)

  • %d: 与任何数字配对

  • %l: 与任何小写字母配对

  • %p: 与任何标点(punctuation)配对

  • %s: 与空白字符配对

  • %u: 与任何大写字母配对

  • %w: 与任何字母/数字配对

  • %x: 与任何十六进制数配对

  • %z: 与任何代表0的字符配对

  • %x(此处x是非字母非数字字符): 与字符x配对. 主要用来处理表达式中有功能的字符(^$()%.[]*+-?)的配对问题, 例如%%与%配对

  • [数个字符类]: 与任何[]中包含的字符类配对. 例如[%w]与任何字母/数字, 或下划线符号()配对

  • 数个字符类. 与任何不包含在[]中的字符类配对. 例如%s与任何非空白字符配对

当上述的字符类用大写书写时, 表示与非此字符类的任何字符配对. 例如, %S表示与任何非空白字符配对.例如,’%A’非字母的字符:

1
2
> print(string.gsub("hello, up-down!", "%A", "."))
hello..up.down. 4

数字4不是字符串结果的一部分,他是gsub返回的第二个结果,代表发生替换的次数。

在模式匹配中有一些特殊字符,他们有特殊的意义,Lua中的特殊字符如下:

1
( ) . % + - * ? [ ^ $

‘%’ 用作特殊字符的转义字符,因此 ‘%.’ 匹配点;’%%’ 匹配字符 ‘%’。转义字符 ‘%’不仅可以用来转义特殊字符,还可以用于所有的非字母的字符。

5.5.8.2 模式条目

  • 单个字符类匹配该类别中任意单个字符;
  • 单个字符类跟一个 ‘*‘, 将匹配零或多个该类的字符。 这个条目总是匹配尽可能长的串;
  • 单个字符类跟一个 ‘+‘, 将匹配一或更多个该类的字符。 这个条目总是匹配尽可能长的串;
  • 单个字符类跟一个 ‘-‘, 将匹配零或更多个该类的字符。 和 ‘*‘ 不同, 这个条目总是匹配尽可能短的串;
  • 单个字符类跟一个 ‘?‘, 将匹配零或一个该类的字符。 只要有可能,它会匹配一个;
  • %*n*, 这里的 n 可以从 1 到 9; 这个条目匹配一个等于 n 号捕获物(后面有描述)的子串。
  • %b*xy*, 这里的 xy 是两个明确的字符; 这个条目匹配以 x 开始 y 结束, 且其中 xy 保持 平衡 的字符串。 意思是,如果从左到右读这个字符串,对每次读到一个 x+1 ,读到一个 y-1, 最终结束处的那个 y 是第一个记数到 0 的 y。 举个例子,条目 %b() 可以匹配到括号平衡的表达式。
  • %f[*set*], 指 边境模式; 这个条目会匹配到一个位于 set 内某个字符之前的一个空串, 且这个位置的前一个字符不属于 set 。 集合 set 的含义如前面所述。 匹配出的那个空串之开始和结束点的计算就看成该处有个字符 ‘\0‘ 一样。

模式:

模式 指一个模式条目的序列。 在模式最前面加上符号 ‘^‘ 将锚定从字符串的开始处做匹配。 在模式最后面加上符号 ‘‘ 出现在其它位置,它们均没有特殊含义,只表示自身。

捕获:

模式可以在内部用小括号括起一个子模式; 这些子模式被称为 捕获物。 当匹配成功时,由 捕获物 匹配到的字符串中的子串被保存起来用于未来的用途。 捕获物以它们左括号的次序来编号。 例如,对于模式 "(a*(.)%w(%s*))" , 字符串中匹配到 "a*(.)%w(%s*)" 的部分保存在第一个捕获物中 (因此是编号 1 ); 由 “.“ 匹配到的字符是 2 号捕获物, 匹配到 “%s*“ 的那部分是 3 号。

作为一个特例,空的捕获 () 将捕获到当前字符串的位置(它是一个数字)。 例如,如果将模式 "()aa()" 作用到字符串 "flaaap" 上,将产生两个捕获物: 3 和 5 。

5.2 Lua 数组

数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。

Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。


5.2.1 一维数组

一维数组是最简单的数组,其逻辑结构是线性表。一维数组可以用for循环出数组中的元素,如下实例:

实例

1
2
3
4
5
array = {"Lua", "Tutorial"}

for i = 0, 2 do
print(array[i])
end

以上代码执行输出结果为:

1
2
3
nil
Lua
Tutorial

正如你所看到的,我们可以使用整数索引来访问数组元素,如果指定的索引没有值则返回 nil

在 Lua 索引值是以 1 为起始,但你也可以指定 0 开始。

除此外我们还可以以负数为数组索引值:

1
2
3
4
5
6
7
8
9
array = {}

for i = -2, 2 do
array[i] = i * 2
end

for i = -2, 2 do
print(array[i])
end

以上代码执行输出结果为:

1
2
3
4
5
-4
-2
0
2
4

5.2.2 多维数组

多维数组即数组中包含数组或一维数组的索引键对应一个数组。

以下是一个三行三列的阵列多维数组:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 初始化数组
array = {}
for i = 1, 3 do
array[i] = {}
for j = 1, 3 do
array[i][j] = i * j
end
end

-- 访问数组
for i = 1, 3 do
for j = 1, 3 do
print(array[i][j])
end
end

以上代码执行输出结果为:

1
2
3
4
5
6
7
8
9
1
2
3
2
4
6
3
6
9

不同索引键的三行三列阵列多维数组:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 初始化数组
array = {}
maxRows = 3
maxColumns = 3
for row = 1, maxRows do
for col = 1, maxColumns do
array[row * maxColumns + col] = row * col
end
end

-- 访问数组
for row = 1, maxRows do
for col = 1, maxColumns do
print(array[row * maxColumns + col])
end
end

以上代码执行输出结果为:

1
2
3
4
5
6
7
8
9
1
2
3
2
4
6
3
6
9

正如你所看到的,以上的实例中,数组设定了指定的索引值,这样可以避免出现 nil 值,有利于节省内存空间。

5.3 Lua 迭代器

迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。

在 Lua 中迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。


5.3.1 泛型 for 迭代器

泛型 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。

泛型 for 迭代器提供了集合的 key/value 对,语法格式如下:

1
2
3
for k, v in pairs(t) do
print(k, v)
end

上面代码中,k, v为变量列表;pairs(t)为表达式列表。

查看以下实例:

实例

1
2
3
4
5
array = {"Google", "Runoob"}

for key, value in ipairs(array) do
print(key, value)
end

以上代码执行输出结果为:

1
2
1  Google
2 Runoob

以上实例中我们使用了 Lua 默认提供的迭代函数 ipairs。

下面我们看看泛型 for 的执行过程:

  • 首先,初始化,计算 in 后面表达式的值,表达式应该返回泛型 for 需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  • 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
  • 第三,将迭代函数返回的值赋给变量列表。
  • 第四,如果返回的第一个值为nil循环结束,否则执行循环体。
  • 第五,回到第二步再次调用迭代函数

在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。Lua 的迭代器包含以下两种类型:

  • 无状态的迭代器
  • 多状态的迭代器

5.3.2 无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

这种无状态迭代器的典型的简单的例子是 ipairs,它遍历数组的每一个元素,元素的索引需要是数值。

以下实例我们使用了一个简单的函数来实现迭代器,实现 数字 n 的平方:

1
2
3
4
5
6
7
8
9
10
function square(iteratorMaxCount, currentNumber)
if currentNumber < iteratorMaxCount then
currentNumber = currentNumber + 1
return currentNumber, currentNumber * currentNumber
end
end

for i, n in square, 3, 0 do
print(i, n)
end

以上实例输出结果为:

1
2
3
1    1
2 4
3 9

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs 和迭代函数都很简单,我们在 Lua 中可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
function iter(a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end

function ipairs(a)
return iter, a, 0
end

当 Lua 调用 ipairs(a) 开始循环时,他获取三个值:迭代函数 iter、状态常量 a、控制变量初始值 0;然后 Lua 调用 iter(a,0) 返回 1, a[1](除非 a[1]=nil);第二次迭代调用 iter(a,1) 返回 2, a[2]……直到第一个 nil 元素。


5.3.3 多状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

以下实例我们创建了自己的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
array = {"Google", "Runoob"}

function elementIterator(collection)
local index = 0
local count = #collection
-- 闭包函数
return function()
index = index + 1
if index <= count then
-- 返回迭代器的当前元素
return collection[index]
end
end
end

for element in elementIterator(array) do
print(element)
end

以上实例输出结果为:

1
2
Google
Runoob

以上实例中我们可以看到,elementIterator 内使用了闭包函数,实现计算集合大小并输出各个元素。

5.3.4 pairs 和 ipairs区别

  • pairs: 迭代 table,可以遍历表中所有的 key 可以返回 nil
  • ipairs: 迭代数组,不能返回 nil,如果遇到 nil 则退出
  • 同:都是能遍历集合(表、数组)
  • 异:ipairs 仅仅遍历值,按照索引升序遍历,索引中断停止遍历。即不能返回 nil,只能返回数字 0,如果遇到 nil 则退出。它只能遍历到集合中出现的第一个不是整数的 key。
  • pairs 能遍历集合的所有元素。即 pairs 可以遍历集合中所有的 key,并且除了迭代器本身以及遍历表本身还可以返回 nil。
1
2
3
4
5
6
7
8
9
10
11
12
local tab = {
[1] = "a",
[3] = "b",
[4] = "c"
}
for i, v in pairs(tab) do -- 输出 "a" ,"b", "c" ,
print(tab[i])
end

for i, v in ipairs(tab) do -- 输出 "a" ,k=2时断开
print(tab[i])
end

5.4 Lua table(表)

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。

Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。

Lua table 是不固定大小的,你可以根据自己需要进行扩容。

Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用”format”来索引table string。


5.4.1 table(表)的构造

构造器是创建和初始化表的表达式。表是Lua特有的功能强大的东西。最简单的构造函数是{},用来创建一个空表。可以直接初始化数组:

1
2
3
4
5
6
7
8
9
-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存

当我们为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil ,则 b 同样能访问 table 的元素。如果没有指定的变量指向a,Lua的垃圾回收机制会清理相对应的内存。

以下实例演示了以上的描述情况:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 简单的 table
mytable = {}
print("mytable 的类型是 ",type(mytable))

mytable[1]= "Lua"
mytable["wow"] = "修改前"
print("mytable 索引为 1 的元素是 ", mytable[1])
print("mytable 索引为 wow 的元素是 ", mytable["wow"])

-- alternatetable和mytable的是指同一个 table
alternatetable = mytable

print("alternatetable 索引为 1 的元素是 ", alternatetable[1])
print("mytable 索引为 wow 的元素是 ", alternatetable["wow"])

alternatetable["wow"] = "修改后"

print("mytable 索引为 wow 的元素是 ", mytable["wow"])

-- 释放变量
alternatetable = nil
print("alternatetable 是 ", alternatetable)

-- mytable 仍然可以访问
print("mytable 索引为 wow 的元素是 ", mytable["wow"])

mytable = nil
print("mytable 是 ", mytable)

以上代码执行结果为:

1
2
3
4
5
6
7
8
9
mytable 的类型是     table
mytable 索引为 1 的元素是 Lua
mytable 索引为 wow 的元素是 修改前
alternatetable 索引为 1 的元素是 Lua
mytable 索引为 wow 的元素是 修改前
mytable 索引为 wow 的元素是 修改后
alternatetable 是 nil
mytable 索引为 wow 的元素是 修改后
mytable 是 nil

5.4.2 Table 操作

以下列出了 Table 操作常用的方法:

序号 方法 & 用途
1 table.concat (table [, sep [, start [, end]]]):concat是concatenate(连锁, 连接)的缩写. table.concat()函数列出参数中指定table的数组部分从start位置到end位置的所有元素, 元素间以指定的分隔符(sep)隔开。
2 table.insert (table, [pos,] value):在table的数组部分指定位置(pos)插入值为value的一个元素. pos参数可选, 默认为数组部分末尾.
3 table.maxn (table)指定table中所有正数key值中最大的key值. 如果不存在key值为正数的元素, 则返回0。(Lua5.2之后该方法已经不存在了,本文使用了自定义函数实现)
4 table.remove (table [, pos])返回table数组部分位于pos位置的元素. 其后的元素会被前移. pos参数可选, 默认为table长度, 即从最后一个元素删起。
5 table.sort (table [, comp])对给定的table进行升序排序。

接下来我们来看下这几个方法的实例。

5.4.2.1 Table 连接

我们可以使用 concat() 输出一个列表中元素连接成的字符串:

实例

1
2
3
4
5
6
7
8
9
fruits = {"banana","orange","apple"}
-- 返回 table 连接后的字符串
print("连接后的字符串 ",table.concat(fruits))

-- 指定连接字符
print("连接后的字符串 ",table.concat(fruits,", "))

-- 指定索引来连接 table
print("连接后的字符串 ",table.concat(fruits,", ", 2,3))

执行以上代码输出结果为:

1
2
3
连接后的字符串     bananaorangeapple
连接后的字符串 banana, orange, apple
连接后的字符串 orange, apple

5.4.2.2 插入和移除

以下实例演示了 table 的插入和移除操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
fruits = {"banana", "orange", "apple"}

-- 在末尾插入
table.insert(fruits, "mango")
print("索引为 4 的元素为 ", fruits[4])

-- 在索引为 2 的键处插入
table.insert(fruits, 2, "grapes")
print("索引为 2 的元素为 ", fruits[2])

print("最后一个元素为 ", fruits[5])
table.remove(fruits)
print("移除后最后一个元素为 ", fruits[5])

执行以上代码输出结果为:

1
2
3
4
索引为 4 的元素为     mango
索引为 2 的元素为 grapes
最后一个元素为 mango
移除后最后一个元素为 nil

5.4.2.3 Table 排序

以下实例演示了 sort() 方法的使用,用于对 Table 进行排序:

实例

1
2
3
4
5
6
7
8
9
10
11
fruits = {"banana", "orange", "apple", "grapes"}
print("排序前")
for k, v in ipairs(fruits) do
print(k, v)
end

table.sort(fruits)
print("排序后")
for k, v in ipairs(fruits) do
print(k, v)
end

执行以上代码输出结果为:

1
2
3
4
5
6
7
8
9
10
排序前
1 banana
2 orange
3 apple
4 grapes
排序后
1 apple
2 banana
3 grapes
4 orange

5.4.2.4 Table 最大值

table.maxn 在 Lua5.2 之后该方法已经不存在了,我们定义了 table_maxn 方法来实现。

以下实例演示了如何获取 table 中的最大值:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function table_maxn(t)
local mn = nil;
for k, v in pairs(t) do
if (mn == nil) then
mn = v
end
if mn < v then
mn = v
end
end
return mn
end
tbl = {
[1] = 2,
[2] = 6,
[3] = 34,
[26] = 5
}
print("tbl 最大值:", table_maxn(tbl))
print("tbl 长度 ", #tbl)

执行以上代码输出结果为:

1
2
tbl 最大值:    34
tbl 长度 3

注意:

当我们获取 table 的长度的时候无论是使用 # 还是 table.getn 其都会在索引中断的地方停止计数,而导致无法正确取得 table 的长度。

可以使用以下方法来代替:

1
2
3
4
5
6
7
function table_leng(t)
local leng=0
for k, v in pairs(t) do
leng=leng+1
end
return leng;
end

5.4.2.5 table 去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
table = {1, 2, 3, 4, 20, 6, 7, 7, 15, 28};
function table_unique(t)
local check = {};
local n = {};
for key, value in pairs(t) do
if not check[value] then
n[key] = value
check[value] = value
end
end
return n
end

for key, value in pairs(table_unique(table)) do
print('value is ', value)
end

执行结果为:

1
2
3
4
5
6
7
8
9
value is 	1
value is 2
value is 3
value is 4
value is 20
value is 6
value is 7
value is 15
value is 28

5.4.3 地址传递

函数传参类型为table时,是地址传递而不是值传递!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local function f(x)
x["writable"] = "written"
end
local t = {
["readonly"] = "read",
["writable"] = "write"
}
print("before---------")
for k, v in pairs(t) do
print(k, v)
end
f(t)
print("after----------")
for k, v in pairs(t) do
print(k, v)
end

输出为

1
2
3
4
5
6
before---------
readonly read
writable write
after----------
readonly read
writable writteny

六 Lua 模块与包

从 Lua5.1 版本开始,就对模块和包添加了新的支持,可是使用 require 和 module 来定义和使用模块和包。require 用于使用模块,module 用于创建模块。简单的说,一个模块就是一个程序库,可以通过 require 来加载。然后便得到了一个全局变量,表示一个 table。这个 table 就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和常量,一个符合规范的模块还应使 require 返回这个 table。现在就来具体的总结一下 require 和 module 这两个函数。如:

1
2
3
4
5
require "mod"
mod.foo()
local m2 = require "mod2"
local f = mod2.foo
f()

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}

-- 定义一个常量
module.constant = "这是一个常量"

-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end

local function func2()
print("这是一个私有函数!")
end

function module.func3()
func2()
end

return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。

上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.


6.1 require 函数

require 函数的调用形式为 require "模块名"。该调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。在使用 Lua 中的标准库时可以不用显示的调用 require,因为 Lua 已经预先加载了他们。

require 函数在搜素加载模块时,有一套自定义的模式,如:

1
?;?.lua;c:/windows/?;/usr/local/lua/?/?.lua

在上面的模式中,只有问号 (?) 和分号 (;) 是模式字符,分别表示 require 函数的参数(模块名)和模式间的分隔符。如:调用require "sql",将会打开以下的文件:

1
2
3
4
sql
sql.lua
c:/windows/sql
/usr/local/lua/sql/sql.lua

6.1.1 编写模块的基本方法

新建一个文件,命名为 game.lua,代码如下:

1
2
3
4
5
6
7
8
9
10
local M = {};
local modelName = ...;
_G[modelName] = M;
function M.play()
print("那么,开始吧");
end
function M.quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;

加载 game.lua,代码如下:

1
2
3
game = require "test"

game.play()

运行:

lua -e “io.stdout:setvbuf ‘no’” “HelloWorld.lua”
那么,开始吧
Exit code: 0

6.1.2 示例

Lua 将 require 搜索的模式字符串放在变量 package.path 中。当 Lua 启动后,便以环境变量 LUA_PATH 的值来初始化这个变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。如果 require 无法找到与模块名相符的 Lua 文件,就会找 C 程序库。C 程序库的搜索模式存放在变量 package.cpath 中。而这个变量则是通过环境变量 LUA_CPATH 来初始化的。

Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:

1
require("<模块名>")

或者

1
require "<模块名>"

执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

test_module.lua 文件

1
2
3
4
5
6
7
-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")

print(module.constant)

module.func3()

以上代码执行结果为:

1
2
这是一个常量
这是一个私有函数!

或者给加载的模块定义一个别名变量,方便调用:

test_module2.lua 文件

1
2
3
4
5
6
7
8
-- test_module2.lua 文件
-- module 模块为上文提到到 module.lua
-- 别名变量 m
local m = require("module")

print(m.constant)

m.func3()

以上代码执行结果为:

1
2
这是一个常量
这是一个私有函数!

6.1.3 使用环境

仔细阅读上例中的代码,我们可以发现一些细节上问题。比如模块内函数之间的调用仍然要保留模块名的限定符,如果是私有变量还需要加 local 关键字同时不能加模块名限定符。如果需要将私有改为公有,或者反之,都需要一定的修改。那又该如何规避这些问题呢?我们可以通过Lua的函数“全局环境”来有效的解决这些问题。

我们把 game.lua 这个模块里的全局环境设置为 M,于是,我们直接定义函数的时候,不需要再带 M 前缀。
因为此时的全局环境就是 M,不带前缀去定义变量,就是全局变量,这时的全局变量是保存在 M 里。
所以,实际上,play 和 quit 函数仍然是在 M 这个 table 里。

1
2
3
4
5
6
7
8
9
10
11
12
local M = {};
local modelName = ...;
_G[modelName] = M;
package.loaded[modname] = M
setfenv(1, M);
function play()
print("那么,开始吧");
end
function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end
return M;

6.2 module 函数

在 Lua 5.1中,我们可以用 module(…) 函数来代替以下代码,如:

1
2
3
4
5
6
7
8
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
--[[
和普通Lua程序块一样声明外部函数。
--]]
setfenv(1,M)

即是:

1
2
3
4
5
6
7
8
9
module(..., package.seeall);

function play()
print("那么,开始吧")
end

function quit()
print("你走吧,我保证你不会出事的,呵,呵呵");
end

由于在默认情况下,module 不提供外部访问,必须在调用它之前,为需要访问的外部函数或模块声明适当的局部变量。然后 Lua 提供了一种更为方便的实现方式,即在调用 module 函数时,多传入一个 package.seeall 的参数,相当于 setmetatable(M, {__index = _G}) .

如:

module(…,package.seeall)

6.3 加载机制

对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。

require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。

当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 “~/lua/“ 路径加入 LUA_PATH 环境变量里:

1
2
#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"

文件路径以 “;” 号分隔,最后的 2 个 “;;” 表示新加的路径后面加上原来的默认路径。

接着,更新环境变量参数,使之立即生效。

1
source ~/.profile

这时假设 package.path 的值是:

1
/Users/dengjoe/lua/?.lua;./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

那么调用 require(“module”) 时就会尝试打开以下文件目录去搜索目标。

1
2
3
4
5
6
/Users/dengjoe/lua/module.lua;
./module.lua
/usr/local/share/lua/5.1/module.lua
/usr/local/share/lua/5.1/module/init.lua
/usr/local/lib/lua/5.1/module.lua
/usr/local/lib/lua/5.1/module/init.lua

如果找过目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。

搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。

搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。


6.4 C 包

Lua和C是很容易结合的,使用 C 为 Lua 写包。

与Lua中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制。

Lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:

1
2
local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")

loadlib 函数加载指定的库并且连接到 Lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为 Lua 的一个函数,这样我们就可以直接在Lua中调用他。

如果加载动态库或者查找初始化函数时出错,loadlib 将返回 nil 和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:

1
2
3
4
local path = "/usr/local/lua/lib/libluasocket.so"
-- 或者 path = "C:\\windows\\luasocket.dll",这是 Window 平台下
local f = assert(loadlib(path, "luaopen_socket"))
f() -- 真正打开库

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装二进制库的时候可以随便放在某个目录,只需要修改 stub 文件对应二进制库的实际路径即可。

将 stub 文件所在的目录加入到 LUA_PATH,这样设定后就可以使用 require 函数加载 C 库了。

七 Lua 元表(Metatable)

Lua中每个值都可具有元表。 元表是普通的Lua表,定义了原始值在某些特定操作下的行为。你可通过在值的原表中设置特定的字段来改变作用于该值的操作的某些行为特征。

例如,当数字值作为加法的操作数时,Lua检查其元表中的”__add”字段是否有个函数。如果有,Lua调用它执行加法。

我们称元表中的键为事件(event),称值为元方法(metamethod)。前述例子中的事件是”add”,元方法是执行加法的函数。

可通过函数getmetatable查询任何值的元表。

在table中,我可以重新定义的元方法有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

Lua中的每一个表都有其Metatable。Lua默认创建一个不带metatable的新表

1
2
t = {}
print(getmetatable(t)) --> nil

可以使用setmetatable函数设置或者改变一个表的metatable

1
2
3
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

任何一个表都可以是其他一个表的metatable,一组相关的表可以共享一个metatable(描述他们共同的行为)。一个表也可以是自身的metatable(描述其私有行为)。
接下来就介绍介绍如果去重新定义这些方法。

在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作(比如相加)。

因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。

例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。

当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。 __add 等即时字段,其对应的值(往往是一个函数或是 table)就是”元方法”。

有两个很重要的函数来处理元表:

  • setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
  • getmetatable(table): 返回对象的元表(metatable)。

以下实例演示了如何对指定的表设置元表:

1
2
3
mytable = {}              *-- 普通表*
mymetatable = {} *-- 元表*
setmetatable(mytable,mymetatable) *-- 把 mymetatable 设为 mytable 的元表*

以上代码也可以直接写成一行:

1
mytable = setmetatable({},{})

以下为返回对象元表:

1
getmetatable(mytable)                 -- 这会返回 mymetatable

7.1 __index 元方法

这是 metatable 最常用的键。

当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的index 键。如果index包含一个表格,Lua会在表格中查找相应的键。

__newindex 有两个规则:

  • 1、如果 __newindex 是一个函数,则在给 table 中不存在的字段赋值时,会调用这个函数,并且赋值不成功。
  • 2、如果 newindex 是一个 table,则在给 table 中不存在的字段赋值时,会直接给 newindex的table 赋值。

我们可以在使用 lua 命令进入交互模式查看:

1
2
3
4
5
6
7
8
$ lua
Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> other = { foo = 3 }
> t = setmetatable({}, { __index = other })
> t.foo
3
> t.bar
nil

如果__index包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。

index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 index 返回结果。

实现 __index 元方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
text = {}
text.defaultValue = {
size = 14,
content = "hello"
}
text.mt = {} -- 创建元表

function text.new(a)
setmetatable(a, text.mt)
return a
end

text.mt.__index = function(tb, key)
return text.defaultValue[key]
end

local x = text.new {
content = "bye"
}
print(x.size) -- > 14

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
mytable = setmetatable({
key1 = "value1"
}, {
__index = function(mytable, key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
})

print(mytable.key1, mytable.key2)

实例输出结果为:

1
value1    metatablevalue

实例解析:

  • mytable 表赋值为 {key1 = “value1”}

  • mytable 设置了元表,元方法为 __index。

  • 在mytable表中查找 key1,如果找到,返回该元素,找不到则继续。

  • 在mytable表中查找 key2,如果找到,返回 metatablevalue,找不到则继续。

  • 判断元表有没有index方法,如果index方法是一个函数,则调用该函数。

  • 元方法中查看是否传入 “key2” 键的参数(mytable.key2已设置),如果传入 “key2” 参数返回 “metatablevalue”,否则返回 mytable 对应的键值。

我们可以将以上代码简单写成:

1
2
mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)

总结

Lua 查找一个表元素时的规则,其实就是如下 3 个步骤:

  • 1.在表中查找,如果找到,返回该元素,找不到则继续
  • 2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
  • 3.判断元表有没有 index 方法,如果 index 方法为 nil,则返回 nil;如果 index 方法是一个表,则重复 1、2、3;如果 index 方法是一个函数,则返回该函数的返回值。

该部分内容来自作者寰子:https://blog.csdn.net/xocoder/article/details/9028347


7.2 __newindex 元方法

newindex 元方法用来对表更新,index则用来对表访问 。

当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。

以下实例演示了 __newindex 元方法的应用:

实例

1
2
3
4
5
6
7
8
9
10
mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)

mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)

mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)

以上实例执行输出结果为:

1
2
3
value1
nil 新值2
新值1 nil

以上实例中表设置了元方法 newindex,在对新索引键(newkey)赋值时(mytable.newkey = “新值2”),会调用元方法,而不进行赋值。而如果对已存在的索引键(key1),则会进行赋值,而不调用元方法 newindex。

以下实例使用了 rawset 函数来更新表:

实例

1
2
3
4
5
6
7
8
9
10
mytable = setmetatable({key1 = "value1"}, {
__newindex = function(mytable, key, value)
rawset(mytable, key, "\""..value.."\"")
end
})

mytable.key1 = "new value"
mytable.key2 = 4

print(mytable.key1,mytable.key2)

以上实例执行输出结果为:

1
new value    "4"

7.3 为表添加操作符

以下实例演示了两表相加操作:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end

-- 两表相加操作
mytable = setmetatable({1, 2, 3}, {
__add = function(mytable, newtable)
for i = 1, table_maxn(newtable) do
table.insert(mytable, table_maxn(mytable) + 1, newtable[i])
end
return mytable
end
})

secondtable = {4, 5, 6}

mytable = mytable + secondtable
for k, v in ipairs(mytable) do
print(k, v)
end

以上实例执行输出结果为:

1
2
3
4
5
6
1    1
2 2
3 3
4 4
5 5
6 6

add 键包含在元表中,并进行相加操作。 表中对应的操作列表如下:(注意:****是两个下划线)

模式 描述
__add 对应的运算符 ‘+’.
__sub 对应的运算符 ‘-‘.
__mul 对应的运算符 ‘*’.
__div 对应的运算符 ‘/‘.
__mod 对应的运算符 ‘%’.
__unm 对应的运算符 ‘-‘.
__concat 对应的运算符 ‘..’.
__eq 对应的运算符 ‘==’.
__lt 对应的运算符 ‘<’.
__le 对应的运算符 ‘<=’.

7.4 __call 元方法

__call 元方法在 Lua 调用一个值时调用。以下实例演示了计算表中元素的和:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end

-- 定义元方法__call
mytable = setmetatable({10}, {
__call = function(mytable, newtable)
sum = 0
for i = 1, table_maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
})
newtable = {10, 20, 30}
print(mytable(newtable))

以上实例执行输出结果为:

1
70

7.5 __tostring 元方法

__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:

实例

1
2
3
4
5
6
7
8
9
10
mytable = setmetatable({10, 20, 30}, {
__tostring = function(mytable)
sum = 0
for k, v in pairs(mytable) do
sum = sum + v
end
return "表所有元素的和为 " .. sum
end
})
print(mytable)

以上实例执行输出结果为:

1
表所有元素的和为 60

从本文中我们可以知道元表可以很好的简化我们的代码功能,所以了解 Lua 的元表,可以让我们写出更加简单优秀的 Lua 代码。

八 Lua 协同程序(coroutine)

8.1 什么是协同(coroutine)?

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

协同是非常强大的功能,但是用起来也很复杂。

线程和协同程序区别

线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。

在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

8.2 基本语法

方法 描述
coroutine.create() 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume() 重启 coroutine,和 create 配合使用
coroutine.yield() 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status() 查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序
coroutine.wrap() 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复
coroutine.running() 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号

有关协作线程的描述:

  • 创建协作线程
    通过coroutine.create可以创建一个协作线程,该函数接收一个函数类型的参数作为线程的执行体,返回一个线程对象。

  • 启动线程
    通过coroutine.resume可以启动一个线程或者继续一个挂起的线程。该函数接收一个线程对象以及其他需要传递给该线程的参数。线程可以通过线程函数的参数或者coroutine.yield调用的返回值来获取这些参数。当线程初次执行时,resume传递的参数通过线程函数的参数传递给线程,线程从线程函数开始执行;当线程由挂起转为执行时,resume传递的参数以yield调用返回值的形式传递给线程,线程从yield调用后继续执行

  • 线程放弃调度
    线程调用coroutine.yield暂停自己的执行,并把执行权返回给启动/继续它的线程;线程还可利用yield返回一些值给后者,这些值以resume调用的返回值的形式返回。

以下实例演示了以上各个方法的用法:

coroutine_test.lua 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- coroutine_test.lua 文件
co = coroutine.create(function(i)
print(i);
end)

coroutine.resume(co, 1) -- 1
print(coroutine.status(co)) -- dead

print("----------")

co = coroutine.wrap(function(i)
print(i);
end)

co(1)

print("----------")

co2 = coroutine.create(function()
for i = 1, 10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) -- running
print(coroutine.running()) -- thread:XXXXXX
end
coroutine.yield()
end
end)

coroutine.resume(co2) -- 1
coroutine.resume(co2) -- 2
coroutine.resume(co2) -- 3

print(coroutine.status(co2)) -- suspended
print(coroutine.running())

print("----------")

以上实例执行输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
1
dead
----------
1
----------
1
2
3
running
thread: 0x7fb801c05868 false
suspended
thread: 0x7fb801c04c88 true
----------

coroutine.running就可以看出来,coroutine在底层实现就是一个线程。

当create一个coroutine的时候就是在新线程中注册了一个事件。

当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。

接下来我们分析一个更详细的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function foo(a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end

co = coroutine.create(function(a, b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1)

print("第二次协同程序执行输出", r)
local r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入

print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)

print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")

以上实例执行输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
第一次协同程序执行输出    1    10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 r
main true 11 -9
---分割线---
第三次协同程序执行输出 x y
main true 10 结束协同程序
---分割线---
main false cannot resume dead coroutine
---分割线---

以上实例接下如下:

  • 调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;
  • 协同程序运行;
  • 运行到yield语句;
  • yield挂起协同程序,第一次resume返回;(注意:此处yield返回,参数是resume的参数)
  • 第二次resume,再次唤醒协同程序;(注意:此处resume的参数中,除了第一个参数,剩下的参数将作为yield的参数)
  • yield返回;
  • 协同程序继续运行;
  • 如果使用的协同程序继续运行完成后继续调用 resume方法则输出:cannot resume dead coroutine

resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。


8.3 生产者-消费者问题

现在我就使用Lua的协同程序来完成生产者-消费者这一经典问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
local newProductor

function productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end

function consumer()
while true do
local i = receive() -- 从生产者那里得到物品
print(i)
end
end

function receive()
local status, value = coroutine.resume(newProductor)
return value
end

function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end

-- 启动程序
newProductor = coroutine.create(productor)
consumer()

以上实例执行输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
……

第一次调用(第一次调用的时候,协同程序是一个挂起的状态),resume 的参数 1,10 传入主体函数,打印得出 1,10,之后调用 foo 打印得出 2,程序挂起,之后返回这个值到 resume,作为第二个参数值为 4。

第二次调用 resume 参数为 r ,从主函数中 print(“第二次协同程序执行输出”, r) 开始运行,因为此时的状态是挂起的状态,resume 的参数传入 yield,作为挂起点的返回值为 r。

所以打印得出 r ,之后继续运行,执行 local r, s = coroutine.yield(a + b, a - b),因为此时,并非 resume 直接调用的情况,所以 yield 函数 使用主函数传入的 a,b 参数 作为参数,得出结果为 11, -9,之后 再次挂起。

结果: r 11 -9

第三次调用 resume 参数为 x, y ,从程序挂起点运行,并参数传入 yield 中,yield 此时作为返回值点,所以得出 r,s 结果为 x , y,之后继续运行 return b, “结束协同程序” , 返回 b, 为 10。

结果: x,y true, 10

总结: resume 执行的情况如果(排除第一次执行情况)是挂起的状态,那么 resume 的参数传递给 yield,yield 不论参数表达式形式,返回的值 resume 传递的所有参数。

特别的,注意如果运行之后,再次挂起,那么此时传入的 yield 值,就是主函数的参数值,如果使用的话。

如果 resume 的执行是第一次(上面讲到排除第一次挂起的特殊情况)的情况或者是挂起之后再次运行,那么 resume 的参数 作为主函数的参数。

8.4 实例

8.4.1 yield()和resume()两个函数

1
2
3
4
5
6
co = coroutine.create(function (a)
local r = coroutine.yield(a+1) -- yield()返回a+1给调用它的resume()函数,即2
print("r=" ..r) -- r的值是第2次resume()传进来的,100
end)
status, r = coroutine.resume(co, 1) -- resume()返回两个值,一个是自身的状态true,一个是yield的返回值2
coroutine.resume(co, 100) --resume()返回true

8.4.2 coroutine.creat方法和coroutine.wrap

coroutine.creat方法和coroutine.wrap需要特别注意的是这个返回值的类型,功能上有些类似,但并不完全一样。

coroutine.creat返回的是一个协同程序,类型为thread,需要使用coroutine.resume进行调用;而coroutine.wrap返回的是一个普通的方法(函数),类型为function,和普通function有同样的使用方法,并且不能使用coroutine.resume进行调用。

以下代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
co_creat = coroutine.create(
function()
print("co_creat类型是"..type(co_creat))
end
)

co_wrap = coroutine.wrap(
function()
print("co_wrap类型是"..type(co_wrap))
end
)

coroutine.resume(co_creat)
co_wrap()

输出:

1
2
co_creat类型是thread
co_wrap类型是function

8.4.3 coroutine.resume

coroutine.resume方法需要特别注意的一点是,这个方法只要调用就会返回一个boolean值。

coroutine.resume方法如果调用成功,那么返回true,如果有yield方法,同时返回yield括号里的参数;如果失败,那么返回false,并且带上一句”cannot resume dead coroutine”

以下代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
co_yieldtest = coroutine.create(
function()
coroutine.yield()
coroutine.yield(1)
return 2
end
)

for i = 1,4 do
print("第"..i.."次调用协程:", coroutine.resume(co_yieldtest))
end

输出:

1
2
3
4
第1次调用协程:    true
第2次调用协程: true 1
第3次调用协程: true 2
第4次调用协程: false cannot resume dead coroutine

8.4.4 coroutine.creat

coroutine.creat方法只要建立了一个协程 ,那么这个协程的状态默认就是suspend。使用resume方法启动后,会变成running状态;遇到yield时将状态设为suspend;如果遇到return,那么将协程的状态改为dead。

coroutine.resume方法需要特别注意的一点是,这个方法只要调用就会返回一个boolean值。

coroutine.resume方法如果调用成功,那么返回true;如果有yield方法,同时返回yield括号里的参数;如果没有yield,那么继续运行直到协程结束;直到遇到return,将协程的状态改为dead,并同时返回return的值。

coroutine.resume方法如果调用失败(调用状态为dead的协程会导致失败),那么返回false,并且带上一句”cannot resume dead coroutine”

以下代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function yieldReturn(arg) return arg end

co_yieldtest = coroutine.create(
function()
print("启动协程状态"..coroutine.status(co_yieldtest))
print("--")
coroutine.yield()
coroutine.yield(1)
coroutine.yield(print("第3次调用"))
coroutine.yield(yieldReturn("第4次调用"))
return 2
end
)

print("启动前协程状态"..coroutine.status(co_yieldtest))
print("--")

for i = 1,6 do
print("第"..i.."次调用协程:", coroutine.resume(co_yieldtest))
print("当前协程状态"..coroutine.status(co_yieldtest))
print("--")
end

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
启动前协程状态suspended
--
启动协程状态running
--
第1次调用协程: true
当前协程状态suspended
--
第2次调用协程: true 1
当前协程状态suspended
--
第3次调用
第3次调用协程: true
当前协程状态suspended
--
第4次调用协程: true 第4次调用
当前协程状态suspended
--
第5次调用协程: true 2
当前协程状态dead
--
第6次调用协程: false cannot resume dead coroutine
当前协程状态dead
--

8.4.5 挂起协程

挂起协程: yield 除了挂起协程外,还可以同时返回数据给 resume ,并且还可以同时定义下一次唤醒时需要传递的参数。

1
2
3
4
5
6
7
8
9
10
11
print();
cor = coroutine.create(function(a)
print("参数 a值为:", a);
local b, c = coroutine.yield(a + 1); --这里表示挂起协程,并且将a+1的值进行返回,并且指定下一次唤醒需要 b,c 两个参数。
print("参数 b,c值分别为:", b, c);
return b * c; --协程结束,并且返回 b*c 的值。
end);

print("第一次调用:", coroutine.resume(cor, 1));
print("第二次调用:", coroutine.resume(cor, 2, 2));
print("第三次调用:", coroutine.resume(cor));

执行结果(结果中 true 表示本次调用成功):

1
2
3
4
5
参数 a值为:    1
第一次调用: true 2
参数 b,c值分别为: 2 2
第二次调用: true 4
第三次调用: false cannot resume dead coroutine

九 Lua 文件 I/O

Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式。

  • 简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
  • 完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法

简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。

打开文件操作语句如下:

1
file = io.open (filename [, mode])

mode 的值有:

模式 描述
r 以只读方式打开文件,该文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
r+ 以可读写方式打开文件,该文件必须存在。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a+ 与a类似,但此文件可读可写
b 二进制模式,如果文件是二进制文件,可以加上b
+ 号表示对文件既可以读也可以写

9.1 简单模式

I/O库将当前输入文件作为标准输入(stdin),将当前输出文件作为标准输出(stdout)。这样当我们执行io.read,就是在标准输入中读取一行。

简单模式使用标准的 I/O 或使用一个当前输入文件和一个当前输出文件。

  • io.input([file])设置默认的输入文件,file为文件名(此时会以文本读入)或文件句柄(可以理解为把柄,有了把柄就可以找到文件),返回文件句柄。
  • io.output([file])设置默认的输出文件,参数意义同上。
  • io.close([file]) 关闭文件,不带参数关闭默认的文件
  • io.read(formats) 读入默认文件,formats取值为"*a"(读入全部)、“*n”(按数字读入)、 "*l"(按行读入,默认方式)、n(即数字,读取n个字符)。
  • io.lines([fn]) fn文件名,若无文件,取默认文件,返回一个迭代器,可以用在for循环里。
  • io.write(value) 向默认文件写入内容。
  • io.flush()把文件缓存里的操作立即作用到默认输出文件。

以下为 file.lua 文件代码,操作的文件为test.lua(如果没有你需要创建该文件),代码如下:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 以只读方式打开文件
file = io.open("test.lua", "r")

-- 设置默认输入文件为 test.lua
io.input(file)

-- 输出文件第一行
print(io.read())

-- 关闭打开的文件
io.close(file)

-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")

-- 设置默认输出文件为 test.lua
io.output(file)

-- 在文件最后一行添加 Lua 注释
io.write("-- test.lua 文件末尾注释")

-- 关闭打开的文件
io.close(file)

执行以上代码,你会发现,输出了 test.lua 文件的第一行信息,并在该文件最后一行添加了 lua 的注释。如我这边输出的是:

1
-- test.lua 文件

注:Write函数与print函数不同在于,write不附加任何额外的字符到输出中去,例如制表符,换行符等等。还有write函数是使用当前输出文件,而print始终使用标准输出。另外print函数会自动调用参数的tostring方法,所以可以显示出表(tables)函数(functions)和nil。

read函数:从当前输入文件读取串,由它的参数控制读取的内容:

在以上实例中我们使用了 io.”x” 方法,其中 io.read() 中我们没有带参数,参数可以是下表中的一个:

模式 描述
“*n” 读取一个数字并返回它。例:file.read(“*n”)
“*a” 从当前位置读取整个文件。例:file.read(“*a”)
“*l”(默认) 读取下一行,在文件尾 (EOF) 处返回 nil。例:file.read(“*l”)
number 返回一个指定字符个数的字符串,或在 EOF 时返回 nil。例:file.read(5)

其他的 io 方法有:

  • io.tmpfile():返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除
  • io.type(file): 检测obj是否一个可用的文件句柄
  • io.flush(): 向文件写入缓冲中的所有数据
  • io.lines(optional file name): 返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil,但不关闭文件。

9.2 完全模式

简单模型里只能处理文本类型的文件,在完整模型里可以处理二进制文件。

处理文件的一般流程为:打开文件,获取文件的句柄;以文件句柄操作文件;关闭文件。

可以看到完整模型比简单模型复杂点,但优点是功能更强大。

io.open (fn [, m]) 打开文件,返回文件句柄,fn文件名,m模式有:

  • r 以只读方式打开文件,该文件必须存在。

  • w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。

  • a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)

  • r+ 以可读写方式打开文件,该文件必须存在。

  • w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。

  • a+ 与a类似,但此文件可读可写

  • b 二进制模式,如果文件是二进制文件,可以加上b

  • + 号表示对文件既可以读也可以写

以下的file是io.open返回的文件句柄,功能与简单模式类似,不再详述。

  • file :close ()

  • file :read ( formats )

  • file :lines ()

  • file :write ( values )

  • file :seek ([p] [, of]) 设置文件读写的偏移,p文件偏移起始位置(取值有”set”,文件头,此为默认值,”cur”当前位置、”end”文件尾),of偏移量(默认值0,正的表示向前,负的表示向后),返回在文件里新的当前位置。

  • file :flush ()

  • 完全模式的核心在于文件句柄(file handle)。该结构类似于C语言中的文件流(FILE*),其呈现了一个打开的文件以及当前存取位置。打开一个文件的函数是io.open。它模仿C语言中的fopen函数,同样需要打开文件的文件名参数,打开模式的字符串参数:

通常我们需要在同一时间处理多个文件。我们需要使用 file:function_name 来代替 io.function_name 方法。以下实例演示了如何同时处理同一个文件:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 以只读方式打开文件
file = io.open("test.lua", "r")

-- 输出文件第一行
print(file:read())

-- 关闭打开的文件
file:close()

-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")

-- 在文件最后一行添加 Lua 注释
file:write("--test")

-- 关闭打开的文件
file:close()

执行以上代码,你会发现,输出了 test.lua 文件的第一行信息,并在该文件最后一行添加了 lua 的注释。如我这边输出的是:

1
-- test.lua 文件

read 的参数与简单模式一致。

其他方法:

  • file:seek(optional whence, optional offset): 设置和获取当前文件位置,成功则返回最终的文件位置(按字节),失败则返回nil加错误信息。参数 whence 值可以是:

    • “set”: 从文件头开始
    • “cur”: 从当前位置开始[默认]
    • “end”: 从文件尾开始
    • offset:默认为0

    不带参数file:seek()则返回当前位置,file:seek(“set”)则定位到文件头,file:seek(“end”)则定位到文件尾并返回文件大小

  • file:flush(): 向文件写入缓冲中的所有数据

  • io.lines(optional file name): 打开指定的文件 filename 为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil,并自动关闭文件。
    若不带参数时io.lines() <=> io.input():lines(); 读取默认输入设备的内容,但结束时不关闭文件,如:

    1
    2
    3
    4
    5
    for line in io.lines("main.lua") do

      print(line)

      end

以下实例使用了 seek 方法,定位到文件倒数第 25 个位置并使用 read 方法的 *a 参数,即从当前位置(倒数第 25 个位置)读取整个文件。

1
2
3
4
5
6
7
8
-- 以只读方式打开文件
file = io.open("test.lua", "r")

file:seek("end",-25)
print(file:read("*a"))

-- 关闭打开的文件
file:close()

我这边输出的结果是:

1
st.lua 文件末尾--test

9.3 示例

9.3.1 示例1

使用 *n 作为参数读取文件中的数字的时候,只有文件中第一个字符是数字(或者空格加数字)的情况下才能读取到并返回这个数字:

1
2
3
4
file = io.open("test.txt","r")
io.input(file)
print(io.read("*n"))
io.close(file)

test.txt 文件内容:

1
2
 321
aaa

输出:

1
321

9.3.2 示例2

1.若使用LuaStudio,其中io.open()的默认查找路径是LuaStudio软件的根目录下,若需要open其他路径的文件,使用绝对路径即可;

2.需要注意的是,简单模式下,io.read()不能放在io.write()后面,否则,虽然不会报错,但会打印出奇怪的字符,打开文件看,会发现多了一串乱码。

1
2
3
4
5
t=io.open("t.lua","a+")
io.input(t)
io.output(t)
io.write("test") --test
print(io.read()) --?(或者其他符号)

3.若需要io.write后io.read,则需要先关闭文件,然后再打开文件查看

1
2
3
4
5
6
7
t=io.open("t.lua","a+")
io.output(t)
io.write("test") --test
io.close(t)
t=io.open("t.lua","a+")
io.input(t)
print(io.read()) --test

4.简单模式下,当使用io.close()关闭文件的时候,之前设置默认输入文件和默认输出文件会失效,当你使用io.open()重新打开文件时,则需要重新设置

5.关于 *n ,还有一点需要注意,若数字中有非数字的字符的话,会返回该字符前的数字,如:

1
2
3
t=io.open("t.lua","a+")
io.input(t)
print(io.read("*n"))

t.lua文件内容是:

1
123456test789

输出:

1
123456

9.3.3 文件路径

文件操作中的绝对地址,从资源管理器复制的话,需要反斜杠前面再加一个,因为反斜杠本身是转义字符,下面的文件路径:

1
D:\Lua\5.1\SciTE\codes\txts\a1.txt

应该是:

1
t = io.open("D:\\Lua\\5.1\\SciTE\\codes\\txts\\a1.txt", "r+")

9.3.4 完整实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
------------------简单模型-----------------
-- 读
local file1 = io.input("1.txt") -- 当前目录"1.txt"要存在,不然出错
local str = io.read("*a")
print(str)
-- 写
local file2 = io.output("2.txt") -- 当前目录"2.txt"不需要存在
io.write(str)
io.flush()
io.close()

-- 利用这几个函数可以做一个文件复制的函数
function copy(fileA, fileB)
local file1 = io.input(fileA)
if not file1 then
print(fileA .. "不存在")
return
end
local str = io.read("*a")
local file2 = io.output(fileB)
io.write(str)
io.flush()
io.close()
end

for line in io.lines("1.txt") do
print(line)
end
------------------完整模型-----------------
local f = io.open("3.txt", "a+")
f:write("Happy New Year!")
f:flush()

f:seek("end", -1) -- 定位到文件末尾前一个字节
local str = f:read(1) -- 读取一个字符
print(str) -- 输出"!"
f:close()

十 Lua 错误处理

程序运行中错误处理是必要的,在我们进行文件操作,数据转移及web service 调用过程中都会出现不可预期的错误。如果不注重错误信息的处理,就会造成信息泄露,程序无法运行等情况。

任何程序语言中,都需要错误处理。错误类型有:

  • 语法错误
  • 运行错误

10.1 语法错误

语法错误通常是由于对程序的组件(如运算符、表达式)使用不当引起的。一个简单的实例如下:

1
2
-- test.lua 文件
a == 2

以上代码执行结果为:

1
lua: test.lua:2: syntax error near '=='

正如你所看到的,以上出现了语法错误,一个 “=” 号跟两个 “=” 号是有区别的。一个 “=” 是赋值表达式两个 “=” 是比较运算。

实例

1
2
3
for a= 1,10
print(a)
end

执行以上程序会出现如下错误:

1
lua: test2.lua:2: 'do' expected near 'print'

语法错误比程序运行错误更简单,运行错误无法定位具体错误,而语法错误我们可以很快的解决,如以上实例我们只要在for语句下添加 do 即可:

1
2
3
4
for a= 1,10
do
print(a)
end

10.2 运行错误

运行错误是程序可以正常执行,但是会输出报错信息。如下实例由于参数输入错误,程序执行时报错:

1
2
3
4
5
function add(a,b)
return a+b
end

add(10)

当我们编译运行以下代码时,编译是可以成功的,但在运行的时候会产生如下错误:

1
2
3
4
5
lua: test2.lua:2: attempt to perform arithmetic on local 'b' (a nil value)
stack traceback:
test2.lua:2: in function 'add'
test2.lua:5: in main chunk
[C]: ?

lua 里调用函数时,即使实参列表和形参列表不一致也能成功调用,多余的参数会被舍弃,缺少的参数会被补为 nil。

以上报错信息是由于参数 b 被补为 nil 后,nil 参与了 + 运算。

假如 add 函数内不是 “return a+b” 而是 “print(a,b)” 的话,结果会变成 “10 nil” 不会报错。


10.3 错误处理

我们可以使用两个函数:assert 和 error 来处理错误。实例如下:

实例

1
2
3
4
5
6
local function add(a,b)
assert(type(a) == "number", "a 不是一个数字")
assert(type(b) == "number", "b 不是一个数字")
return a+b
end
add(10)

执行以上程序会出现如下错误:

1
2
3
4
5
6
lua: test.lua:3: b 不是一个数字
stack traceback:
[C]: in function 'assert'
test.lua:3: in local 'add'
test.lua:6: in main chunk
[C]: in ?

实例中assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。

10.4 error函数

语法格式:

1
error (message [, level])

功能:终止正在执行的函数,并返回message的内容作为错误信息(error函数永远都不会返回)

通常情况下,error会附加一些错误位置的信息到message头部。

Level参数指示获得错误的位置:

  • Level=1[默认]:为调用error位置(文件+行号)
  • Level=2:指出哪个调用error的函数的函数
  • Level=0:不添加错误位置信息

10.5 pcall 和 xpcall、debug

Lua中处理错误,可以使用函数pcall(protected call)来包装需要执行的代码。

pcall接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值true或者或false, errorinfo。

语法格式如下

1
2
3
4
5
if pcall(function_name, ….) then
-- 没有错误
else
-- 一些错误
end

简单实例:

1
2
3
4
5
6
7
> =pcall(function(i) print(i) end, 33)
33
true

> =pcall(function(i) print(i) error('error..') end, 33)
33
false stdin:1: error..
1
2
3
> function f() return false,2 end
> if f() then print '1' else print '0' end
0

pcall以一种”保护模式”来调用第一个参数,因此pcall可以捕获函数执行中的任何错误。

通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但pcall返回时,它已经销毁了调用桟的部分内容。

Lua提供了xpcall函数,xpcall接收第二个参数——一个错误处理函数,当错误发生时,Lua会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用debug库来获取关于错误的额外信息了。

debug库提供了两个通用的错误处理函数:

  • debug.debug:提供一个Lua提示符,让用户来检查错误的原因
  • debug.traceback:根据调用桟来构建一个扩展的错误消息
1
2
3
4
5
6
7
8
9
10
>=xpcall(function(i) print(i) error('error..') end, function() print(debug.traceback()) end, 33)
33
stack traceback:
stdin:1: in function <stdin:1>
[C]: in function 'error'
stdin:1: in function <stdin:1>
[C]: in function 'xpcall'
stdin:1: in main chunk
[C]: in ?
false nil

xpcall 使用实例 2:

1
2
3
4
5
6
7
8
9
10
function myfunction ()
n = n/nil
end

function myerrorhandler( err )
print( "ERROR:", err )
end

status = xpcall( myfunction, myerrorhandler )
print( status)

执行以上程序会出现如下错误:

1
2
ERROR:    test2.lua:2: attempt to perform arithmetic on global 'n' (a nil value)
false

十一 Lua 垃圾回收

Lua 采用了自动内存管理。 这意味着你不用操心新创建的对象需要的内存如何分配出来, 也不用考虑在对象不再被使用后怎样释放它们所占用的内存。

Lua 运行了一个垃圾收集器来收集所有死对象 (即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。 Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、 内部结构等,都服从自动管理。

Lua 实现了一个增量标记-扫描收集器。 它使用这两个数字来控制垃圾收集循环: 垃圾收集器间歇率和垃圾收集器步进倍率。 这两个数字都使用百分数为单位 (例如:值 100 在内部表示 1 )。

垃圾收集器间歇率控制着收集器需要在开启新的循环前要等待多久。 增大这个值会减少收集器的积极性。 当这个值比 100 小的时候,收集器在开启新的循环前不会有等待。 设置这个值为 200 就会让收集器等到总内存使用量达到 之前的两倍时才开始新的循环。

垃圾收集器步进倍率控制着收集器运作速度相对于内存分配速度的倍率。 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。 不要把这个值设得小于 100 , 那样的话收集器就工作的太慢了以至于永远都干不完一个循环。 默认值是 200 ,这表示收集器以内存分配的”两倍”速工作。

如果你把步进倍率设为一个非常大的数字 (比你的程序可能用到的字节数还大 10% ), 收集器的行为就像一个 stop-the-world 收集器。 接着你若把间歇率设为 200 , 收集器的行为就和过去的 Lua 版本一样了: 每次 Lua 使用的内存翻倍时,就做一次完整的收集。


11.1 垃圾回收器函数

Lua 提供了以下函数collectgarbage ([opt [, arg]])用来控制自动内存管理:

  • collectgarbage(“collect”): 做一次完整的垃圾收集循环。通过参数 opt 它提供了一组不同的功能:
  • collectgarbage(“count”): 以 K 字节数为单位返回 Lua 使用的总内存数。 这个值有小数部分,所以只需要乘上 1024 就能得到 Lua 使用的准确字节数(除非溢出)。
  • collectgarbage(“restart”): 重启垃圾收集器的自动运行。
  • collectgarbage(“setpause”): 将 arg 设为收集器的 间歇率。 返回 间歇率 的前一个值。
  • collectgarbage(“setstepmul”): 返回 步进倍率 的前一个值。
  • collectgarbage(“step”): 单步运行垃圾收集器。 步长”大小”由 arg 控制。 传入 0 时,收集器步进(不可分割的)一步。 传入非 0 值, 收集器收集相当于 Lua 分配这些多(K 字节)内存的工作。 如果收集器结束一个循环将返回 true 。
  • collectgarbage(“stop”): 停止垃圾收集器的运行。 在调用重启前,收集器只会因显式的调用运行。

以下演示了一个简单的垃圾回收实例:

1
2
3
4
5
6
7
8
9
10
11
mytable = {"apple", "orange", "banana"}

print(collectgarbage("count"))

mytable = nil

print(collectgarbage("count"))

print(collectgarbage("collect"))

print(collectgarbage("count"))

执行以上程序,输出结果如下(注意内存使用的变化):

1
2
3
4
20.9560546875
20.9853515625
0
19.4111328125
1
2
3
collectgarbage("setpause", 200) : 内存增大 2 倍(200/100)时自动释放一次内存 (200 是默认值)。

collectgarbage("setstepmul", 200) :收集器单步收集的速度相对于内存分配速度的倍率,设置 200 的倍率等于 2 倍(200/100)。(200 是默认值)

十二 Lua 面向对象

12.1 Lua 中面向对象

我们知道,对象由属性和方法组成。LUA中最基本的结构是table,所以需要用table来描述对象的属性。

lua 中的 function 可以用来表示方法。那么LUA中的类可以通过 table + function 模拟出来。

至于继承,可以通过 metetable 模拟出来(不推荐用,只模拟最基本的对象大部分实现够用了)。

Lua 中的表不仅在某种意义上是一种对象。像对象一样,表也有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值的对象(table)代表两个不同的对象;一个对象在不同的时候也可以有不同的值,但他始终是一个对象;与对象类似,表的生命周期与其由什么创建、在哪创建没有关系。对象有他们的成员函数,表也有:

1
2
3
4
Account = {balance = 0}
function Account.withdraw (v)
Account.balance = Account.balance - v
end

这个定义创建了一个新的函数,并且保存在Account对象的withdraw域内,下面我们可以这样调用:

1
Account.withdraw(100.00)

12.2 一个简单实例

12.2.1 创建类

以下简单的类包含了三个属性: area, length 和 breadth,printArea方法用于打印计算结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 元类
Rectangle = {
area = 0,
length = 0,
breadth = 0
}

-- 派生类的方法 new
function Rectangle:new(o, length, breadth)
o = o or {}
setmetatable(o, self)
self.__index = self
self.length = length or 0
self.breadth = breadth or 0
self.area = length * breadth;
return o
end

-- 派生类的方法 printArea
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end

12.2.2 创建对象

创建对象是为类的实例分配内存的过程。每个类都有属于自己的内存并共享公共数据。

1
r = Rectangle:new(nil,10,20)

12.2.3 访问属性

我们可以使用点号(.)来访问类的属性:

1
print(r.length)

12.2.4 访问成员函数

我们可以使用冒号 : 来访问类的成员函数:

1
r:printArea()

内存在对象初始化时分配。

12.2.5 完整实例

以下我们演示了 Lua 面向对象的完整实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 元类
Shape = {
area = 0
}

-- 基础类方法 new
function Shape:new(o, side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side * side;
return o
end

-- 基础类方法 printArea
function Shape:printArea()
print("面积为 ", self.area)
end

-- 创建对象
myshape = Shape:new(nil, 10)

myshape:printArea()

执行以上程序,输出结果为:

1
面积为     100

12.3 Lua 继承

继承是指一个对象直接使用另一对象的属性和方法。可用于扩展基础类的属性和方法。

以下演示了一个简单的继承实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Meta class
Shape = {
area = 0
}
-- 基础类方法 new
function Shape:new(o, side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side * side;
return o
end
-- 基础类方法 printArea
function Shape:printArea()
print("面积为 ", self.area)
end

接下来的实例,Square 对象继承了 Shape 类:

1
2
3
4
5
6
7
8
Square = Shape:new()
-- Derived class method new
function Square:new (o,side)
o = o or Shape:new(o,side)
setmetatable(o, self)
self.__index = self
return o
end

完整实例

以下实例我们继承了一个简单的类,来扩展派生类的方法,派生类中保留了继承类的成员变量和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
-- Meta class
Shape = {
area = 0
}
-- 基础类方法 new
function Shape:new(o, side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side * side;
return o
end
-- 基础类方法 printArea
function Shape:printArea()
print("面积为 ", self.area)
end

-- 创建对象
myshape = Shape:new(nil, 10)
myshape:printArea()

Square = Shape:new()
-- 派生类方法 new
function Square:new(o, side)
o = o or Shape:new(o, side)
setmetatable(o, self)
self.__index = self
return o
end

-- 派生类方法 printArea
function Square:printArea()
print("正方形面积为 ", self.area)
end

-- 创建对象
mysquare = Square:new(nil, 10)
mysquare:printArea()

Rectangle = Shape:new()
-- 派生类方法 new
function Rectangle:new(o, length, breadth)
o = o or Shape:new(o)
setmetatable(o, self)
self.__index = self
self.area = length * breadth
return o
end

-- 派生类方法 printArea
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end

-- 创建对象
myrectangle = Rectangle:new(nil, 10, 20)
myrectangle:printArea()

执行以上代码,输出结果为:

1
2
3
面积为     100
正方形面积为 100
矩形面积为 200

12.4 函数重写

Lua 中我们可以重写基础类的函数,在派生类中定义自己的实现方式:

1
2
3
4
-- 派生类方法 printArea
function Square:printArea ()
print("正方形面积 ",self.area)
end

12.5 .: 的区别

.: 的区别在于使用 : 定义的函数隐含 self 参数,使用 : 调用函数会自动传入 tableself 参数,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
classA = {}
function classA:getob(name)
print(self)
ob = {}
setmetatable(ob, self)
self.__index = self
self.name = name
return ob
end

function classA:getself()
return self
end

c1 = classA:getob("A")
c2 = classA:getob("B")
print(string.rep("*", 30))
print(c1:getself())
print(c2:getself())
print(string.rep("*", 30))

----------------------继承------------------------

classB = classA:getob() ----非常重要,用于获取继承的self
function classB:getob(name, address)
ob = classA:getob(name)
setmetatable(ob, self)
self.__index = self
self.address = address
return ob
end

c3 = classB:getob("gray.yang", "shenzhen")
print(c3:getself())

输出结果:

1
2
3
4
5
6
7
8
9
table: 0x7fc99d404c80
table: 0x7fc99d404c80
******************************
table: 0x7fc99d402c50
table: 0x7fc99d404cc0
******************************
table: 0x7fc99d404c80
table: 0x7fc99d404c80
table: 0x7fc99d405640

lua编程中,经常遇到函数的定义和调用,有时候用点号调用,有时候用冒号调用,这里简单的说明一下原理。如:

12.5.1 点号调用:

1
2
3
4
5
6
7
8
9
10
11
-- 点号定义和点号调用:
girl = {
money = 200
}

function girl.goToMarket(girl, someMoney)
girl.money = girl.money - someMoney
end

girl.goToMarket(girl, 100)
print(girl.money)

12.5.2 引用参数self:

1
2
3
4
5
6
7
8
9
-- 参数self指向调用者自身(类似于c++里的this 指向当前类)
girl = {money = 200}

function girl.goToMarket(self ,someMoney)
self.money = self.money - someMoney
end

girl.goToMarket(girl, 100)
print(girl.money)

12.5.3 冒号调用:

1
2
3
4
5
6
7
8
9
-- 冒号定义和冒号调用:
girl = {money = 200}

function girl:goToMarket(someMoney)
self.money = self.money - someMoney
end

girl:goToMarket(100)
print(girl.money)

冒号定义和冒号调用其实跟上面的效果一样,只是把第一个隐藏参数省略了,而该参数self指向调用者自身。

总结:冒号只是起了省略第一个参数self的作用,该self指向调用者本身,并没有其他特殊的地方。

引用博文:http://www.xuebuyuan.com/1613223.html

12.6 模拟类和继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
classA = {}

function classA.new(cls, ...) -- 定义类方法时使用"."号,不适用隐式传参
this = {}
setmetatable(this, cls)
cls.__index = cls -- 将元表的__index设为自身,访问表的属性不存在时会搜索元表
cls.init(this, ...) -- 初始化表,注意访问类的方法都是".",此时不会隐式传入参数
return this
end

function classA.init(self, name)
self.name = name
end

function classA.getname(self)
return self.name
end

p = classA:new("gray.yang")
print(p:getname())
print(string.rep("*", 50))

输出为

1
2
gray.yang
**************************************************

12.7 多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
-- 在table 'plist'中查找'k'
local function search(k, plist)
for i = 1, #plist do
local v = plist[i][k] -- 尝试第i个基类
if v then
return v
end
end
end

function createClass(...)
local c = {} -- 新类
local parents = {...}

-- 类在其父类列表中的搜索方法
setmetatable(c, {
__index = function(t, k)
return search(k, parents)
end
})

-- 将'c'作为其实例的元表
c.__index = c

-- 为这个新类定义一个新的构造函数
function c:new(o)
o = o or {}
setmetatable(o, c)
return o
end

return c -- 返回新类
end

-- 类Named
Named = {}
function Named:getname()
return self.name
end

function Named:setname(n)
self.name = n
end

-- 类Account
Account = {
balance = 0
}
function Account:withdraw(w)
self.balance = self.balance - v
end

-- 创建一个新类NamedAccount,同时从Account和Named派生
NamedAccount = createClass(Account, Named)

account = NamedAccount:new()
account:setname("Ives")
print(account:getname()) -- 输出 Ives

十三 Lua 数据库访问

本文主要为大家介绍 Lua 数据库的操作库:LuaSQL。他是开源的,支持的数据库有:ODBC, ADO, Oracle, MySQL, SQLite 和 PostgreSQL。

本文为大家介绍MySQL的数据库连接。

LuaSQL 可以使用 LuaRocks 来安装可以根据需要安装你需要的数据库驱动。

LuaRocks 安装方法:

1
2
3
4
5
6
7
8
$ wget http://luarocks.org/releases/luarocks-2.2.1.tar.gz
$ tar zxpf luarocks-2.2.1.tar.gz
$ cd luarocks-2.2.1
$ ./configure; sudo make bootstrap
$ sudo luarocks install luasocket
$ lua
Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> require "socket"

Window 下安装 LuaRocks:https://github.com/keplerproject/luarocks/wiki/Installation-instructions-for-Windows

安装不同数据库驱动:

1
2
3
4
5
luarocks install luasql-sqlite3
luarocks install luasql-postgres
luarocks install luasql-mysql
luarocks install luasql-sqlite
luarocks install luasql-odbc

你也可以使用源码安装方式,Lua Github 源码地址:https://github.com/keplerproject/luasql

Lua 连接MySql 数据库:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require "luasql.mysql"

--创建环境对象
env = luasql.mysql()

--连接数据库
conn = env:connect("数据库名","用户名","密码","IP地址",端口)

--设置数据库的编码格式
conn:execute"SET NAMES UTF8"

--执行数据库操作
cur = conn:execute("select * from role")

row = cur:fetch({},"a")

--文件对象的创建
file = io.open("role.txt","w+");

while row do
var = string.format("%d %s\n", row.id, row.name)

print(var)

file:write(var)

row = cur:fetch(row,"a")
end


file:close() --关闭文件对象
conn:close() --关闭数据库连接
env:close() --关闭数据库环境

file:close() —关闭文件对象
conn:close() —关闭数据库连接
env:close() —关闭数据库环境

注意

5.2 版本之后,require 不再定义全局变量,需要保存其返回值。

1
require "luasql.mysql"

需要写成:

1
luasql = require "luasql.mysql"