Lua - 基础

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
print 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
    6
    function foo()
    end

    -- 等价于
    foo = function ()
    end
1
2
3
4
5
6
local function foo()
end

-- 等价于
local foo = function ()
end

表达式

关系运算符

  • 注意:Lua 语言中“不等于”运算符的写法为:~=

  • 在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等。可以看下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    local a = { x = 1, y = 0}
    local b = { x = 1, y = 0}
    if a == b then
    print("a==b")
    else
    print("a~=b")
    end

    ---output:
    a~=b

逻辑运算符

逻辑运算符 说明
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
2
3
4
5
6
7
8
9
10
local c = nil
local d = 0
local e = 100
print(c and d) -->打印 nil
print(c and e) -->打印 nil
print(d and e) -->打印 100
print(c or d) -->打印 0
print(c or e) -->打印 100
print(not c) -->打印 true
print(not d) -->打印 false
  • 注意:所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。

字符串连接

  • 在 Lua 中连接两个字符串,可以使用操作符“..”(两个点)。如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。

  • 注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数 string.format 连接字符串。

  • 由于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 .. 来拼接最终结果),则性能损耗会非常大。在这种情况下,推荐使用 table 和 table.concat() 来进行很多字符串的拼接,例如:

    1
    2
    3
    4
    5
    local pieces = {}
    for i, elem in ipairs(my_list) do
    pieces[i] = my_process(elem)
    end
    local res = table.concat(pieces)

控制结构

if-else

1
2
3
4
5
6
7
8
9
score = 90
if score == 100 then
print("Very good!Your score is 100")
elseif score >= 60 then
print("Congratulations, you have passed it,your score greater or equal to 60")
--此处可以添加多个elseif
else
print("Sorry, you do not pass the exam! ")
end

while

  • Lua 跟其他常见语言一样,提供了 while 控制结构,语法上也没有什么特别的。但是没有提供 do-while 型的控制结构,但是提供了功能相当的 repeat。
1
2
3
while 表达式 do
--body
end
  • 值得一提的是,Lua 并没有像许多其他语言那样提供类似 continue 这样的控制语句用来立即进入下一个循环迭代(如果有的话)。因此,我们需要仔细地安排循环体里的分支,以避免这样的需求。

  • 没有提供 continue,却也提供了另外一个标准控制语句 break,可以跳出当前循环。

repeat 控制结构 - (do while)

  • Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。简单点说,执行 repeat 循环体后,直到 until 的条件为真时才结束,而其他语言(如:C++ 语言)的 do-while 则是当条件为假时就结束循环。

  • 以下代码将会形成死循环:

    1
    2
    3
    4
    x = 10
    repeat
    print(x)
    until false
  • Lua 中的 repeat 也可以在使用 break 退出。

for

  • for 语句有两种形式:数字 for(numeric for)和范型 for(generic for)。

数字型 for

1
2
3
for var = begin, finish, step do
--body
end
  • 关于数字 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
    6
    for 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
2
3
4
5
6
7
8
9
10
11
-- 打印数组a的所有值
local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
print("index:", i, " value:", v)
end

-- output:
index: 1 value: a
index: 2 value: b
index: 3 value: c
index: 4 value: d
  • Lua 的基础库提供了 ipairs,这是一个用于遍历数组的迭代器函数。在每次循环中,i 会被赋予一个索引值,同时 v 被赋予一个对应于该索引的数组元素值。

  • 下面是另一个类似的示例,演示了如何遍历一个 table 中所有的 key

    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
    5
    local function foo()
    print("before")
    do return end
    print("after") -- 这一行语句永远不会执行到
    end

lua 函数

全局函数 & 局部函数

1
2
3
4
5
6
7
8
function function_name (arc)  -- arc 表示参数列表,函数的参数列表可以为空
-- body
end

-- 另外一种写法
function_name = function (arc)
-- body
end
  • 由于全局变量一般会污染 全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符:
1
2
3
local function function_name (arc)
-- body
end
  • 由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。

  • 由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。

    1
    2
    3
    function foo.bar(a, b, c)
    -- body ...
    end
  • 此时我们是把一个函数类型的值赋给了 foo 表的 bar 字段。换言之,上面的定义等价于

1
2
3
foo.bar = function (a, b, c)
print(a, b, c)
end
  • 对于此种形式的函数定义,不能再使用 local 修饰符了,因为不存在定义新的局部变量了。

函数的参数

