Lua基础

Lua是一种灵活小巧的脚本语言,可以被嵌入应用程序中,为应用程序提供灵活的扩展和定制功能。

注释

单行注释

1
2
-- 单行注释
print("Hello World!") -- 另一个单行注释

多行注释

1
2
3
4
--[[
多行注释
另一个多行注释
--]]

变量

变量标识符

Lua的变量标识符用大小写字母、下划线、数字等组成,必须用字母或下划线作为开头。最好不要使用下划线加大写字母的标示符,以免与Lua的保留字冲突。在Lua的变量标识符中,字母需要区分大小写。一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION等)被保留用于 Lua 内部全局变量,_G表示全局作用域,后边还会提到。

变量作用域

Lua中作用域分为三种:全局作用域、局部作用域、表内作用域。声明变量时会默认使用全局作用域;声明局部变量需要显式的在变量名之前加上Local关键字。表(table)中存储的是键值对,可以根据键名来获取对应的值。

1
2
3
4
5
var1 = 1 -- 全局
local var2 = 2 -- 局部
table1= {}
var3 = "key3"
table1.var3 = 3 -- 表内

全局变量可以看做在名为_G的表中的变量。

基本数据类型

在Lua中使用变量不需要提前声明,变量的类型决定于用户赋值的类型。可以使用type()函数判断变量的类型。

Lua中有8种基本的数据类型:

  • nil
  • boolean
  • string
  • number
  • table
  • function
  • thread
  • userdata

nil

nil类型表示没有有效值,当访问一个没有初始化的全局变量时并不会出错,而会得到nil
通过将全局变量和表内的变量赋值为nil,可以达到释放该变量的目的。

1
2
-- 引用未定义变量将返回 nil ,并不会出错
foo = anUnknownVariable -- 等价于 foo = nil

boolean

boolean 类型只有两个可选值:truefalse,在Lua中falsenil被看作是“假”,其他的都为“真”。

string

与python类似,字符串可以使用两个单引号'或两个双引号"包裹起来表示,也可以用多行注释来表示跨行的字符串。

1
2
3
4
5
6
7
str = 'aspythonstring' -- 像 Python 一样不可变
str = "aspythonuse" -- 可以双引号
str = [[
像 Python 的多行注释可用于
表示多行字符串一样
方便
]]

对于[[ ... ]]形式的字符串,可以两个方括号之间加相等数量的=,如:

