Priest Tomb



Lua语言学习(三)——控制结构(Control Structure)、Table、Function

这一篇同样是根据 Lua-Users wiki 梳理的一些细节


控制结构(Control Structure)

break

在嵌套循环中,break 只作用于最内层的一个循环:

> for i = 1, 2 do
>> while true do
>> break
>> end
>> print(i)
>> end
1
2

在循环外使用 break 是一个语法错误:

> break
stdin:1: <break> at line 1 not inside a loop

类 continue

Lua 不像其他的语言拥有 continue 关键字能跳出当前循环,在 Lua 5.2 中,可以使用 goto 关键字实现:

> for i = 1, 10 do
>> if i > 3 and i < 6 then goto continue end
>> print(i)
>> ::continue:: --使用双冒号包围一个指定位置
>> end
1
2
3
6
7
8
9
10

在5.2以前的版本中,想实现类 continue 的效果,只能用其他的一些变通方法:

--直接使用内部 if 判断
for i = 1, 10 do
  if (i <= 3 or i >= 6) then
    print(i)
  end
end

--使用 repeat + if
for i = 1, 10 do
  repeat
    if (i > 3 and i < 6) then
      break
    end
    print(i)
  until true
end

--使用 while + if
for i = 1, 10 do
  while true do
    if (i > 3 and i < 6) then
      break
    end
    print(i)
    break
  end
end

条件

条件不一定非要是 boolean 值,在 Lua 中,除了 nil 和 false 外,任何值都是 true:

> if 5 then print("true") else print("false") end
true
> if 0 then print("true") else print("false") end
true
> if true then print("true") else print("false") end
true
> if {} then print("true") else print("false") end
true
> if "string" then print("true") else print("false") end
true
> if nil then print("true") else print("false") end
false
> if false then print("true") else print("false") end
false

其他语言中,条件也可以是一个表达式,但在 Lua 中是不行的:

> i = 0
> while (i = i + 1) <= 10 do print(i) end
stdin:1: ')' expected near '='

Table

在 Lua 中,除了 nil 和 NAN(Not a Number),任何值都可以做 key:

> t = {}
> k = {}
> f = function () end
> t[k] = 123
> t[f] = 456
> = t[k]
123
> = t[f]
456
> t[nil] = 123
stdin:1: table index is nil
stack traceback:
        stdin:1: in main chunk
        [C]: in ?
> t[0/0] = 123
stdin:1: table index is NaN
stack traceback:
        stdin:1: in main chunk
        [C]: in ?

当 key 是 string 时,可以有这几种方法初始化:

-- 初始化时使用方括号设置 key,要有引号
> t = {["test"] = 123}

-- 初始化时不用方括号,不需要引号
> t = {test = 123}

-- 使用点设置 key,不用引号
> t.test = 123

-- 使用点和方括号设置 key,要有引号
> t.["test"] = 123

使用方括号设置 key 时,如果不带引号,则代表是一个变量,如果这个变量没有初始化,就会报错:

-- 没初始化 test,报错
> t[test] = 123
stdin:1: table index is nil
stack traceback:
        stdin:1: in main chunk
        [C]: ?

-- 初始化 test,正常
> test = "test"
> t[test] = 123

如果初始化时不指定 key 值,则 key 默认从1开始递增,在使用 iterator 遍历时可以看到,会先遍历默认 key,再遍历指定 key:

> t = {"a", "b", [123]="foo", "c", name="bar", "d", "e"}
> for k,v in pairs(t) do print(k,v) end
1       a
2       b
3       c
4       d
5       e
123     foo
name    bar

table 的长度也可以使用 # 符号获取:

> t = {"a", "b", "c"}
> = #t
3

使用 table.insert() 方法向 table 中插入值:

> t = {"a", "c"}

-- 两个参数:要操作的 table 要插入的值,默认在末尾插入
> table.insert(t, "d")

-- 三个参数:要操作的 table 在哪个位置插入 要插入的值
> table.insert(t, 2, "b")

> for k,v in pairs(t) do print(k,v) end
1	a
2	b
3	c
4	d

使用 table.remove() 方法从 table 中移除值:

> t = {"a", "b", "c", "d", test = 123}

-- 默认删除默认 key 的最后一个
> table.remove(t)  -- 删除 d 而不是123

-- 删除指定 key
> table.remove(t, 2)  -- 删除 b

> for k,v in pairs(t) do print(k,v) end
1	a
2	c
test	123

使用 table.concat() 方法可以拼接 table 中的值,但测试发现两点,key 为字符串的自定义值不被拼接、不连贯的数字 key 也不会被拼接:

-- 指定一个数字 key 为5,则可拼接
> t = {"a", "b", "c", "d", [5] = "e", ["test"] = 123}
> print(table.concat(t,"-"))
a-b-c-d-e

-- 指定一个数字 key 为8,不可拼接
> t = {"a", "b", "c", "d", [8] = "e", ["test"] = 123}
> print(table.concat(t,"-"))
a-b-c-d

-- 指定索引从第一位拼到第三位
> t = {"a", "b", "c", "d", [5] = "e", ["test"] = 123}
> print(table.concat(t, "-", 1, 3))
a-b-c