按值传递

  • Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    local function swap(a, b) --定义函数swap,函数内部进行交换两个变量的值
    local temp = a
    a = b
    b = temp
    print(a, b)
    end

    local x = "hello"
    local y = 20
    print(x, y)
    swap(x, y) --调用swap函数
    print(x, y) --调用swap函数后,x和y的值并没有交换

    -->output
    hello 20
    20 hello
    hello 20
  • 在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。

变长参数

  • 上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为 … , 表示该函数可以接收不同长度的参数。访问参数的时候也要使用 …。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    local function func( ... )                -- 形参为 ... ,表示函数采用变长参数

    local temp = {...} -- 访问的时候也要使用 ...
    local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
    -- 组内容使用 " " 拼接成字符串。
    print(ans)
    end

    func(1, 2) -- 传递了两个参数
    func(1, 2, 3, 4) -- 传递了四个参数

    -->output
    1 2

    1 2 3 4
  • 值得一提的是,LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。

具名参数

  • Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function change(arg) --change函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2 --表arg不是表rectangle的拷贝,他们是同一个表
arg.height = arg.height * 2
end -- 没有return语句了

local rectangle = { width = 20, height = 15 }
print("before change:", "width = ", rectangle.width,
" height = ", rectangle.height)
change(rectangle)
print("after change:", "width = ", rectangle.width,
" height =", rectangle.height)

--> output
before change: width = 20 height = 15
after change: width = 40 height = 30
  • 在常用基本类型中,除了 table 是按址传递类型外,其它的都是按值传递参数。 用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。

函数返回值

  • Lua 具有一项与众不同的特性,允许函数返回多个值。Lua 的库函数中,有一些就是返回多个值。

  • 示例代码:使用库函数 string.find,在源字符串中查找目标字符串,若查找成功,则返回目标字符串在源字符串中的起始位置和结束位置的下标。

1
2
local s, e = string.find("hello world", "llo")
print(s, e) -->output 3 5
  • 返回多个值时,值之间用“,”隔开。

  • 示例代码:定义一个函数,实现两个变量交换值

    1
    2
    3
    4
    5
    6
    7
    8
    local function swap(a, b)   -- 定义函数 swap,实现两个变量交换值
    return b, a -- 按相反顺序返回变量的值
    end

    local x = 1
    local y = 20
    x, y = swap(x, y) -- 调用 swap 函数
    print(x, y) --> output 20 1
  • 当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。

  • 调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function init()             --init 函数 返回两个值 1 和 "lua"
    return 1, "lua"
    end

    x = init()
    print(x)

    x, y, z = init()
    print(x, y, z)

    --output
    1
    1 lua nil
  • 当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    local function init()       -- init 函数 返回两个值 1 和 "lua"
    return 1, "lua"
    end

    local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
    print(x, y, z) -->output 1 2 nil

    local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
    print(a, b, c) -->output 2 1 lua
  • 函数调用的实参列表也是一个列表表达式。考虑下面的例子:

    1
    2
    3
    4
    5
    6
    local function init()
    return 1, "lua"
    end

    print(init(), 2) -->output 1 2
    print(2, init()) -->output 2 1 lua
  • 如果你确保只取函数返回值的第一个值,可以使用括号运算符,例如

    1
    2
    3
    4
    5
    6
    local function init()
    return 1, "lua"
    end

    print((init()), 2) -->output 1 2
    print(2, (init())) -->output 2 1
  • 值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。

全动态函数调用

  • 调用回调函数,并把一个数组参数作为回调函数的参数。
    1
    2
    local args = {...} or {}
    method_name(unpack(args, 1, table.maxn(args)))

使用场景

  • 如果你的实参 table 中确定没有 nil 空洞,则可以简化为

    1
    method_name(unpack(args))
  • 你要调用的函数参数是未知的;

  • 函数的实际参数的类型和数目也都是未知的。

1
2
3
4
5
add_task(end_time, callback, params)

if os.time() >= endTime then
callback(unpack(params, 1, table.maxn(params)))
end
  • 值得一提的是,unpack 内建函数还不能为 LuaJIT 所 JIT 编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。

小试牛刀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local function run(x, y)
print('run', x, y)
end

local function attack(targetId)
print('targetId', targetId)
end

local function do_action(method, ...)
local args = {...} or {}
method(unpack(args, 1, table.maxn(args)))
end

do_action(run, 1, 2) -- output: run 1 2
do_action(attack, 1111) -- output: targetId 1111