对 Lua 语言基础的梳理

Lua 的目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

特点是轻量级、可扩展、面向过程编程和函数式编程、自动内存管理、语言内置模式匹配、闭包(closure)、函数也可以看做一个值、提供多线程(协同进程,并非操作系统所支持的线程)支持、通过闭包和 table 可以支持面向对象编程:数据抽象,虚函数,继承和重载。

1
print("Hello World!")

两个减号是单行注释: –
多行注释:

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

Lua 是一个区分大小写的编程语言。

Lua 的保留关键字有:

1
2
3
4
5
6
and	break	do	else
elseif end false for
function if in local
nil not or repeat
return then true until
while

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

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

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

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

当且仅当一个变量不等于 nil 时,这个变量即存在。

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

Lua 中有 8 个基本类型分别为:

1
2
3
4
5
nil、boolean、number、string、
userdata (表示任意存储在变量中的 C 数据结构)、
function (由 C 或 Lua 编写的函数)、
thread (表示执行的独立线路,用于执行协同程序) 、
table (是一个 "关联数组" associative arrays,数组的索引可以是数字或者是字符串)。

Lua 默认只有一种 number 类型 即 double(双精度)类型。

字符串由一对双引号或单引号来表示。
也可以用 2 个方括号 “[[]]” 来表示”一块”字符串。

1
2
3
4
5
6
7
8
9
10
html = [[
<html>
<head></head>
<body>

</body>
</html>
]]

print(html)

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

1
2
3
4
5
6
> print("2" + 6)
8.0
> print("2" + "6")
8.0
> print("2 + 6")
2 + 6

字符串连接使用的是 ..

1
2
> print("a" .. 'b')
ab

使用 # 来计算字符串的长度,放在字符串前面

1
2
3
> len = "www.w3cschool.cc"
> print(#len)
16

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

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

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

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

1
2
a["key"] = "value"
a[10] = 100

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

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


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


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


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


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

Lua 中的变量默认全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量。局部变量的作用域为从声明位置开始到所在语句块结束。变量的默认值均为 nil。

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

遇到赋值语句 Lua 会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:x, y = y, x -- swap 'x' for 'y'
变量个数 > 值的个数 按变量个数补足 nil;
变量个数 < 值的个数 多余的值会被忽略;


循环

1
2
3
4
while( true )
do
print("循环将永远执行下去")
end
1
2
3
for var = exp1,exp2,exp3 do  
<执行体>
end

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

1
2
3
repeat
statements
until( condition )

Lua 认为 false 和 nil 为假,true 和非 nil 为真
Lua 中 0 为 true

1
2
3
if(0) then
print("0 为 true")
end

逻辑运算符

1
2
3
and	逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。
or 逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。
not 逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。

.. 连接两个字符串。
# 一元运算符,返回字符串或表的长度


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

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

string.upper(argument):
字符串全部转为大写字母。

string.lower(argument):
字符串全部转为小写字母。

string.gsub(mainString,findString,replaceString,num)
在字符串中替换, mainString 为要替换的字符串, findString 为被替换的字符,replaceString 要替换的字符,num 替换次数(可以忽略,则全部替换)

string.strfind (str, substr, [init, [end]])
在一个指定的目标字符串中搜索指定的内容(第三个参数为索引),返回其具体位置。不存在则返回 nil。

string.reverse(arg)
字符串反转

string.format(…)
返回一个类似 printf 的格式化字符串

string.char(arg) 和 string.byte(arg[,int])
char 将整型数字转成字符并连接, byte 转换字符为整数值(可以指定某个字符,默认第一个字符)。

string.len(arg)
计算字符串长度。

string.rep(string, n))
返回字符串 string 的 n 个拷贝

—- 并不华丽的分割线 —-

数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。
在 Lua 索引值是以 1 为起始,但你也可以指定 0 开始。
除此外我们还可以以负数为数组索引值。

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

迭代器是用于遍历集合或容器中元素的一种结构。在 Lua 语言中,集合往往指的是可以用来创建各种数据结构的表。比如,数组就是用表来创建的。
通用迭代器可以访问集合中的键值对,Lua 提供默认迭代器函数 ipairs。

无状态迭代器,这一类的迭代器函数中不会保存任何中间状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function square(iteratorMaxCount, currentNumber)
if currentNumber <iteratorMaxCount>
then
currentNumber = currentNumber + 1
return currentNumber, currentNumber * currentNumber
end
end

function squares(iteratorMaxCount)
return square, iteratorMaxCount, 0
end

for i,n in squares(3)
do
print(i,n)
end

有状态迭代器,在 Lua 中可以使用闭包来存储当前元素的状态。闭包通过函数调用得到变量的值。为了创建一个新的闭包,我们需创建两个函数,包括闭包函数本身和一个工厂函数,其中工厂函数用于创建闭包。


