lua 基础篇
简介
过程动态语言
变量名没有类型,值才有类型,变量名在运行时可与任何类型的值绑定;
语言只提供唯一一种数据结构,称为表(table),它混合了数组、哈希,可以用任何类型的值作为 key 和 value。提供了一致且富有表达力的表构造语法,使得 Lua 很适合描述复杂的数据;
函数是一等类型,支持匿名函数和正则尾递归(proper tail recursion);
支持词法定界(lexical scoping)和闭包(closure);
提供 thread 类型和结构化的协程(coroutine)机制,在此基础上可方便实现协作式多任务;
运行期能编译字符串形式的程序文本并载入虚拟机执行;
通过元表(metatable)和元方法(metamethod)提供动态元机制(dynamic meta-mechanism),从而允许程序运行时根据需要改变或扩充语法设施的内定语义;
能方便地利用表和动态元机制实现基于原型(prototype-based)的面向对象模型;
从 5.1 版开始提供了完善的模块机制,从而更好地支持开发大型的应用程序;
lua 中的数据类型
例子 | 类型 |
---|---|
“hello world!” | string |
function | |
true | boolean |
3 / 3.04 | number |
nil | nil |
nil (空)
- nil 是一种类型,Lua 将 nil 用于表示“无效值”。一个变量在第一次赋值前的默认值是 nil,将 nil 赋予给一个全局变量就等同于删除它。
boolean(布尔)
- 布尔类型,可选值 true/false;Lua 中 nil 和 false 为“假”,其它所有值均为“真”。比如 0 和空字符串就是“真”;
number(数字)
- math.floor(向下取整)
- math.ceil(向上取整)
string(字符串)
字符串的表达方式
- 1、使用一对匹配的单引号。例:’hello’。
- 2、使用一对匹配的双引号。例:”abclua”。
- 3、字符串还可以用一种长括号(即[[ ]])括起来的方式定义。我们把两个正的方括号(即[[)间插入 n 个等号定义为第 n 级正长括号。就是说,0 级正的长括号写作 [[ ,一级正的长括号写作 [=[,如此等等。反的长括号也作类似定义;举个例子,4 级反的长括号写作 ]====]。一个长字符串可以由任何一级的正的长括号开始,而由第一个碰到的同级反的长括号结束。整个词法分析过程将不受分行限制,不处理任何转义符,并且忽略掉任何不同级别的长括号。这种方式描述的字符串可以包含任何东西,当然本级别的反长括号除外。例:[[abc\nbc]],里面的 “\n” 不会被转义。
Lua 的字符串是不可改变的值,不能像在 c 语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符。
在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。 这意味着
- 创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销),
- 内容相同的 Lua 字符串不会占用多份存储空间,
- 已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n).
table
table 如果作为函数的入参的话,是地址传递
Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,索引通常是字符串(string)或者 number 类型,但也可以是除 nil 以外的任意类型的值。
在内部实现上,table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。
function
在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值。
有名函数的定义本质上是匿名函数对变量的赋值。为说明这一点,考虑
1
2
3
4
5
6function foo()
end
-- 等价于
foo = function ()
end
1 | local function foo() |
表达式
关系运算符
注意:Lua 语言中“不等于”运算符的写法为:~=
在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等。可以看下面的例子:
1
2
3
4
5
6
7
8
9
10local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
print("a==b")
else
print("a~=b")
end
---output:
a~=b
逻辑运算符
逻辑运算符 | 说明 |
---|---|
and | 逻辑与 |
or | 逻辑或 |
not | 逻辑非 |
- Lua 中的 and 和 or 是不同于 c 语言的。在 c 语言中,and 和 or 只得到两个值 1 和 0,其中 1 表示真,0 表示假。而 Lua 中 and 的执行过程是这样的:
- a and b 如果 a 为 nil,则返回 a,否则返回 b;
- a or b 如果 a 为 nil,则返回 b,否则返回 a。
1 | local c = nil |
- 注意:所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。
字符串连接
在 Lua 中连接两个字符串,可以使用操作符“..”(两个点)。如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。
注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数 string.format 连接字符串。
由于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 .. 来拼接最终结果),则性能损耗会非常大。在这种情况下,推荐使用 table 和 table.concat() 来进行很多字符串的拼接,例如:
1
2
3
4
5local pieces = {}
for i, elem in ipairs(my_list) do
pieces[i] = my_process(elem)
end
local res = table.concat(pieces)
控制结构
if-else
1 | score = 90 |
while
- Lua 跟其他常见语言一样,提供了 while 控制结构,语法上也没有什么特别的。但是没有提供 do-while 型的控制结构,但是提供了功能相当的 repeat。
1 | while 表达式 do |
值得一提的是,Lua 并没有像许多其他语言那样提供类似 continue 这样的控制语句用来立即进入下一个循环迭代(如果有的话)。因此,我们需要仔细地安排循环体里的分支,以避免这样的需求。
没有提供 continue,却也提供了另外一个标准控制语句 break,可以跳出当前循环。
repeat 控制结构 - (do while)
Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。简单点说,执行 repeat 循环体后,直到 until 的条件为真时才结束,而其他语言(如:C++ 语言)的 do-while 则是当条件为假时就结束循环。
以下代码将会形成死循环:
1
2
3
4x = 10
repeat
print(x)
until falseLua 中的 repeat 也可以在使用 break 退出。
for
- for 语句有两种形式:数字 for(numeric for)和范型 for(generic for)。
数字型 for
1 | for var = begin, finish, step do |
关于数字 for 需要关注以下几点:
- 1.var 从 begin 变化到 finish,每次变化都以 step 作为步长递增 var - 2.begin、finish、step 三个表达式只会在循环开始时执行一次 3
- .第三个表达式 step 是可选的,默认为 1
- 4.控制变量 var 的作用域仅在 for 循环内,需要在外面控制,则需将值赋给一个新的变量
- 5.循环过程中不要改变控制变量的值,那样会带来不可预知的影响
如果不想给循环设置上限的话,可以使用常量 math.huge:
1
2
3
4
5
6for i = 1, math.huge do
if (0.3*i^3 - 20*i^2 - 500 >=0) then
print(i)
break
end
end
for 泛型
- 泛型 for 循环通过一个迭代器(iterator)函数来遍历所有值:
1 | -- 打印数组a的所有值 |
Lua 的基础库提供了 ipairs,这是一个用于遍历数组的迭代器函数。在每次循环中,i 会被赋予一个索引值,同时 v 被赋予一个对应于该索引的数组元素值。
下面是另一个类似的示例,演示了如何遍历一个 table 中所有的 key
1
2
3
4-- 打印table t中所有的key
for k in pairs(t) do
print(k)
end从外观上看泛型 for 比较简单,但其实它是非常强大的。通过不同的迭代器,几乎可以遍历所有的东西, 而且写出的代码极具可读性。
标准库提供了几种迭代器,包括:
- 用于迭代文件中每行的(io.lines)
- 迭代 table 元素的(pairs)
- 迭代数组元素的(ipairs)
- 迭代字符串中单词的(string.gmatch)
- …
break, return 关键字
break
- 语句 break 用来终止 while、repeat 和 for 三种循环的执行,并跳出当前循环体, 继续执行当前循环之后的语句。
return
return 主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。return 只能写在语句块的最后,一旦执行了 return 语句,该语句之后的所有语句都不会再执行。
return 主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。 关于函数返回值的细节可以参考 函数的返回值 章节。return 只能写在语句块的最后,一旦执行了 return 语句,该语句之后的所有语句都不会再执行。
1
2
3
4
5local function foo()
print("before")
do return end
print("after") -- 这一行语句永远不会执行到
end
lua 函数
全局函数 & 局部函数
1 | function function_name (arc) -- arc 表示参数列表,函数的参数列表可以为空 |
- 由于全局变量一般会污染 全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符:
1 | local function function_name (arc) |
由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。
由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。
1
2
3function foo.bar(a, b, c)
-- body ...
end此时我们是把一个函数类型的值赋给了 foo 表的 bar 字段。换言之,上面的定义等价于
1 | foo.bar = function (a, b, c) |
- 对于此种形式的函数定义,不能再使用 local 修饰符了,因为不存在定义新的局部变量了。
函数的参数
按值传递
Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17local function swap(a, b) --定义函数swap,函数内部进行交换两个变量的值
local temp = a
a = b
b = temp
print(a, b)
end
local x = "hello"
local y = 20
print(x, y)
swap(x, y) --调用swap函数
print(x, y) --调用swap函数后,x和y的值并没有交换
-->output
hello 20
20 hello
hello 20在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。
变长参数
上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为 … , 表示该函数可以接收不同长度的参数。访问参数的时候也要使用 …。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15local function func( ... ) -- 形参为 ... ,表示函数采用变长参数
local temp = {...} -- 访问的时候也要使用 ...
local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
-- 组内容使用 " " 拼接成字符串。
print(ans)
end
func(1, 2) -- 传递了两个参数
func(1, 2, 3, 4) -- 传递了四个参数
-->output
1 2
1 2 3 4值得一提的是,LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。
具名参数
- Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
1 | function change(arg) --change函数,改变长方形的长和宽,使其各增长一倍 |
- 在常用基本类型中,除了 table 是按址传递类型外,其它的都是按值传递参数。 用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
函数返回值
Lua 具有一项与众不同的特性,允许函数返回多个值。Lua 的库函数中,有一些就是返回多个值。
示例代码:使用库函数 string.find,在源字符串中查找目标字符串,若查找成功,则返回目标字符串在源字符串中的起始位置和结束位置的下标。
1 | local s, e = string.find("hello world", "llo") |
返回多个值时,值之间用“,”隔开。
示例代码:定义一个函数,实现两个变量交换值
1
2
3
4
5
6
7
8local function swap(a, b) -- 定义函数 swap,实现两个变量交换值
return b, a -- 按相反顺序返回变量的值
end
local x = 1
local y = 20
x, y = swap(x, y) -- 调用 swap 函数
print(x, y) --> output 20 1当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。
调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。
1
2
3
4
5
6
7
8
9
10
11
12
13function init() --init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
x = init()
print(x)
x, y, z = init()
print(x, y, z)
--output
1
1 lua nil当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。
1
2
3
4
5
6
7
8
9local function init() -- init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -->output 1 2 nil
local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -->output 2 1 lua函数调用的实参列表也是一个列表表达式。考虑下面的例子:
1
2
3
4
5
6local function init()
return 1, "lua"
end
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua如果你确保只取函数返回值的第一个值,可以使用括号运算符,例如
1
2
3
4
5
6local function init()
return 1, "lua"
end
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
全动态函数调用
- 调用回调函数,并把一个数组参数作为回调函数的参数。
1
2local args = {...} or {}
method_name(unpack(args, 1, table.maxn(args)))
使用场景
如果你的实参 table 中确定没有 nil 空洞,则可以简化为
1
method_name(unpack(args))
你要调用的函数参数是未知的;
函数的实际参数的类型和数目也都是未知的。
1 | add_task(end_time, callback, params) |
- 值得一提的是,unpack 内建函数还不能为 LuaJIT 所 JIT 编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。
小试牛刀
1 | local function run(x, y) |