1
2
3
str = [=[
[[[[[[[字符串的内容就是 好多方括号]]
]=]

使用 ..来连接两个字符串:

1
print("a" .. 'b') -- ab

使用#来获取字符串长度

1
2
str = "hello"
print(#str) -- 5

字符串的一些操作其他操作

1
2
3
4
5
6
7
8
9
10
11
12
string.upper(argument) -- 将所有的小写字母转为大写
string.lower(argument) -- 将所有的大写字母转为小写
string.gsub(mainString,findString,replaceString,num)
-- 替换操作 将mainString中的findString替换为replaceString,num表示替换的次数,如不指定则表示替换全部
string.strfind (str, substr, [init, [end]])
-- 在字符串str中搜索指定的substr,返回起止的索引,不存在则返回nil
string.reverse(arg) -- 反转字符串
string.format(...) -- 返回格式构造的字符串 如 string.format("the value is:%d",4)
string.char(arg) -- 将整型数字转成字符并连接,可接收多个字符作为参数
string.byte(arg[,int]) -- 转换字符为整数值 arg是字符串时转换第int个值,如int未指定则默认第一个字符
string.len(arg) -- 字符串长度
string.rep(string, n)) -- 将string重复n次拼接起来

number

Lua中的number都是双精度的实数值。整数同样也用number表示。

table

Lua中的table是一种关联数组(associative array),其中存储的是键值对。也可以仅指定值而使用默认的数字索引作为键。Lua的表中默认初始索引从 1 开始。table是一种非常重要的数据类型,后边专用一节细讲。

function

表示由C或Lua编写的函数。Lua中的function是“第一类值”,可以存储在变量中,可以作为参数或返回值,后边详细介绍。

thread

表示执行的独立线路,用于执行协同程序(coroutine)。

userdata

表示任意存储在变量中的C数据结构。

控制流

Lua中的代码块以总是以end结尾(函数的定义也用end表示结束)。在Lua中有以下一些流程控制语句。

分支语句

使用if ... then ... else来表示分支结构。除了nilfalse之外,其它的值(包括数字0和空字符串''等)均为“真”。

逻辑的与、或、非用andornot表示,判断相等、不等使用==~=

if判断可以多分支和嵌套,相关语句使用方式参见下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
if a <= 0
then
print("a <= 0")
if b > 0
then
print("b > 0")
end
elseif a >= 100
then
print("a >= 100")
else
print("0 < a < 100")
end

某些情况下可以使用以下的方式来达到C语言中问号表达式的效果:

1
2
ans = aBoolValue and 'yes' or 'no'
-- 对于表达式 a ? b : c 需注意排除 b = false 或 nil 的情况

循环语句

Lua中的循环有以下四种方式:

while…do循环

与C语言中相似,详见下边代码,注意在Lua中没有自增和自减运算符:

1
2
3
while num < 50 do
num = num + 1
end

for循环

for循环指定起止的数值及步长,循环变量可以取到起止的值,当不指定迭代步长时步长使用默认值1。

1
2
3
4
5
6
7
8
sum = 0
for i = 1, 100 do
sum = sum + i
end
--
for j = 100, 1, -2 do
print(j)
end

泛型for循环

遍历数组,对其中的每一个元素进行循环内的操作。Lua中使用迭代器函数ipairs来遍历访问数组,详见示例代码:

1
2
days = {"Suanday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
for i,v in ipairs(days) do print(v) end

其中i是数组索引值,v是对应索引的数组元素值。

对于非数组形式的table,使用迭代器函数pairs来遍历访问,详见示例代码:

1
2
colors = {red = "FF0000", green = "00FF00", blue = "0000FF"}
for k,v in pairs(colors) do print(v) end

其中其中k是键,v是对应的值。

repeat…until 循环

repeatuntil之间的语句至少会执行一次。

1
2
3
4
5
rest = 10
repeat
print(string.format("%d",rest))
rest = rest - 1
until rest == 0

函数

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

函数定义

Lua中的函数定义方式如下:

1
2
3
4
[作用域] function 函数名( 参数1, 参数2, 参数3..., 参数n)
函数体
return 逗号分隔的返回值
end

函数的作用域有局部与全局之分。定义局部函数使用local关键字,定义表内的函数直接将函数指定给表内对应的键。

以下的示例代码定义了一个求两个数中最大值的函数:

1
2
3
4
5
6
7
8
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end

以下的示例代码使用函数递归调用来实现斐波那契数列求和:

1
2
3
4
function fib(n)
if n < 2 then return 1 end
return fib(n - 2) + fib(n - 1)
end

Lua中的函数是“第一类值”,函数可以保存为变量。

1
2
function f(x) return x * x end
f = function (x) return x * x end

高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
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 ##

可变参数

函数可以接受可变数量的参数,在函数参数列表中使用三点(),表示函数有可变的参数。Lua将函数的参数放在一个叫arg的表中,#arg可以计算传入参数的个数。
以下示例代码用来计算几个数的平均值:

1
2
3
4
5
6
7
8
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
return result/#arg
end

多返回值

Lua中的函数可以有多个返回值,返回值之间用逗号隔开,如下示例:

1
2
3
function func(a, b, c)
return a+1, b+2, c+3
end

表是Lua中唯一的复合类型,本质是关联数组,可用来构建不同的数据类型,如数组、字典等。table中的键(数组索引)可以使用除nil外任意类型的值。table的大小是不固定的,你可以根据自己需要进行扩容。

构造

表的构造有以下三种形式:

1
2
3
tab1 = {} -- 构造空表
tab2 = {"apple", "pear", "orange", "grape"} -- 只有值,使用默认的索引
tab3 = { ["key1"] = "val1", ["key2"] = "val2", [3] = "val3"} -- 指定键和值

对于上边的第三种方式,当键为字符串时,可以省略方括号和引号,所以下边的代码与上边的第三行等价:

1
tab3 = { key1 = "val1", key2 = "val2", [3] = "val3"} -- 指定键和值

Lua中的全局变量其实是存储在一个名为_G的table中,所以,以下的代码会输出true

1
print(_G['_G'] == _G) -- 输出true

Lua中table有以下的方法:

1
2
3
4
5
6
7
8
9
10
11
table.concat (table [, sep [, start [, end]]])
-- 将table中的元素拼接成字符串。列出参数table的数组部分从start位置到end位置的所有元素,元素间以指定的分隔符(sep)隔开,返回拼接完成的字符串。
table.insert (table, [pos,] value)
-- 在table的数组部分指定位置(pos)插入值为value的一个元素,如果未指定pos,则插入位置默认为数组部分末尾
table.remove (table [, pos])
-- 返回table数组部分位于pos位置的元素,其后的元素会被前移,如果未指定pos,默认为table长度,即移除最后一个元素
table.sort (table [, comp])
-- 对给定的table进行升序排序。如果指定了comp,comp必须是一个接收两个参数且返回true或false的函数

数组

table中省略键时,变成使用默认数字索引的数组,索引值是从1开始的。

1
2
3
4
v = {'value1', 'value2', 1.21, 'gigawatts'}
for i = 1, #v do
print(v[i])
end

metatable

元表(metatable)可以看做表的表,类似 JavaScript中原型(prototype)的概念。使用元表(metatable)可以改变table的行为,table的每个行为都关联了对应的元方法(metamethod)。主要涉及两个方法:

  • setmetatable(table,metatable):对指定table设置元表,如果table的元表中已存在__metatable键,setmetatable则会失败 。为一个表的元表增加__metatable键可以保护和锁定该表中的元方法;

  • getmetatable(table):返回table的元表。

元表能够被用于定义算术运算符和关系运算符的行为。以下是一段示例代码,为表定义了加法运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
f1 = {a = 1, b = 2}
f2 = {a = 2, b = 3}
-- 此时如果计算 s = f1 + f2 会出错
mt = {}
function mt.__add(x, y)
sum = {}
sum.a = x.a + y.a
sum.b = x.b + y.b
return sum
end
setmetatable(f1, mt)
setmetatable(f2, mt)
-- 实际调用 f1 的 metatable 中的 __add(f1, f2)
-- 所以只为 f1 设置元表即可
s = f1 + f2 -- s = {a = 3, b = 5}
-- 此时如果 s2 = s + s 为错,因为s 未定义元表

Lua中的值都具有元方法,表中的元方法可以被重载,以下是表中可以被重载的元方法:

元表的键 重载的方法
__add(a, b) a + b
__sub(a, b) a - b
__mul(a, b) a * b
__div(a, b) a / b
__mod(a, b) a % b
__pow(a, b) a ^ b
__unm(a) -a
__concat(a, b) a .. b
__len(a) #a
__eq(a, b) a == b
__lt(a, b) a < b
__le(a, b) a <= b
__index(a, b) (可以是函数或表) a.b
__newindex(a, b, c) a.b = c
__call(a, …) a(…)

例如重载表中根据键取值的符号"."。元表中的__index可以是一个表或一个函数,当通过键来访问表的时候:

  • 如果这个键有值,则会返回对应的值;
  • 如果这个键没有值,且该表没有元表,或该表有元表但元表中没有__index键,则返回nil
  • 如果这个键没有值,但该表有元表且元表中__index键对应了一个表,则会在对应的表中查找对应该键的值;
  • 如果这个键没有值,但该表有元表且元表中__index键对应了一个函数,则会把表和键一起传入函数,返回函数的结果
1
2
3
4
defaultFavs = {animal = 'gru', food = 'donuts'}
myFavs = {food = 'pizza'}
setmetatable(myFavs, {__index = defaultFavs})
food = myFavs.food

table实现对象与继承

Lua中的表可以作为对象,为了使由表实现的对象具有独立的生命周期,同样的方法(函数)可以对不同的对象单独作用,Lua中使用self关键字,用于区分函数作用的对象。self表示函数调用者,使用冒号":"可以在生命或调用函数时隐藏self参数,但是在函数内部仍然可以使用self。以下的两行代码等价:

1
2
function tab1.fun1(tab1,arg1) -- ...略去剩余部分
function tab1:fun1(arg1) -- ...略去剩余部分

在调用时也是同样规律,以下两行代码等价:

1
2
r = tab1.fun1(tab1, 100)
r = tab1:fun1(100)

以下是定义类与创建对象的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Shape = {area = 0}
-- 类的方法 new
function Shape:new (o,side)
o = o or {}
setmetatable(o, self) -- 将类传给实例的元表
self.__index = self -- 访问实例的字段或方法时会通过元表的__index找到类的字段或方法
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
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
-- 父类
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
Square = Shape:new()
-- 子类1方法new 重写父类的new
function Square:new (o,side)
o = o or Shape:new(o,side)
setmetatable(o, self)
self.__index = self
return o
end
-- 子类1方法printArea 重写父类的printArea
function Square:printArea ()
print("正方形面积为 ",self.area)
end
-- 创建子类1对象
mysquare = Square:new(nil,10)
mysquare:printArea()
-- 子类2
Rectangle = Shape:new()
-- 子类2方法 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
-- 子类2方法 printArea
function Rectangle:printArea ()
print("矩形面积为 ",self.area)
end
-- 创建子类2对象
myrectangle = Rectangle:new(nil,10,20)
myrectangle:printArea()

其它

多个变量同时赋值

赋值语句可以同时对逗号分隔的多个值赋值。当变量数不足时,多余的值被舍弃;当值不足时,变量被赋值为nil

1
2
3
a, b = 10, 20 -- 相当于 a=10; b=20
x, y = y, x -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i] -- swap 'a[i]' for 'a[j]'

闭包

匿名函数与闭包:

1
2
3
4
5
6
7
8
function adder(x)
return function (y) return x + y end -- 返回一个函数 闭包内封存 x 值
end
a1 = adder(9)
a2 = adder(36)
print(a1(16)) -- 输出 25
print(a2(64)) -- 输出 100

在泛型for循环中,使用自定义的闭包实现带状态的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
array = {"Lua", "Tutorial"}
function elementIterator (collection)
local index = 0
local count = #collection
-- 返回一个函数 闭包内封存 index 和 count
return function ()
index = index + 1
if index <= count
then
-- 返回迭代器当前指向的元素
return collection[index]
end
end
end
for element in elementIterator(array)
do
print(element)
end

协程

协程(coroutine)与线程比较类似:协程拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。与线程不同,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

以下是协程相关的一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
coroutine.create()
-- 创建coroutine,参数是一个函数,只接收单个参数,这个参数是coroutine的主函数。返回的是创建的coroutine。创建完之后并不会运行协程,而是在首次resume时唤醒,唤醒会调用指定的函数
coroutine.resume()
-- 唤醒或恢复coroutine,resume中第一个参数为要唤醒或恢复的协程,后边的参数会传给创建协程时指定的函数或通过yield返回到协程中,返回的第一个参数为一个布尔值
coroutine.yield()
-- 挂起coroutine,将coroutine设置为挂起状态,此时与之对应的resume会立即返回,返回值为true,如果yield中有其它参数,也会一并通过resume返回
coroutine.status()
-- 查看coroutine的状态注:coroutine的状态有四种:suspend,running,dead,normal
coroutine.wrap()
-- 创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,所有传入这个函数的参数等同于传入resume的参数
coroutine.running()
-- 返回正在运行的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程编号

参看以下的一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo (a)
print("foo", a)
return coroutine.yield(2*a)
end
co = coroutine.create(function (a,b)
print("co-body", a, b)
local r = foo(a+1)
print("co-body", r)
local r, s = coroutine.yield(a+b, a-b)
print("co-body", r, s)
return b, "end"
end)
print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))

首次调用resume时,resume中除了第一个参数之外,后边的参数会传给创建协程的函数。当协程被挂起回到主线程,再次调用resume时,resume中除了第一个参数之外后边的参数会通过yield传回到协程中。上边的代码输出结果如下:

1
2
3
4
5
6
7
8
co-body 1 10
foo 2
main true 4
co-body r
main true 11 -9
co-body x y
main true 10 end
main false cannot resume dead coroutine

模块

Lua 的模块是由变量、函数等已知元素组成的table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个table就行。

require

以下是文件mod.lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 文件名 module.lua
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end
local function func2()
print("这是一个私有函数!")
end
function module.func3()
func2()
end
return module -- 最后要返回整个表
-- module.lua 结束

在主文件中使用前边定义的模块module.lua:

1
2
3
4
5
require("module") -- 使用require引用
print(module.constant)
module.func3()

引用模块也可以写成以下的形式:

1
2
require "<module>"
local m = require("module") --引用并保存在局部变量中

require的特点是仅加载一次,并且对于模块会按照特定的搜索规则去查找文件并加载。如果被包含的模块中有可执行的函数,多次require,被调用文件只运行一次。如以下文件mod1.lua:

1
2
3
4
5
6
7
8
-- 文件名 mod1.lua
mod = {}
print('Hi!')
return mod
-- mod1.lua 结束

主文件中两次包含:

1
2
local a = require("mod1") -- 会输出 'Hi!'
local b = require("mod1") -- 不再输出,且b等于a

dofile

使用dofile不会缓存被包含的文件,所以对于之前的mod1.lua:

1
2
dofile("mod1") -- 会输出 'Hi!'
dofile("mod1") -- 会再次输出 'Hi!'

loadfile

loadfile会编译代码,将整个模块文件当成一个函数返回,但是不会执行代码。

1
2
f = loadfile('mod1.lua')
f() -- 输出 "Hi!"

loadstring

loadstringloadfile相似,是以字符串的形式读取一段代码。

1
2
f = loadstring("print('Hi!')")
f() -- 输出 "Hi!"

loadlib

loadlib用于加载C的库,一般是*.so或*.dll库。需要传入两个参数,一个是库的路径,一个是初始化函数,

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

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

REFERENCE

http://www.runoob.com/lua/lua-tutorial.html
http://tylerneylon.com/a/learn-lua/
Lua程序设计(第二版)