-- 指定的索引超过了 table 的 key
> t = {"a", "b", "c", "d", [5] = "e", ["test"] = 123}
> print(table.concat(t, "-", 1, 10))
stdin:1: invalid value (nil) at index 6 in table for 'concat'
stack traceback:
        [C]: in function 'concat'
        stdin:1: in main chunk
        [C]: ?

当把一个 table 传给一个 function,或存为一个新的变量时,Lua 不会创建一个新的 table 副本,而是同一个 table 的引用:

> t = {}
> u = t
> u.foo = "bar"
> = t.foo
bar
> function f(x) x[1] = 2 end
> f(t)
> = u[1]
2

Function

多返回值

Lua 中的函数可以返回多个值:

> f = function ()
>>  return "x", "y", "z" -- 返回三个值
>> end
> a, b, c, d = f() -- 将三个值返回给四个变量,第四个会被设置成 nil
> = a, b, c, d
x y z nil
> a, b = (f()) -- 使用括号包裹,会丢弃多余的返回值
> = a, b
x, nil
> = "w"..f() -- 使用函数作为一个子表达式,同样会丢弃多余的返回值
wx
> print(f(), "w") -- 作为另一个函数的参数时,也会丢弃多余的返回值
x w
> print("w", f()) -- 但当函数调用是最后一个参数时,会返回所有返回值
w x y z
> print("w", (f())) -- 这时使用括号包裹也会起作用
w x
> t = {f()} -- 多个返回值可以直接被存储在 table 中
> = t[1], t[2], t[3]
x y z

在最后一个例子中需要注意的是,如果函数返回的值中有 nil,那使用 # 操作符获取 table 中值的数量时就不一定准确了:

> f = function () return "x","y",nil,"z" end
> t = {f()}
> print(#t)
4

函数作为参数

将函数作为参数或用它们返回值是一个有用的功能,这里有个很好的例子——table.sort:


> list = {{3}, {5}, {2}, {-1}}
> table.sort(list)
attempt to compare two table values
stack traceback:
        [C]: in function 'sort'
        stdin:1: in main chunk
        [C]: in ?
> table.sort(list, function (a, b) return a[1] < b[1] end)
> for i,v in ipairs(list) do print(v[1]) end
-1
2
3
5

可变参数

函数可以在参数列表的末尾使用 … 作为可变参数:

> f = function (x, ...)
>>  x(...)
>> end
> f(print, "1 2 3")
1 2 3

想从 … 中获取特定的项可以使用 select() 函数,该函数可指定从第几位开始获取入参,而且还可以接收 “#” 作为参数,获取可变参数的长度:

> f=function(...) print(select("#", ...)) print(select(3, ...)) end
> f(1, 2, 3, 4, 5)
5
3 4 5

… 还可以被打包(packed)进一个 table:

> f=function(...) tbl={...} print(tbl[2]) end
> f("a", "b", "c")
b

一个数组项 table 还可以被拆包(unpacked)成参数列表:

> f=function(...) tbl={...} print(table.unpack(tbl)) end -- 在 Lua 5.1 中是直接使用 unpack() 方法,不需要前面的 table.
> f("a", "b", "c")
a b c
> f("a", nil, "c") -- undefined result, may or may not be what you expect

像前面说到的,如果 table 中有 nil 值存在,就会出现一些乱七八糟的问题,所以从 Lua 5.2 开始加入了 table.pack() 方法,也新增了一个 “n” 字段表示 table 中项的数量:

> f=function(...) tbl=table.pack(...) print(tbl.n, table.unpack(tbl, 1, tbl.n)) end
> f("a", "b", "c")
3 a b c
> f("a", nil, "c")
3 a nil c

递归和尾调用

一个简单的求阶乘的递归函数:

function factorial(x)
  if x == 1 then
    return 1
  end
  return x * factorial(x-1)
end

上面这个递归函数在 x 的值非常大的时候,将有很严重的性能问题,在 return 中调用自身函数时,需要等待下一个调用的返回值,用来乘以当前函数中的 x,这就使得需要保存一些信息在调用栈(call stack)中,每次调用都需要保存,所以栈空间将迅速增长

这个问题可以通过尾调用(tail calls)来优化,这里有一个使用尾调用来重写上述求阶乘的函数:

function factorial_helper(i, acc)
  if i == 0 then
    return acc
  end
  return factorial_helper(i-1, acc*i)
end

function factorial(x)
  return factorial_helper(x, 1)
end

在 factorial_helper 函数中可以看到,return 中仅仅是调用自身函数,不需要等待该次调用的返回,所以不会向调用栈中保存额外的信息

简单的一句话来讲就是:尾调用仅仅是一次跳跃,而不是一次实际的函数调用(a tail call is just a jump, not an actual function call)

这里还有一些例子来说明什么是尾调用、什么不是尾调用(就不翻译了):

return f(arg) -- tail call
return t.f(a+b, t.x) -- tail call
return 1, f() -- not a tail call, the function's results are not the only thing returned
return f(), 1 -- not a tail call, the function's results are not the only thing returned
return (f()) -- not a tail call, the function's possible multiple return values need to be cut down to 1 after it returns
return f() + 5 -- not a tail call, the function's return value needs to be added to 5 after it returns
return f().x -- not a tail call, the function's return value needs to be used in a table index expression after it returns

最后,Lua 官方也把尾调用称之为“正确的尾调用”(Proper Tail Calls)