在 Lua 语言中,表是唯一可以用来创建不同数据类型的数据结构,比如常见的数组和字典都是用表来创建的。表没有固定的大小,当数据量增加时表会自动增大。表被称之为对象,它既不是值也不是变量。Lua 用构造表达式 {} 创建一个空表。需要注意的是,在存储表的变量和表本身之间没有什么固定的对应关系(注:一个表可以被不同的变量引用,一个变量也可以随时改变其所引用的表对象)。

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

--简单的表赋值
mytable[1]= "Lua"

--移除引用
mytable = nil
-- lua 的垃圾回收机制负责回收内存空间

当我们有一个拥有一系列元素的表时,如果我们将其赋值给 b。那么 a 和 b 都会引用同一个表对象(a 先引用该表),指向相同的内存空间。而不会再单独为 b 分配内存空间。即使给变量 a 赋值 nil,我们仍然可以用变量 b 访问表本身。如果已经没有变量引用表时,Lua 语言垃圾回收机制负责回收不再使用的内存以被重复使用。

下面的示例代码使用到了上面提到的表的特性。  

Lua 语言内置的表操作函数:

table.concat(table[, sep [, i[,j]]]): 根据指定的参数合并表中的字符串。

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

-- 返回表中字符串连接后的结果 bananaorangeapple
print("Concatenated string ",table.concat(fruits))

--用字符串连接 banana, orange, apple
print("Concatenated string ",table.concat(fruits,", "))

--基于索引连接 orange, apple
print("Concatenated string ",table.concat(fruits,", ", 2,3))

table.insert(table,[pos,]value):在表中指定位置插入一个值。
table.maxn(table):返回表中最大的数值索引。
table.remove(table[,pos]):从表中移出指定的值。
table.sort(table[,comp]):根据指定的(可选)比较方法对表进行排序操作。

sort 函数默认使用字母表对表中的元素进行排序(可以通过提供比较函数改变排序策略)。


Lua 中的模块与库的概念相似,每个模块都有一个全局唯一名字,并且每个模块都包含一个表。使用一个模块时,可以使用 require 加载模块。模块中可以包括函数和变量,所有这些函数和变量被表存储于模块的表中。模块中的表的功能类似于命名空间,用于隔离不同模块中的相同的变量名。在使用模块的时候,我们应该遵守模块定义的规范,在 require 加载模块时返回模块中的包含函数和变量的表对象。

模块中表的使用使得我们可在绝大多数情况下可以像操作其它表一样操作模块。由于 Lua 语言允许对模块本身进行操作,所以 Lua 也就具备了许多其它语言需要特殊机制才能实现的特殊性质。例如,这种自由的表操作机制使得编程人员可以用多种方法调用模块中的函数。下面的例子演示了其中的一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
-- 方法 1
require "printFormatter"
printFormatter.simpleFormat("test")

-- 方法 2
local formatter = require "printFormatter"
formatter.simpleFormat("test")

-- 方法 3
require "printFormatter"
local formatterFunction = printFormatter.simpleFormat
formatterFunction("test")

Lua 提供了一个高层次抽象的函数 require,使用这个函数可以加载所有需要的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local mymath =  {}
function mymath.add(a,b)
print(a+b)
end

function mymath.sub(a,b)
print(a-b)
end

function mymath.mul(a,b)
print(a*b)
end

function mymath.div(a,b)
print(a/b)
end

return mymath

在另一个文件 moduletutorial.lua 文件中访问这个模块。

1
2
3
4
5
mymathmodule = require("mymath")
mymathmodule.add(10,20)
mymathmodule.sub(30,20)
mymathmodule.mul(10,20)
mymathmodule.div(30,20)

注意的几点:
把模块和待运行的文件放在相同的目录下;
模块的名称与文件名称相同;
为 require 函数返回模块(在模块中使用 return 命令返回存储了函数和变量的表)。尽管有其它的模块实现的方式,但是建议您使用上面的实现方法;

 

可以通过设置元表的键和相关方法来改变表的行为。元方法的功能十分强大,使用元方法可以实现很多的功能:修改表的操作符功能或为操作符添加新功能(实现操作的重载)、使用元表中的 __index 方法,我们可以实现在表中查找键不存在时转而在元表中查找键值的功能;

Lua 提供了两个十分重要的用来处理元表的方法:setmetatable(table,metatable):此方法用于为一个表设置元表; getmetatable(table):此方法用于获取表的元表对象。

1
2
3
4
-- 将一个表设置为另一个表的元表
mytable = {}
mymetatable = {}
setmetatable(mytable, mytatable)

简写成一行代码:mytable = setmetatable({}, {})


协程具有协同的性质,它允许两个或多个方法以某种可控的方式协同工作。在任何一个时刻,都只有一个协程在运行,只有当正在运行的协程主动挂起时它的执行才会被挂起(暂停)。当我们使用 resume 函数调用一个协程时,协程才开始执行。当在协程调用 yield 函数时,协程挂起执行。再次调用 resume 函数时,协程再从上次挂起的地方继续执行。这个过程一直持续到协程执行结束为止。

