自从接触了 JerryScript(见开源库jerryscript使用备忘),本以为再也不会用到 Lua 了,关于 Lua 的记忆已经逐渐在我的脑海中删除,但最近维护的几个老项目又都使用了 Lua,真是命运的捉弄啊。

来吧,刷新记忆!

一、基础

  • Lua区分大小写
  • 注释格式
    1
    2
    3
    4
    -- 行注释
    --[[
    块注释
    --]]
  • 局部变量使用local声明, 其他的全部为全局变量,变量的默认值为nil
  • 只有false和nil为假,其他的全部为真
  • Lua中字符串都是不可变的常量
  • 使用..可以连接字符串
  • Lua没有整形,都是实数
  • 可以在字符串前放置操作符#来获取字符串长度,某些情况下对table也适用
  • 关系操作符: < > == >= <=,不等于使用~=,而不是!=
  • and, or, not 逻辑操作符返回的不一定是true和false,这点与C++不同,它返回的是对应表达式的运行结果
  • 支持多重赋值
    1
    2
    -- a为0, b为1, c为nil
    a,b,c = 0,1
  • 控制结构
    1
    2
    3
    4
    5
    6
    7
    if exp then
    -- something
    elseif exp then
    -- something
    else
    -- something
    end
1
2
3
while <exp is true> do
-- something
end
1
2
3
repeat
-- something
until <exp is true>
1
2
3
for var=exp1, exp2, exp3 do
-- something
end
1
2
3
for i,v in ipairs(a) do
-- something
end
1
2
3
for i in pairs(a) do
-- something
end

二、函数

函数也是值

2.1 多重返回值

Lua中函数可以返回多个值,但有下面的特殊情况:

  • 函数作为单独的语句时,会丢弃所有返回值
  • 函数作为表达式的一部分时,只保留第一个返回值
  • 在多重赋值中,函数作为最后一个表达式时,会保留尽可能多的返回值
  • 将函数调用放在一对括号中,会迫使其只返回一个值

2.2 变长参数

1
2
3
4
5
6
7
8
9
function add(...)
local ret = 0
for i,v in ipairs({...}) do -- 或 for i,v in ipairs{...} do
ret = ret + v
end
return ret
end

print(add(1,2,9))
1
2
3
function foo(...)
local a,b,c = ...
end
1
2
3
4
5
-- 跟踪函数foo的调用
function FooCallLog(...)
print("Call foo:", ...)
return foo(...)
end
1
2
3
4
5
6
7
8
9
10
11
12
-- 使用函数select访问变长参数
function add(...)
local ret = 0
for i = 1, select("#", ...) do -- select("#", ...)获取参数个数
local arg = select(i, ...) -- select(n, ...) 获取第n个参数
ret = ret + arg
end

return ret
end

print(add(1,3,5))

2.3 闭合(Closure)函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function newCounter() -- 该函数返回一个匿名函数
local i = 0
return function () -- 在匿名函数内部,变量i是一个“非局部的变量”
i = i + 1
return i
end
end

c1 = newCounter()
print(c1()) -- 输出:1
print(c1()) -- 输出:2

c2 = newCounter()
print(c2()) -- 输出:1

2.4 非全局函数

将一个函数存储到一个局部变量中,即得到了一个“局部函数”。

1
2
3
4
5
6
7
8
local lib={
foo = function(x,y)
return x + y
end,
goo = function(x,y)
return x - y
end
}
1
2
3
local lib={}
function lib.foo(x,y) return x + y end
function lib.goo(x,y) return x - y end

对与递归程序等,为了防止局部函数为尚定义完毕,就要被编译,可以使用前置定义的方式:

1
2
3
4
5
6
7
8
local fact
fact = function(n)
if n == 0 then
return 1
else
return fact(n-1)
end
end

三、编译、执行与错误

3.1 loadstring

1
2
3
a = 20
f = loadstring("local a = 10; print(a)")
f() -- 输出:10

3.2 pcall

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(n)
if n > 0 then
print(n)
else
error("n need > 0")
end
end

if pcall(foo, -1) then
print("ok")
else
print("error")
end

pcall也可以执行匿名函数。

四、协同程序

Lua将所有和协同程序相关的函数存储在名为“coroutine”的table中。一个协同程序可以有4种状态:suspended, running, dead, normal。

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
co = coroutine.create(function()
coroutine.yield()
for i = 0, 2 do
if i == 2 then
error("oh No.")
end
print(i)
end
end
)


print(coroutine.resume(co)) -- 当创建协同程序时,其处于suspended状态

print(coroutine.resume(co)) -- resume是在保护模式中运行的

print(coroutine.resume(co))

--[[ 输出:
true
0
1
false D:\MyLua\Basic\coroutine.lua:5: oh No.
false cannot resume dead coroutine
--]]

五、元表与元方法

Lua中每个值都有一个元表,table和userdata可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。 Lua在创建新的table时不会创建元表。除了标准的字符串程序库外,其他的类型在默认情况下都没有元表。

1
2
3
4
5
t={}
print(getmetatable(t)) -->nil

print(getmetatable("hi")) --> table: 0x80772e0
print(getmetatable(10)) --> nil