协程与线程的区别在于协程不能并发,任意时刻只会有一个协程执行,而线程允许并发的存在。

Lua 为支持协程提供的函数和功能:

coroutine.create(f): 用函数 f 创建一个协程,返回 thread 类型对象。
coroutine.resume(co[,val1,…]): 传入参数(可选),重新执行协程 co。此函数返回执行状态,也可以返回其它值。
coroutine.running(): 返回正在运行的协程,如果在主线程中调用此函数则返回 nil。
coroutine.status(co): 返回指定协程的状态,状态值允许为:正在运行(running),正常(normal),挂起(suspended),结束(dead)。
coroutine.wrap(f): 与前面 coroutine.create 一样,coroutine.wrap 函数也创建一个协程,与前者返回协程本身不同,后者返回一个函数。当调用该函数时,重新执行协程。
coroutine.yield(…): 挂起正在执行的协程。为此函数传入的参数值作为执行协程函数 resume 的额外返回值(默认会返回协程执行状态)。


Lua 的文件 IO 库用于读取或操作文件。Lua IO 库提供两类文件操作,它们分别是隐式文件描述符(implict file descriptors)和显式文件描述符 (explicit file descriptors)。

简单的打开文件操作:file = io.open (filename [, mode])

隐式文件描述符使用标准输入输出模式或者使用单个输入文件和输出文件。使用隐匿文件描述符的示例代码如下:

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)

-- 将内容追加到文件最后一行
io.write("-- End of the test.lua file")

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

我们也会经常用到显示文件描述符,因为它允许我们同时操作多个文件。这些函数与隐式文件描述符非常相似,只不过我们在这儿使用 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")

-- 添加内容到文件的尾行
file:write("--test")

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

为什么需要错误处理机制,在真实的系统中程序往往非常复杂,它们经常涉及到文件操作、数据库事务操作或网络服务调用等,这个时候错误处理就显得非常重要。不关注错误处理可能在处理诸如涉密或金融交易这些业务时造成重大的损失。
无论什么时候,程序开发都要求小心地做好错误处理工作。在 Lua 中错误可以被分为两类:语法错误、运行时错误;

对于运行时错误,虽然程序也能成功运行,但是程序运行过程中可能因为错误的输入或者错误的使用函数而导致运行过程中产生错误。

我们经常用到 assert 和 error 两个函数处理错误。下面是一个简单的例子。

1
2
3
4
5
6
local function add(a,b)
assert(type(a) == "number", "a is not a number")
assert(type(b) == "number", "b is not a number")
return a+b
end
add(10)

使用 pcall(f,arg1,…) 函数可以使用保护模式调用一个函数。如果函数 f 中发生了错误, 它并不会抛出一个错误,而是返回错误的状态。

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

if pcall(myfunction) then
print("Success")
else
print("Failure")
end

xpcall(f,err) 函数调用函数 f 同时为其设置了错误处理方法 err,并返回调用函数的状态。任何发生在函数 f 中的错误都不会传播,而是由 xpcall 函数捕获错误并调用错误处理函数 err,传入的参数即是错误对象本身。xpcall 的使用示例如下:

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)

Lua 调试库提供了创建自己的调试器所需要的所有原语函数,Lua 没有内置调试器,但是有不少开源调试器。http://wiki.jikexueyuan.com/project/lua/debugging.html


Lua 通过特定算法的垃圾回收机制实现自动内存管理。由于自动内存管理机制的存在,作为程序开发人员:

不需要关心对象的内存分配问题。
不再使用对象时,除了将引用它的变量设为 nil,不需要主动释放对象。
Lua 的垃圾回收器会不断运行去收集不再被 Lua 程序访问的对象。

http://wiki.jikexueyuan.com/project/lua/garbage-collection.html


在游戏开发领域,Lua 语言因其结构和语法的简洁性而在各类游戏引擎中被广泛使用。游戏对图形画面要求非常苛刻,这无疑需消耗大量的内存空间,而这些内存空间的管理是非常棘手的问题。Lua 语言有自动的垃圾回收机制,这种自动化的内存管理机制也使得 Lua 受到游戏引擎开发者的青睐。


Lua 标准库利用 C 语言 API 实现并提供了丰富的函数,它们内置于 Lua 语言中。该标准库不仅可以提供 Lua 语言内服务,还能提供外部服务,比如文件或数据库的操作。

这些标准库使用标准的 C API 接口实现,它们作为独立的 C 语言模块提供给使用者。主要包括以下的内容:

基本库,包括协程子库
模块库
字符串操作
表操作
数学计算库
文件输入与输出
操作系统工具库
调试工具库

End.