可以在元表中定义的原方法(或字段)如下:

  • __add 加
  • __sub 减
  • __mul 乘
  • __div 除
  • __unm 相反数
  • __mod 取模
  • __pow 乘幂
  • __eq 等于
  • __lt 小于
  • __le 大于
  • __concat 连接
  • __tostring 字符串转化
  • __metatable 保护元表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    local mt = {}
    mt.__tostring = function(t)
    local l = {}
    for e in pairs(t) do
    l[#l + 1] = e
    end
    return "{" .. table.concat(l, ", ") .. "}"
    end

    ages = {Jeff = 18, Jim = 19, Lucy = 20}

    print(ages) --> table: 0x003286e8

    setmetatable(ages, mt)
    print(ages) --> {Lucy, Jeff, Jim}

    mt.__metatable = "not your business"
    print(getmetatable(ages)) --> not your business
  • __index Table访问
  • __newindex Table更新
    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
    -- 跟踪Table访问的示例
    local index = {}
    local mt={
    __index = function(t, k)
    print(string.format("*access to element %s, value %s", k, t[index][k]))
    return t[index][k]
    end,

    __newindex = function(t, k, v)
    print("*update of element " .. tostring(k) .. "to " .. tostring(v))
    t[index][k] = v
    end
    }

    function track(t)
    local proxy = {} -- __index和__newindex都是在table中没有所需访问的index时才发挥作用
    proxy[index] = t -- 类似多维数组
    setmetatable(proxy, mt)

    return proxy
    end


    ages = {Jeff = 18, Jim = 19, Lucy = 20}
    ages = track(ages)
    print(ages.Jeff)

    address = {Jeff = "WuHan", Jim = "Beijin", Lucy = "Taiyuan"}
    address = track(address);
    print(address.Jim)

六、模块与包

6.1 模块搜索

对于require(“foo”)语句,Lua会先在预定路径中搜索foo.lua,如果没有搜索到才会搜索foo.DLL。

require在搜索Lua文件时,会在变量package.path存放的路径中进行搜索,Lua启动后便以环境变量LUA_PATH的值来初始化该变量; 而搜索DLL文件的路径则存放在package.cpath中,同理,Lua启动后会用LUA_CPATH环境变量的值来初始化该变量。

1
2
3
4
5
print(package.path)

--[[
;.\?.lua;C:\Program Files (x86)\ZeroBrane\bin\lua\?.lua;
--]]

require会用模块名来替换每个“?”,依次匹配搜索,直到搜索成功。

6.2 基本模块编写

编译一个模块最简单的方法就是:创建一个table,并将所有需要导出的函数放入其中,最后返回这个table。

1
2
3
4
5
6
7
8
9
10
-- file: basic_mod.lua
basicmod = {}

basicmod.name = "basic mod"

function basicmod.func1()
print("func1")
end

return basicmod

调用该模块:

1
2
3
require("basic_mod")
print(basicmod.name) --> basic mod
print(basicmod.func1()) --> func1

但是上面的写法会导致模块中的每个函数或成员前面都带有该模块的名称,更改模块名称会牵一发而动全身,针对这个问题,可以进行如下改进:

1
2
3
4
5
6
7
8
9
10
11
-- file: basic_mod.lua
local M = {} -- 借用一个局部table变量
basicmod = M

M.name = "basic mod"

function M.func1()
print("func1")
end

return basicmod

实际上,可以完全避免写模块名,因为require会将模块名作为参数传递给模块:

1
2
3
4
5
6
7
8
9
10
11
-- file: basic_mod.lua
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M

M.name = "basic mod"

function M.func1()
print("func1")
end
1
2
3
require("basic_mod") -- 此时模块函数存储的table名称就是模块的文件名
print(basic_mod.name)
print(basic_mod.func1())

在Lua5.1中,提供了一个新的函数module,简化模块的编写:

1
2
3
4
5
6
7
module(..., package.seeall)

name = "basic mod"

function func1() -- 函数前不需要加入模块名限定
print("func1")
end

七、面向对象

使用this(或self)参数是所有面向对象语言的核心。大多数面向对象语言都能对程序员隐藏this参数,从而使得程序员不必显示的声明这个参数。

Lua只需要使用冒号就能隐藏该参数,冒号的作用就是在一个方法的定义中添加一个额外的隐藏参数,以及在一个方法的调用中添加一个额外的实参,冒号只是一种语法便利,并没有引入任何新的东西。

1
2
3
4
5
6
7
8
9
10
Account = {balance = 0}
function Account.cost(v)
Account.balance = Account.balance - v -- 只能针对全局名称Account来工作
end

Account.cost(100) -- 调用正常


a = Account; Account = nil
a.cost(100) -- 错误:attempt to index global 'Account' (a nil value)

改进如下:

1
2
3
4
5
6
7
8
9
Account = {balance = 0}
function Account.cost(self, v)
self.balance = self.balance - v
end

Account.cost(Account, 100)

a = Account; Account = nil
a.cost(a, 100)

使用冒号改进:

1
2
3
4
5
6
7
8
9
Account = {balance = 0}
function Account:cost(v)
self.balance = self.balance - v
end

Account:cost(100)

a = Account; Account = nil
a:cost(100)