Lua 5.1 学习笔记

由 clean C 实现。需要被宿主程序调用,可以注入 C 函数。

采用基于 BNF 的语法规则。

Lua 对大小写敏感。

2.1.1 保留关键字

C 语言中没有的关键字有:

andelseiffunction

innillocalnotor

repeatthenuntil

规范:全局变量以下划线开头。

2.1.2 操作符

C 语言中没有的操作符:

^ 
~= 
//  -- 向下取整

Lua 中没有的操作符:

+=
-=

2.1.3 字符串定义

采用转义符:通过转义符表示那些有歧义的字符

字符表示

a           -- 代表字符 a
\97         -- 代表字符 a
\049        -- 代表数字字符 1 

其他转义符表示

\\n         -- 代表字符串 \n
\n          -- 代表换行

注意数字字符必须是三位。其他字符则不能超过三位。

采用长括号:长括号内的所有内容都作为普通字符处理。
[[]]        -- 0级长括号
[==[]==]    -- 2级长括号

Lua 是动态语言,变量没有类型,值才有。值自身携带类型信息。

Lua 有八种基本数据类型:nil, boolean, number, string, function, userdata, thread, table

nilfalse 导致条件为假,其他均为真。

userdata 类型变量用于保存 C 数据。 Lua 只能对该类数据进行使用,而不能进行创建或修改,保证宿主程序完全掌握数据。

thread 用于实现协程(coroutine)。

table 用于实现关联数组。table 允许任何类型的数据做索引,也允许任何类型做 table 域中的值(前述

任何类型 不包含 nil)。table 是 Lua 中唯一的数据结构。

由于函数也是一种值,所以 table 中可以存放函数。

function, userdata, thread, table这些类型的值都是对象。这些类型的变量都只是保存变量的引用,并且在进行赋值,参数传递,函数返回等操作时不会进行任何性质的拷贝。

库函数 type() 返回变量的类型描述信息。

2.2.1 强制转换

Lua 提供数字字符串间的自动转换。

可以使用 format 函数控制数字向字符串的转换。

变量有三种类型:全局变量、局部变量、表中的域

函数外的变量默认为全局变量,除非用 local 显示声明。函数内变量与函数的参数默认为局部变量。

局部变量的作用域为从声明位置开始到所在语句块结束(或者是直到下一个同名局部变量的声明)。

变量的默认值均为 nil。


a = 5               -- 全局变量
local b = 5     -- 局部变量

function joke()
    c = 5           -- 局部变量
    local d = 6 -- 局部变量
end

print(c,d)      --> nil nil

do 
    local a = 6 -- 局部变量
    b = 6           -- 全局变量
    print(a,b); --> 6 6
end

print(a,b)      --> 5 6

方便标记,--> 代表前面表达式的结果。

2.3.1 索引

对 table 的索引使用方括号 []。Lua使用语法糖提供 . 操作。

t[i]
t.i                 -- 当索引为字符串类型时的一种简化写法
gettable_event(t,i) -- 采用索引访问本质上是一个类似这样的函数调用

2.3.2 环境表

所有全局变量放在一个环境表里,该表的变量名为 _env 。对某个全局变量 a 的访问即 _env.a_env_ 只是为了方便说明)。

每个函数作为变量持有一个环境表的引用,里面包含该函数可调用的所有变量。

子函数会从父函数继承环境表。

可以通过函数 getfenv / setfenv 来读写环境表。

支持赋值,控制结构,函数调用,还有变量声明。

不允许空的语句段,因此 ;; 是非法的。

2.4.1 语句组 | chuncks

chunck ::= {stat[';']}

([';'] 应该是表示语句组后面 ; 是可选项。)

2.4.2 语句块 | blocks

block ::= chunck
stat ::= do block end

可以将一个语句块显式地写成语句组,可以用于控制局部变量的作用范围。

2.4.3 赋值 | assignment

Lua 支持多重赋值。

多重赋值时,按序将右边的表达式的值赋值给左值。右值不足补 nil,右值多余舍弃。

b = 1
a,b = 4 -- a = 4,b = nil 

+++

Lua 在进行赋值操作时,会一次性把右边的表达式都计算出来后进行赋值。

i = 5
i,a[i] = i+1, 7 -- i = 6 ,a[5] = 7

特别地,有

x,y = y,x -- 交换 x,y 的值

+++

对全局变量以及表的域的赋值操作含义可以在元表中更改。

2.4.4 控制结构

条件语句
if [exp]
    [block]
elseif [exp]
    [block]
else
    [block]
end
循环语句
while [exp]
    [block]
end

+++

repeat
    [block]
until [exp]

注意,由于 repeat 语句到 until 还未结束,因此在 until 之后的表达式中可以使用 block 中定义的局部变量。

例如:

a = 1
c = 5
repeat
    b = a + c
    c = c * 2
until b > 20
print(c)            -->     40

+++

breakreturn

breakreturn 只能写在语句块的最后一句,如果实在需要写在语句块中间,那么就在两个关键词外面包围 do end 语句块。

do break end

2.4.5 For 循环

for 循环的用法比较多,单独拎出来讲。

for 中的表达式会在循环开始前一次性求值,在循环过程中不再更新。

数字形式
for [Name] = [exp],[exp],[exp] do [block] end

三个 exp 分别代表初值,结束值,步进。exp 的值均需要是一个数字。

第三个 exp 默认为 1,可以省略。

a = 0

for i = 1,6,2 do
    a = a + i
end

等价于

int a = 0;
for (int i = 1; i <= 6;i += 2){ // 取到等号,如果步进是负的,那么会取 i >= 6
    a += i;
}
迭代器形式

迭代器形式输出一个表时,如果表中有函数,则输出的顺序及个数不确定(笔者测试得出的结果,具体原因未知)。

迭代器形式的 for 循环的实质

-- 依次返回 迭代器、状态表、迭代器初始值
function mypairs(t)

    function iterator(t,i)
        i = i + 1
        i = t[i] and i      -- 如果 t[i] == nil 则 i = nil;否则 i = i
        return i,t[i]
    end

    return iterator,t,0
    
end

-- 一个表
t = {[1]="1",[2]="2"}

-- 迭代形式 for 语句的 等价形式
do
local f, s, var = mypairs(t)
    while true do
        local var1, var2 = f(s, var)
        var = var1
        if var == nil then break end
        
        -- for 循环中添加的语句
        print(var1,var2)

    end
end

-- 迭代形式 for 语句
for var1,var2 in mypairs(t) do
    print(var1,var2)
end

--> 1   1
--> 2   2
--> 1   1
--> 2   2
数组形式
ary = {[1]=1,[2]=2,[5]=5}
for i,v in ipairs(ary) do
    print(v)                    --> 1 2
end

从1开始,直到数值型下标结束或者值为 nil 时结束。

表遍历
table = {[1]=1,[2]=2,[5]=5}
for k,v in pairs(table) do
    print(v)                    --> 1 2 5
end

遍历整个表的键值对。

关于迭代器的更多内容,可参考Lua 迭代器和泛型 for

2.5.1 数学运算操作符

% 操作符

Lua 中的 % 操作符与 C 语言中的操作符虽然都是取模的含义,但是取模的方式不一样。

在 C 语言中,取模操作是将两个操作数的绝对值取模后,在添加上第一个操作数的符号。

而在 Lua 中,仅仅是简单的对商相对负无穷向下取整后的余数。

+++

在 C 中,

a1 = abs(a);
b1 = abs(b);
c = a1 % b1 = a1 - floor(a1/b1)*b1;

a % b = (a >= 0) ? c : -c;

在 Lua 中,

a % b == a - math.floor(a/b)*b

Lua 是直接根据取模定义进行运算。 C 则对取模运算做了一点处理。

+++

举例:

在 C 中

int a = 5 % 6;
int b = 5 % -6;
int c = -5 % 6;
int d = -5 % -6;

printf("a,b,c,d");--5,5,-5,-5

在 Lua 中

a = 5 % 6
b = 5 % -6
c = -5 % 6
d = -5 % -6

x = {a,b,c,d}

for i,v in ipairs(x) do
    print(i,v)
end


--> 5
--> -1
--> 1
--> -5

可以看到,仅当操作数同号时,两种语言的取模结果相同。异号时,取模结果的符号与数值均不相等。

在 Lua 中的取模运算总结为:a % b,如果 a,b 同号,结果取 a,b 绝对值的模;异号,结果取 b 绝对值与绝对值取模后的差。取模后值的符号与 b 相同。

比较操作的结果是 boolean 型的,非 truefalse

支持的操作符有:

< <= ~= == > >=

不支持 ! 操作符。

+++

对于 == 操作,运算时先比较两个操作数的类型,如果不一致则结果为 false。此时数值与字符串之间并不会自动转换。

比较两个对象是否相等时,仅当指向同一内存区域时,判定为 true。·

a = 123
b = 233
c = "123"
d = "123"
e = {1,2,3}
f = e
g = {1,2,3}

print(a == b)       --> false
print(a == c)       --> false      -- 数字与字符串作为不同类型进行比较
print(c == d)       --> true       
print(e == f)       --> true       -- 引用指向相同的对象
print(e == g)       --> false      -- 虽然内容相同,但是是不同的对象
print(false == nil) --> false      -- false 是 boolean,nil 是 nil 型

方便标记,--> 代表前面表达式的结果。

+++

userdatatable 的比较方式可以通过元方法 eq 进行改变。

大小比较中,数字和字符串的比较与 C 语言一致。如果是其他类型的值,Lua会尝试调用元方法 ltle

2.5.3 逻辑操作符

and,or,not

仅认为 falsenil 为假。

not

取反操作 not 的结果为 boolean 类型。(andor 的结果则不一定为 boolean)

b = not a           -- a 为 nil,b 为 true
c = not not a       -- c 为 false
and

a and b,如果 a 为假,返回 a,如果 a 为真, 返回 b

注意,为什么 a 为假的时候要返回 a 呢?有什么意义?这是因为 a 可能是 false 或者 nil,这两个值虽然都为假,但是是有区别的。

or

a or b,如果 a 为假,返回 b,如果 a 为真, 返回 a。与 and 相反。

+++

提示: 当逻辑操作符用于得出一个 boolean 型结果时,不需要考虑逻辑运算后返回谁的问题,因为逻辑操作符的操作结果符合原本的逻辑含义。

举例

if (not (a > min and a < max)) then  -- 如果 a 不在范围内,则报错
    error() 
end

+++

其他

andor 遵循短路原则,第二个操作数仅在需要的时候会进行求值操作。

例子


a = 5
x = a or jjjj() -- 虽然后面的函数并没有定义,但是由于不会执行,因此不会报错。


print(a)        -->5
print(x)        -->5

通过上面这个例子,我们应当对于逻辑操作有所警觉,因为这可能会引入一些未能及时预料到的错误。

..

连接两个字符串(或者数字)成为新的字符串。对于其他类型,调用元方法 concat

2.5.5 取长度操作符

[待完善]

2.5.6 优先级

由低到高:

or
and
 <     >     <=    >=    ~=    ==
 ..
 +     -
 *     /     %
 not   #     - (unary)
 ^

幂运算>单目运算>四则运算>连接符>比较操作符>and>or

2.5.7 Table 构造

Table 构造的 BNF 定义

tableconstructor ::= `{´ [fieldlist] `}´
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp
fieldsep ::= `,´ | `;´

举例:

a = {}
b = {["price"] = 5; cost = 4; 2+5}
c = { [1] = 2+5, [2] = 2, 8, price = "abc", ["cost"] = 4} -- b 和 c 构造的表是等价的


print(b["price"])   --> 5
print(b.cost)       --> 4
print(b[1])         --> 7       -- 未给出键值的,按序分配下标,下标从 1 开始

print(c["price"])   --> abc
print(c.cost)       --> 4
print(c[1])         --> 8       
print(c[2])         --> 2       

注意:

  • 未给出键值的,按序分配下标,下标从 1 开始
  • 如果表中有相同的键,那么以靠后的那个值作为键对应的值

上面这两条的存在使得上面的例子中 c1 的输出值为 8。

+++

如果表中有相同的键,那么以靠后的那个值作为键对应的值。

a = {[1] = 5,[1] = 6} -- 那么 a[1] = 6

+++

如果表的最后一个域是表达式形式,并且是一个函数,那么这个函数的所有返回值都会加入到表中。

a = 1
function order()
    a = a + 1
    return 1,2,3,4
end

b = {order(); a; order(); }

c = {order(); a; (order());}

print(b[1])                     --> 1       
print(b[2])                     --> 2       -- 表中的值并不是一次把表达式都计算结束后再赋值的
print(b[3])                     --> 1       
print(b[4])                     --> 2       -- 表达式形式的多返回值函数

print(#b)                       --> 6       -- 表的长度为 6                 
print(#c)                       --> 3       -- 函数添加括号后表的长度为 3

函数是一个表达式,其值为 function 类型的对象。函数每次执行都会被实例化。

Lua 中实现一个函数可以有以下三种形式。

f = function() [block] end
local f; f = function() [block] end
a.f = function() [block] end

Lua 提供语法糖分别处理这三种函数定义。

function f() [block] end
local function f() [block] end
function a.f() [block] end

+++

上面 local 函数的定义之所以不是 local f = function() [block] end,是为了避免如下错误:

local f = function()
    print("local fun")
    if i==0 then 
        f()             -- 编译错误:attempt to call global 'f' (a nil value)
        i = i + 1
    end
end
函数的参数

形参会通过实参来初始化为局部变量。

参数列表的尾部添加 ... 表示函数能接受不定长参数。如果尾部不添加,那么函数的参数列表长度是固定的。

f(a,b)
g(a,b,...)
h(a,...,b)              -- 编译错误
f(1)                    --> a = 1, b = nil
f(1,2)                  --> a = 1, b = 2
f(1,2,3)                --> a = 1, b = 2
                        
g(1,2)                  --> a = 1, b = 2, (nothing)
g(1,2,3)                --> a = 1, b = 2, (3)
g(1,f(4,5),3)           --> a = 1, b = 4, (3)
g(1,f(4,5))             --> a = 1, b = 4, (5)

+++

还有一种形参为self的函数的定义方式:

a.f = function (self, params) [block] end

其语法糖形式为:

function a:f(params) [block] end

使用举例:

a = {name = "唐衣可俊"}
function a:f()
    print(self.name)
end
a:f()                       --> 唐衣可俊   -- 如果这里使用 a.f(),那么 self.name 的地方会报错 attempt to index local 'self';此时应该写为 a.f(a)

: 的作用在于函数定义与调用的时候可以少写一个 self 参数。这种形式是对方法的模拟

Lua 中的函数调用的BNF语法如下:

functioncall ::= prefixexp args

如果 prefixexp 的值的类型是 function, 那么这个函数就被用给出的参数调用。 否则 prefixexp 的元方法 "call" 就被调用, call 的第一个参数就是 prefixexp 的值,接下来的是 args 参数列表(参见 2.8 元表 | Metatable)。

函数调用根据是否传入 self 参数分为 . 调用和 : 调用。

函数调用根据传入参数的类型,可以分为参数列表调用、表调用、字符串调用

[待完善]

2.5.10 函数闭包

如果一个函数访问了它的外部变量,那么它就是一个闭包。

由于函数内部的变量均为局部变量,外界无法对其进行访问。这时如果外界想要改变局部变量的值,那么就可以使用闭包来实现这一目的。

具体的实现过程大致是这样,函数内部有能够改变局部变量的子函数,函数将这个子函数返回,那么外界就可以通过使用这个子函数来操作局部变量了。

例子:利用闭包来实现对局部变量进行改变

-- 实现一个迭代器

function begin(i)
    local cnt = i

    return function ()      -- 这是一个匿名函数,实现了自增的功能;同时它也是一个闭包,因为访问了外部变量 cnt
        cnt = cnt + 1
        return cnt
    end
end


iterator = begin(2)     -- 设置迭代器的初值为 2 ,返回一个迭代器函数

print(iterator())           -- 执行迭代
print(iterator())

提示: 关于闭包的更多说明可参考JavaScript 闭包是如何工作的?——StackOverflow

即变量的作用域,见 2.3 变量 部分。

2.7 错误处理

[待补充]

2.8 元表 | Metatable

我们可以使用操作符对 Lua 的值进行运算,例如对数值类型的值进行加减乘除的运算操作以及对字符串的连接、取长操作等(在 2.5 表达式 这一节中介绍了许多类似的运算)。元表正是定义这些操作行为的地方。

元表本质上是一个普通 Lua 表。元表中的键用来指定操作,称为“事件名”;元表中键所关联的值称为“元方法”,定义操作的行为。

仅表(table)类型值对应的元表可由用户自行定义。其他类型的值所对应的元表仅能通过 Debug 库进行修改。

元表中的事件名均以两条下划线 __ 作为前缀,元表支持的事件名有如下几个:

__index     -- 'table[key]',取下标操作,用于访问表中的域
__newindex  -- 'table[key] = value',赋值操作,增改表中的域
__call      -- 'func(args)',函数调用,参见 [2.5.9 函数调用](#2-5-9)

-- 数学运算操作符
__add       -- '+'
__sub       -- '-'
__mul       -- '*'
__div       -- '/'
__mod       -- '%'
__pow       -- '^'
__unm       -- '-'

-- 连接操作符
__concat    -- '..'

-- 取长操作符
__len       -- '#'

-- 比较操作符
__eq        -- '=='
__lt        -- '<'      -- a > b 等价于 b < a
__le        -- '<='     -- a >= b 等价于 b <= a 

还有一些其他的事件,例如 __tostring__gc 等。

下面进行详细介绍。

2.8.2 元表与值

每个值都可以拥有一个元表。对 userdata 和 table 类型而言,其每个值都可以拥有独立的元表,也可以几个值共享一个元表。对于其他类型,一个类型的值共享一个元表。例如所有数值类型的值会共享一个元表。除了字符串类型,其他类型的值默认是没有元表的。

使用 getmetatable 函数可以获取任意值的元表。getmetatable (object)

使用 setmetatable 函数可以设置表类型值的元表。setmetatable (table, metatable)

例子

只有字符串类型的值默认拥有元表:

a = "5"
b = 5
c = {5}
print(getmetatable(a))      --> table: 0x7fe221e06890
print(getmetatable(b))      --> nil
print(getmetatable(c))      --> nil

事先提醒 Lua 使用 raw 前缀的函数来操作元方法,避免元方法的循环调用。

例如 Lua 获取对象 obj 中元方法的过程如下:

rawget(getmetatable(obj)or{}, "__"..event_name)
元方法 index

index 是元表中最常用的事件,用于值的下标访问 -- table[key]

事件 index 的值可以是函数也可以是表。当使用表进行赋值时,元方法可能引发另一次元方法的调用,具体可见下面伪码介绍。

当用户通过键值来访问表时,如果没有找到键对应的值,则会调用对应元表中的此事件。如果 index 使用表进行赋值,则在该表中查找传入键的对应值;如果 index 使用函数进行赋值,则调用该函数,并传入表和键。

Lua 对取下标操作的处理过程用伪码表示如下:

function gettable_event (table, key)
    -- h 代表元表中 index 的值
    local h     
    if type(table) == "table" then

        -- 访问成功
        local v = rawget(table, key)
        if v ~= nil then return v end

        -- 访问不成功则尝试调用元表的 index
        h = metatable(table).__index

        -- 元表不存在返回 nil
        if h == nil then return nil end
    else

        -- 不是对表进行访问则直接尝试元表
        h = metatable(table).__index

        -- 无法处理导致出错
        if h == nil then
            error(···);
        end
    end

    -- 根据 index 的值类型处理
    if type(h) == "function" then
        return h(table, key)            -- 调用处理器
    else 
        return h[key]                   -- 或是重复上述操作
    end
end

例子:

使用表赋值:

t = {[1] = "cat",[2] = "dog"}
print(t[3])             --> nil
setmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}})
print(t[3])             --> pig

使用函数赋值:

t = {[1] = "cat",[2] = "dog"}
print(t[3])             --> nil
setmetatable(t, {__index = function (table,key)
    key = key % 2 + 1
    return table[key]
end})
print(t[3])             --> dog
元方法 newindex

newindex 用于赋值操作 -- talbe[key] = value

事件 newindex 的值可以是函数也可以是表。当使用表进行赋值时,元方法可能引发另一次元方法的调用,具体可见下面伪码介绍。

当操作类型不是表或者表中尚不存在传入的键时,会调用 newindex 的元方法。如果 newindex 关联的是一个函数类型以外的值,则再次对该值进行赋值操作。反之,直接调用函数。

不是太懂:一旦有了 "newindex" 元方法, Lua 就不再做最初的赋值操作。 (如果有必要,在元方法内部可以调用 rawset 来做赋值。)

Lua 进行赋值操作时的伪码如下:

function settable_event (table, key, value)
    local h
    if type(table) == "table" then

        -- 修改表中的 key 对应的 value
        local v = rawget(table, key)
        if v ~= nil then rawset(table, key, value); return end

        -- 
        h = metatable(table).__newindex

        -- 不存在元表,则直接添加一个域
        if h == nil then rawset(table, key, value); return end
    else
        h = metatable(table).__newindex
        if h == nil then
            error(···);
        end
    end

    if type(h) == "function" then
        return h(table, key,value)    -- 调用处理器
    else 

 
        h[key] = value             -- 或是重复上述操作
    end
end

例子:

元方法为表类型:

t = {}
mt = {}

setmetatable(t, {__newindex = mt})
t.a = 5
print(t.a)      --> nil
print(mt.a)     --> 5

通过两次调用 newindex 元方法将新的域添加到了表 mt 。

+++

元方法为函数:

-- 对不同类型的 key 使用不同的赋值方式
t = {}
setmetatable(t, {__newindex = function (table,key,value)
    if type(key) == "number" then
        rawset(table, key, value*value)
    else
        rawset(table, key, value)
    end
end})
t.name = "product"
t[1] = 5
print(t.name)       --> product
print(t[1])         --> 25

call 事件用于函数调用 -- function(args)

Lua 进行函数调用操作时的伪代码:

function function_event (func, ...)

  if type(func) == "function" then
      return func(...)   -- 原生的调用
  else
      -- 如果不是函数类型,则使用 call 元方法进行函数调用
      local h = metatable(func).__call
      
      if h then
        return h(func, ...)
      else
        error(···)
      end
  end
end

例子:

由于用户只能为表类型的值绑定自定义元表,因此,我们可以对表进行函数调用,而不能把其他类型的值当函数使用。

-- 把数据记录到表中,并返回数据处理结果
t = {}

setmetatable(t, {__call = function (t,a,b,factor)
  t.a = 1;t.b = 2;t.factor = factor
  return (a + b)*factor
end})

print(t(1,2,0.1))       --> 0.3

print(t.a)              --> 1
print(t.b)              --> 2
print(t.factor)         --> 0.1

运算操作符相关元方法自然是用来定义运算的。

以 add 为例,Lua 在实现 add 操作时的伪码如下:

function add_event (op1, op2)
  -- 参数可转化为数字时,tonumber 返回数字,否则返回 nil
  local o1, o2 = tonumber(op1), tonumber(op2)
  if o1 and o2 then  -- 两个操作数都是数字?
    return o1 + o2   -- 这里的 '+' 是原生的 'add'
  else  -- 至少一个操作数不是数字时
    local h = getbinhandler(op1, op2, "__add") -- 该函数的介绍在下面
    if h then
      -- 以两个操作数来调用处理器
      return h(op1, op2)
    else  -- 没有处理器:缺省行为
      error(···)
    end
  end
end

代码中的 getbinhandler 函数定义了 Lua 怎样选择一个处理器来作二元操作。 在该函数中,首先,Lua 尝试第一个操作数。如果这个操作数所属类型没有定义这个操作的处理器,然后 Lua 会尝试第二个操作数。

 function getbinhandler (op1, op2, event)
   return metatable(op1)[event] or metatable(op2)[event]
 end

+++

对于一元操作符,例如取负,Lua 在实现 unm 操作时的伪码:

function unm_event (op)
  local o = tonumber(op)
  if o then  -- 操作数是数字?
    return -o  -- 这里的 '-' 是一个原生的 'unm'
  else  -- 操作数不是数字。
    -- 尝试从操作数中得到处理器
    local h = metatable(op).__unm
    if h then
      -- 以操作数为参数调用处理器
      return h(op)
    else  -- 没有处理器:缺省行为
      error(···)
    end
  end
end

例子:

加法的例子:

t = {}
setmetatable(t, {__add = function (a,b)
  if type(a) == "number" then
      return b.num + a
  elseif type(b) == "number" then
      return a.num + b
  else
      return a.num + b.num
  end
end})

t.num = 5

print(t + 3)  --> 8

取负的例子:

t = {}
setmetatable(t, {__unm = function (a)
  return -a.num
end})

t.num = 5

print(-t)  --> -5

对于连接操作,当操作数中存在数值或字符串以外的类型时调用该元方法。

对于取长操作,如果操作数不是字符串类型,也不是表类型,则尝试使用元方法(这导致自定义的取长基本没有,在之后的版本中似乎做了改进)。

对于三种比较类操作,均需要满足两个操作数为同类型,且关联同一个元表时才能使用元方法。

对于 eq (等于)比较操作,如果操作数所属类型没有原生的等于比较,则调用元方法。

对于 lt (小于)与 le (小于等于)两种比较操作,如果两个操作数同为数值或者同为字符串,则直接进行比较,否则使用元方法。

对于 le 操作,如果元方法 "le" 没有提供,Lua 就尝试 "lt",它假定 a <= b 等价于 not (b < a) 。

对于 tostring 操作,元方法定义了值的字符串表示方式。

例子:

取长操作:

t = {1,2,3,"one","two","three"}
setmetatable(t, {__len = function (t)
  local cnt = 0
  for k,v in pairs(t) do
    if type(v) == "number" then 
      cnt = cnt + 1
      print(k,v)
    end
  end
  return cnt
end})

-- 结果是 6 而不是预期中的 3
print(#t)   --> 6 

等于比较操作:

t = {name="number",1,2,3}
t2 = {name = "number",4,5,6}
mt = {__eq = function (a,b)
    return a.name == b.name
end}
setmetatable(t,mt)              -- 必须要关联同一个元表才能比较
setmetatable(t2,mt)

print(t==t2)   --> true

tostring 操作:

t = {num = "a table"}
print(t)              --> table: 0x7f8e83c0a820

mt = {__tostring = function(t)
  return t.num
end}
setmetatable(t, mt)

print(tostring(t))    --> a table
print(t)              --> a table

2.9 环境表

类型 threadfunctionuserdata 的对象除了能与元表建立关联外,还能关联一个环境表。

关联在线程上的环境表称为全局环境。

全局环境作为子线程及子函数的默认环境。

全局环境能够直接被 C 调用。

关联在 Lua 函数上的环境表接管函数对全局变量的所有访问。并且作为子函数的默认环境。

关联在 C 函数上的环境能直接被 C 调用。

关联在 userdata 上的环境没有实际的用途,只是为了方便程序员把一个表关联到 userdata 上。

2.10.1 垃圾收集的元方法

[待补充]

2.10.2 弱表

弱表是包含弱引用的表。

弱表的弱引用方式有三种。键弱引用,值弱引用,键和值均弱引用

可以通过元表中的 __mode 域来设置一个表是否有弱引用,以及弱引用的方式。

a = {}
b = { __mode = "k"}  -- 引号中添加 k 表示 key 弱引用,v 表示 value 弱引用, kv 表示均弱引用。
setmetable(a,b)     -- b 是 a 的元表,绑定后就不能在更改 __mode 的值。

垃圾回收机制会把弱引用的部分回收。但是不论是哪种弱引用,回收机制都会把整个键值对从弱表中移除。

3 程序接口 (API)

这部分描述 Lua 的 C API,即用来与 Lua 进行通信的 C 函数,所有的函数和常量都定义在 lua.h 头文件里面。

有一部分 C 函数是用宏来实现的。为什么?:由于所有的宏只会使用他们的参数一次(除了第一个参数,即 Lua 状态机),所以不必担心宏展开带来的副作用。

默认情况下 Lua 在进行函数调用时不会检查函数的有效性和坚固性,如果想要进行检查,则使用 luaconf.h 中的 luai_apicheck() 函数开启。

3.1 堆栈

Lua 调用 C API 时使用一个虚拟栈来传递参数,栈中的所有元素都是 Lua 的类型(例如 booleantablenil等)。

Lua 调用 C 函数的时候都会新建一个虚拟栈,而不是使用旧栈或者其他的栈。同时在 C 函数中,对 Lua API 调用时,只能使用当前调用所对应栈中的元素,其他栈的元素是无法访问的。

虚拟栈中包含 C 函数所需的所有参数,函数的返回值也都放在该栈中。

这里所谓的栈概念并不是严格意义上的栈,可以通过下标对栈中的元素进行访问。1表示栈底,-1表示栈顶,又例如 3 表示从栈底开始的第三个元素。

3.2 堆栈尺寸

由于 Lua 的 C API 默认不做有效性和坚固性(鲁棒性)检测,因此开发人员有责任保证坚固性。特别要注意的是,不能让堆栈溢出。Lua 只保证栈大小会大于 LUA_MINSTACK(一般是 20)。开发人员可以使用 lua_checkstack 函数来手动设置栈的大小。

3.3 伪索引

除了用索引访问函数堆栈的 Lua 元素,C 代码还可以使用伪索引来访问堆栈以外的 Lua 元素,例如线程的环境、注册表、函数的环境 以及 C函数的 upvalue(上值)。可以通过特别声明来禁用伪索引。

线程的环境放在伪索引 LUA_GLOBALSINDEX 处,函数的环境放在伪索引 LUA_ENVIRONINDEX 处。

访问环境的方式跟访问表的方式是一致的,例如要访问全局变量的值,可以使用:

lua_getfield(L,LUA_GLOBALSINDEX,varname)

3.4 C 闭包

当我们把创建出来的函数和一些值关联在一起,就得到了一个闭包。那些关联起来的值称为 upvalue (上值)。

函数的上值都放在特定的伪索引处,可以通过 lua_upvalueindex 获取上值的伪索引。例如 lua_upvalueindex(3) 表示获取第三个关联值(按照关联顺序排列)对应的伪索引。

3.5 注册表

Lua 提供了一个注册表,C 代码可以用来存放想要存放的 Lua 值。注册表用伪索引 LUA_REGISTRYINDEX 定位。

为了避免命名冲突,一般采用包含库名的字符串作为键名。什么东西?:或者可以取你自己 C 代码 中的一个地址,以 light userdata 的形式做键。

注册表中的整数键有特定用途(用于实现补充库的引用系统),不建议用于其他用途。

3.6 C 中的错误处理

[待补充]

3.7 函数和类型

本节介绍 C API 中的函数和类型。

4 辅助库

辅助库为我们用 Lua 与 C 的通信提供了一些方便的函数。基础 API 提供 Lua 与 C 交互的所有原始函数。辅助库作为更高层次的函数来解决一些通用的问题。

辅助库的所有函数定义在头文件 luaxlib.h 中,函数带有前缀 luaL_

辅助库函数是建立在基础库之上的,所以基础库做不了的事情辅助库也做不了。

有一些函数是用来检查函数参数的,这些函数都有这样的前缀 luaL_check 或者 luaL_opt。这些函数在检查出问题后会抛出错误。由于抛出的错误消息表明是参数错误(例如,“bad argument #1”),因此不要把这些函数用在参数以外的 Lua 值上。

标准 Lua 库提供了许多有用的函数,这些函数都是直接用 C API 实现的。有一些函数提供了 Lua 语言本身所必要的服务(例如,typegetmetatable);有一些提供了通向“外部”的服务(例如,I/O);还有一些函数,可以由 Lua 进行实现,但是由于相当有用或者有重要的性能需求需要由 C 实现(例如 sort)。

所有的库都以 C 模块的形式分开提供。5.1中,Lua有以下几种标准库:

- 基础库
- 包库
- 字符串操作库
- 表操作库
- 数学功能库
- 输入输出库
- 操作系统工具库
- 调试工具库

除了基础库和包库,其他库都是作为全局表的域或者对象的方法提供。

[待补充]

5.1 基础库函数

基础库为 Lua 提供了一些核心函数。如果没有包含这个库,那么就可能需要自己来实现一些 Lua 语言特性了。

assert (v [, message])

如果其参数 v 的值为假(nilfalse), 它就调用 error; 否则,返回所有的参数。 在错误情况时, message 指那个错误对象; 如果不提供这个参数,参数默认为 "assertion failed!" 。

例子
assert(5==4,"Number Not Equal!")    --> 报错 Number Not Equal!
assert(nil)                         --> 报错 assertion failed!

collectgarbage (opt [, arg])

控制垃圾回收器的参数有两个,pause 和 step multipier。

参数 pause 控制了收集器在开始一个新的收集周期之前要等待多久。 随着数字的增大就导致收集器工作工作的不那么主动。 小于 1 的值意味着收集器在新的周期开始时不再等待。 当值为 2 的时候意味着在总使用内存数量达到原来的两倍时再开启新的周期。

参数 step multiplier 控制了收集器相对内存分配的速度。 更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。 小于 1 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。 缺省值为 2 ,这意味着收集器将以内存分配器的两倍速运行。

该函数是垃圾回收器的通用接口,根据 opt 参数的不同实现不同的功能。

  • "stop": 停止垃圾回收。
  • "restart": 重启垃圾回收。
  • "collect": 执行垃圾回收的完整循环。
  • "count": 返回 Lua 当前使用的总内存(单位为 Kb)。
  • "step": 执行一步(一步可由多步组成)垃圾回收。步数可由参数 arg 控制(值越大,步数越多,0 表示执行一步(指最小的一步))。如果执行后完成了回收循环,返回 true
  • "setpause": 把 arg/100 作为垃圾回收参数 pause 的新值。
  • "setstepmul": 把 arg/100 作为垃圾回收参数 step mutiplier 的新值。
例子

进行垃圾回收前后的内存占用情况:

x = collectgarbage("count")
print(x)            --> 27.5615234375
collectgarbage("collect")
x = collectgarbage("count")
print(x)            --> 26.7490234375

dofile (filename)

打开该名字的文件,并执行文件中的 Lua 代码块。 不带参数调用时, dofile 执行标准输入的内容(stdin)。 返回该代码块的所有返回值。 对于有错误的情况,dofile 将错误反馈给调用者 (即,dofile 没有运行在保护模式下)。

例子

同一目录下新建两个 Lua 文件,代码及输出:

-- another.lua
return "Message from another file!"

-- sample.lua
x = dofile("./another.lua")
print(x)    --> Message from another file!

dofile 在这里等价于

function dofile()
    function func()
        return "Message from another file!"
    end
    
    return func()
end

于是等价的输出为

print(dofile()) --> Message from another file!

error (message [, level])

终止所保护的函数,抛出 message 消息,不再返回。

通常这个函数会在抛出的消息前面加上发生错误的地址信息。堆栈等级决定添加哪个地址。如果堆栈等级为 0 ,不返回地址信息;如果为 1,返回 error 语句所在位置;如果为 2,返回调用 error所在函数的位置;依此类推。

定义
error([报错消息],[堆栈等级]=1)
例子
function division(a,b)
    if b == 0 then error("Divisor cannot be 0!",2) end      -- level 值为 1 时,错误信息指向这里
    return a / b
end

print(division(5,1))
print(division(5,0))        -- level 值为 2 时,错误信息指向这里

_G

_G 持有全局环境的变量,Lua 本身用不到这个变量,更改变量不会影响到全局环境;反过来也一样。

getfenv (f)

返回函数的环境。 f 可以是一个 Lua 函数,也可以是函数在堆栈中的等级。等级 1 代表调用 getfenv() 的那个函数。如果传入的函数不是 Lua 函数,或者 f 是 0,那么 getfenv 返回 全局环境。

[待补充]不是太明白,没能给出合适的代码

参考 Lua中的环境概念

定义
getfenv([目标函数]=1)
例子

获取全局环境:

for k,v in pairs(_G) do
print(k,v)
end

--string            table: 0x7ff200f02330
--xpcall            function: 0x7ff200d03cc0
--package           table: 0x7ff200e00000
--tostring          function: 0x7ff200d04560
--print             function: 0x7ff200d046a0
--os                table: 0x7ff200f01cb0
--unpack            function: 0x7ff200d04610
--require           function: 0x7ff200f006f0
--getfenv           function: 0x7ff200d042f0
--setmetatable      function: 0x7ff200d044a0
--next              function: 0x7ff200d04260
--assert            function: 0x7ff200d03fc0
--tonumber          function: 0x7ff200d04500
--io                table: 0x7ff200f014a0
--rawequal          function: 0x7ff200d046f0
--collectgarbage    function: 0x7ff200d04010
--arg               table: 0x7ff200e01360
--getmetatable      function: 0x7ff200d04340
--module            function: 0x7ff200f006a0
--rawset            function: 0x7ff200d047a0
--math              table: 0x7ff200e00040
--debug             table: 0x7ff200e00a00
--pcall             function: 0x7ff200d042b0
--table             table: 0x7ff200f00790
--newproxy          function: 0x7ff200d04820
--type              function: 0x7ff200d045c0
--coroutine         table: 0x7ff200d048c0
--_G                table: 0x7ff200d02fc0
--select            function: 0x7ff200d04400
--gcinfo            function: 0x7ff200d03000
--pairs             function: 0x7ff200d03e00
--rawget            function: 0x7ff200d04750
--loadstring        function: 0x7ff200d04200
--ipairs            function: 0x7ff200d03d70
--_VERSION          Lua 5.1
--dofile            function: 0x7ff200d04110
--setfenv           function: 0x7ff200d04450
--load              function: 0x7ff200d041b0
--error             function: 0x7ff200d04160
--loadfile          function: 0x7ff200d043a0

getmetatable (object)

如果对象没有元表,返回空;如果对象有 __metatable 域,返回对应的值;否则,返回对象的元表。

例子

对象有 __metatable 域的情况:

t = {num = "a table"}

mt = {__index = {x = 1,y = 2},__metatable = {__index = {x = 5,y = 6}}}
setmetatable(t, mt)

print(getmetatable(t).__index.x)  --> 5
print(t.x)                        --> 1

进行操作时的元表依旧是与值直接关联的那个元表,不知道这样子处理有什么作用?

ipairs (t)

返回三个值:迭代器、传入的表 t、值 0 。迭代器能够根据传入的表 t 和索引 i 得到 i+1 和 t[i+1] 的值。

其实现形式类似于这样:

function ipairs(t)

    function iterator(t,i)
        i = i + 1
        i = t[i] and i      -- 如果 t[i] == nil 则 i = nil;否则 i = i
        return i,t[i]
    end

    return iterator,t,0
    
end
例子

使用 ipairs 对表进行遍历,会从键值为 1 开始依次向后遍历,直到值为 nil。

t = {"1","2",nil,[4]="4"}
-- t = {"1","2",[4]="4"}   -- 使用该表会得到相同的输出

for i,v in ipairs(t) do
    print(i,v)
end

--> 1   1
--> 2   2

load (func [, chunkname])

通过传入函数 func 的返回值获取代码块片段。func 函数的后一次调用返回的字符串应当能与前一次调用返回的字符串衔接在一起,最终得到完整的代码块。函数返回 nil 或无返回值时表示代码块结束。

load 函数会将得到的代码块作为函数返回。返回函数的环境为全局环境。如果出现错误,load 会返回 nil 和 错误信息。

chunkname 作为该代码块的名称,用在错误信息与调试信息中。

例子

[待补充]

loadfile ([filename])

使用方式与 dofile 类似,函数内容与 load 类似。从文件中获取代码块,如果没有指定文件,则从标准输入中获取。

loadfile 把文件代码编译为中间码,以文件代码作为一个代码块(chunk),并返回包含此代码块的函数。

编译代码成中间码,并返回编译后的chunk作为一个函数。 如果出现错误,则返回 nil 以及错误信息。

使用 loadfile,可以一次编译多次运行;而每次使用 dofile,都会执行一次编译。

例子

同一目录下新建两个 Lua 文件,代码及输出:

-- sample.lua
f = loadfile("./sample.lua")
print(f())
--> Message from another file!
--> 0

-- another.lua
function fun()
 print("Message from another file!")
 return 0
end
res = fun()
return res

loadfile 在这里等价于

function loadfile()
    function fun()
     print("Message from another file!")
     return 0
    end
    res = fun()
    return res
end

loadstring (string [, chunkname])

与 load 类似,只不过是从字符串中获取代码块。

要想加载并运行所给的字符串,使用如下惯用形式:

assert(loadingstring(s))()

next (table [, index])

返回传入的表中下一个键值对。

定义
next([表],[键]=nil)

第一个参数是要操作的表,第二个参数是表中的某个键。如果传入的键值为 nil ,则函数返回第一个键值对。如果传入一个有效的键值,则输出下一对键值对。如果没有下一个键值对,返回 nil。

根据定义,可以使用 next 来判断一个表是否为空表。

注意:

键值对的遍历顺序是不一定的,即使是对数字索引也是如此。如果想要按照数字索引的顺序获取键值对,参见 ipairs (t) 函数。

例子
t = {"table",["a"] = 5, ["c"] = 6}

-- index 为 nil
print(next(t, nil))         --> 1   table

-- index 为 无效键
print(next(t,"d"))          --> 编译错误

-- index 为 数字索引
print(next(t,1))            --> a   5

-- index 为 一般键
print(next(t, "a"))         --> c   6

-- index 为 最后一个键
print(next(t,"c"))          --> nil

遍历顺序与定义顺序不一致的例子:

t = {[1]="table",b = 4,["a"] = 5, ["c"] = 6, func}

t.func = function (...)
    return true
end

for k,v in pairs(t) do
    print(k,v)
end
--> a   5
--> func    function: 0x7f7f63c0ad50
--> c   6
--> b   4

而且从上面的例子中可以看出 name = exp 的键值对形式会占用

pairs (t)

返回三个值:next 函数,表 t,nil。通常用来遍历表中的所有键值对。

如果 t 有元方法 __pairs ,将 t 作为参数 传入该函数并返回前三个返回值。

在使用 pairs 函数遍历表的过程中,可以删除域或者修改已有的域,但是如果添加新的域,可能出现无法预期的问题。

例子
t = {"table",["a"] = 5, ["c"] = 6}
for k,v in pairs(t) do
    print(k,v)
end
--> 1   table
--> a   5
--> c   6

在遍历表的过程中添加新的域导致问题出现的情况:

t = {"table",["a"] = 5, ["c"] = 6}

for k,v in pairs(t) do
    -- 添加一个新的域
    if k == 'a' then
        t[2] = 8
    end
    print(k,v)
end
--> 1   table
--> a   5

pcall (f [, arg1, ...])

以保护模式调用传入的函数,也就是说不会抛出错误。如果捕获到抛出的错误,第一个参数返回 false,第二个参数返回错误信息;如果没有出现错误,第一个参数返回 ture,后面的参数返回传入函数的返回值。

function fun(a,b)

    assert(not(b == 0), "divisor can't be 0 !")

    return a / b    
end

success, res = pcall(fun,5,0) --> false .../sample.lua:3: divisor can't be 0 !
success, res = pcall(fun,5,1) --> true  5

print (...)

仅作为快速查看某个值的工具,不用做格式化输出。正式的格式化输出见 string.format 与 io.write。

rawequal (v1, v2)

raw 作为前缀的函数均表示该方法在不触发任何元方法的情况下调用。

rawequal 检查 v1 是否与 v2 相等,返回比较结果。

例子
t = {"value"}
s = "value"
s2 = "value"
print(rawequal(t, s))     --> false
print(rawequal(s, s2))    --> true

rawget (table, index)

获取 table 中键 index 的关联值,table 参数必须是一个表,找不到返回 nil 。

例子
t = {"value",x = 5}

print(rawget(t, 1))     --> value
print(rawget(t, "x"))   --> 5
print(rawget(t, 2))     --> nil
print(rawget("value",1))--> bad argument #1 to 'rawget' (table expected, got string)

rawset (table, index, value)

将 table[index] 的值设置为 value 。table 必须是一张表,index 不能是 nil 或 NaN 。value 可以是任何值。返回修改后的 table 。

例子
t = {"value",x = 5}
t2 = {"sub table"}
rawset(t, 1,"new value")
rawset(t, "y", 6)
rawset(t, t2,"sub table")
rawset(t,NaN,"NaN")         --> table index is nil

print(t[1])                 --> new value
print(t.y)                  --> 6
print(t[t2])                --> sub table

select (index, ...)

index 可以是数字或者字符 '#' 。当 index 为数字时,返回第 index + 1 个参数及后面的参数(支持负数形式的 index);当 index 为 '#' 时,返回参数的个数(不包括第一个参数)。

例子
t = {"table",x = 5}
t2 = {"table2"}

print(select(  1, 1, t, t2))    --> 1  table: 0x7fad7bc0a830 table: 0x7fad7bc0ac20
print(select( -3, 1, t, t2))    --> 1  table: 0x7fad7bc0a830 table: 0x7fad7bc0ac20
print(select("#", 1, t, t2))    --> 3

setfenv (f, table)

设置函数 f 的环境表为 table 。f 可以是一个函数,或者是代表栈层级的数字。栈层级为 1 的函数是那个调用 setfenv 的函数,栈层级为 2 的函数就是更上一层的函数。 setfenv 返回 f。

特别的,如果 f 为 0,那么 setfenv 会把全局环境设置为 table 。并且不做任何返回。

Lua 的之后版本中去掉了 setfenv 和 getfenv 函数。

例子

使用栈层级操作 setfenv 的例子:

function foobar(...)

    -- 设置 foobar 的环境
    t = {}
    setmetatable(t, {__index = _G })
    setfenv(1,t)
    a = 1
    b = 2

    -- 输出 foobar 的环境
    for k,v in pairs(getfenv(1)) do
        print(k,v)
    end
    print()

    function foo(...)
        -- 设置 foo 的环境,继承 foobar 的环境
        local t = {}
        setmetatable(t, {__index = _G})
        setfenv(1,t)
        x = 3
        y = 4
        
        -- 输出 foo 的环境
        for k,v in pairs(getfenv(1)) do
            print(k,v)
        end
        print()

        -- 再次设置 foobar 的环境
        setfenv(2, t)
    end
    

    foo()

    -- 再次输出 foobar 的环境
    for k,v in pairs(getfenv(1)) do
        print(k,v)
    end
end

foobar()

--> a   1
--> b   2
--> 
--> y   4
--> x   3
--> 
--> y   4
--> x   3

将 setfenv 用于模块加载:

-- sample.lua 文件
local FuncEnv={}    -- 作为环境
setmetatable(FuncEnv, {__index = _G}) -- 为了能够访问原本全局环境的值,将全局环境表(_G)放在元表中

local func=loadfile("other.lua")    -- 返回一个函数,函数以 other 文件内容作为代码块
setfenv(func,FuncEnv)
func()                              -- 执行代码块,得到定义的 message 函数,该函数会存在环境中
FuncEnv.message()                   --通过环境调用函数,FuncEnv 此时就相当于一个独立模块
-- other.lua 文件
function message()
    print("Message from another file!")
end

本小节参考了 斯芬克斯设置函数环境——setfenvicydaylua5.1中的setfenv使用 两篇博客。

setmetatable (table, metatable)

给 table 关联元表 metatable 。返回参数 table 。

如果元表定义了 __metatable 域,会抛出错误。

metatable 参数为 nil 表示解除已经关联的元表。

例子
-- 关联一个定义了加法操作的元表
t = setmetatable({}, {__add = function(a,b)
    if type(a) == "table" and type(b) == "table" then
        return a.num + b.num
    end
end})

t.num = 5
t2 = {num = 6}
print(t+t2)         --> 11      -- 只要有一个表进行了关联就能够进行运算

setmetatable(t, nil)            

-- 解除关联后再进行加法运算会报错
print(t+t2)         --> attempt to perform arithmetic on global 't' (a table value)

tonumber (e [, base])

tonumber([值],[基数]=10)

尝试把 e 转换为十进制数值并返回。如果无法转换返回 nil 。

base 表示传入参数的进制,默认为 10 进制。base 的可输入范围 [2,36]。高于 10 的数字用字母表示,A-Z 分别表示 11-35 。

例子
print(tonumber(123))            --> 123
print(tonumber("123"))          --> 123
print(tonumber("abc"))          --> nil
print(tonumber("abc", 20))      --> 4232
print(tonumber("ABC", 20))      --> 4232

tostring (e)

能将任意类型的值转换为合适的字符串形式返回。要控制数字转换为字符串的方式,使用 string.format(formatstring,...)

如果值所关联的元表有 __tostring 域,则使用该域的元方法获取字符串。

例子
function func()
    print("this is a function")
end
t = {name = "table"}

print(tostring(123))        --> 123
print(tostring("abc"))      --> abc
print(tostring(func))       --> function: 0x7f86348013b0
print(tostring(t))          --> table: 0x7f86348013e0

type (v)

返回 v 的类型,类型以字符串形式返回。 有以下八种返回值: "nil" , "number", "string", "boolean", "table", "function", "thread", "userdata"。

例子
type(nil)                   --> "nil"
type(false)                 --> "boolean"
type(123)                   --> "number"
type("abc")                 --> "string"

print(type(nil) == "nil")   --> true

unpack (list [, i [, j]])

unpack([列表],[起始位置]=1,[返回个数]=[列表长度])

返回表中的各个域的值,等价于返回

return list[i], list[i+1], ···, list[j]
例子
t = {1,2,3,a = 4,b = 5}

print(unpack(t, 1, 4))      --> 1   2   3   nil

_VERSION

包含有当前解释器版本号的全局变量,当前版本的值为 "Lua 5.1"。

xpcall (f, err [, arg1, ...]

pcall (f, arg1, ...) 类似。不同的是,如果 f 函数抛出了错误,那么 xpcall 不会返回从 f 抛出的错误信息,而是使用 err 函数返回的错误信息。

例子
function fun(a,b)   -- 这里的参数没什么实际作用,就是展示下用法
    error("something wrong !!", 1)
end

-- pcall 
local success, res = pcall(fun,1,2)
print(success,res)      --> false   .../sample.lua:2: something wrong !!

-- xpcall
local success, res = xpcall(fun,function()
    return "an error occured !!"
end,1,2)
print(success,res)      --> false   an error occured !!

5.4 字符串操作

5.4.1 模式

字符类

字符类代表一组字符。可以用下列组合来表示一个字符类。

组合代表字母代表字符类型
x(变量 x)^$()%.[]*+-?以外的任一字符
.(dot)任意字符
%a(alphabet)字母
%b(bracket)对称字符以及字符间的内容
%c(control)控制字符(即各类转义符)
%d(digits)数字
%l(lowercase)小写字母
%p(punctuation)标点符号
%s(space)空格
%u(uppercase)大写字母
%w(word)字母和数字
%x(hexadecimal)十六进制字符
%z(zero)值为 0 的字符,即 '\0'
%x(变量 x)字母和数字以外的任一字符

如果组合中的字符写成大写形式(例如将 '%a' 写成 '%A'),相当于对原来所代表的字符类型取补集

例子:

前两行的数字标出每个字符的下标。find函数返回找出第一个符合查找条件的字符的下标。

-----------------00000000001111111112 222222222333333333344444444445555 5
-----------------12345678901234567890 123456789012345678901234567890123 4
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","i")
    --> 6
x = string.find("Tutu is a young man.\n His student number is 20230001.\0",".")
    --> 1
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%a")    --> 1   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%c")    --> 21 
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%d")    --> 45 
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%l")    --> 2   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%p")    --> 20 
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%s")    --> 5   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%u")    --> 1   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%w")    --> 1   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%x")    --> 9   
x = string.find("Tutu is a young man.\n His student number is 20230001.\0","%z")    --> 54 

() 表示捕捉,find的第三个参数返回被捕捉到的字符串,在这里即返回找到的那个字符。

x,y,z = string.find("%()~!@#$%^&*():\";\'?<>._[]","(%%)")   --> 1   1   %
x,y,z = string.find("%()~!@#$%^&*():\";\'?<>._[]","(%#)")   --> 7   7   #
x,y,z = string.find("%()~!@#$%^&*():\";\'?<>._[]","(%\")")  --> 16  16  "

下句中的 + 表示取一个或多个满足条件的连续字符。

                 --1 2 3 4 5 6 7 8
x,y = string.find("\a\b\f\n\r\t\v\0","%c+")     --> 1   7

上句基本列出了所有控制字符,并不是所有转义符都是控制字符,例如 \\\xff 不属于控制字符。

match 函数返回符合匹配条件的字符子串。

x = string.match("0123456789ABCDEFabcdefg","%x+")   --> 0123456789ABCDEFabcdef

输出的符号即为 %x 所支持的所有字符。

%b 的使用方法与前面的组合形式略有不同,其形式为 %bxy,使用示例如下:

---------------------00000000001111111112 22222222233333333334444444444555555 5
---------------------12345678901234567890 12345678901234567890123456789012345 6
x,y,z = string.find("Tutu is a young man.\n His student number is [20230001].\0","(%b[])")  --> 45  54  [20230001]
x,y,z = string.find("Tutu is a young man.\n His student number is _20230001_.\0","(%b__)")  --> 45  54  _20230001_
x,y,z = string.find("Tutu is a young man.\n His student number is _20230001_.\0","(%b21)")  --> 48  53  230001
[] 字符集

字符集操作是对字符类组合的一个扩展。可以通过 [] 制定出用户所需的一套字符选取范围。

---------------------0000000001111111111222222222
---------------------1234567890123456789012345678
x,y,z = string.find("[Email]: tangyikejun@163.com","([123])")           --> 22  22  1
x,y,z = string.find("[Email]: tangyikejun@163.com","([l]])")            --> 6   7   l]
x,y,z = string.find("[Email]: tangyikejun@163.com","([1-3])")           --> 22  22  1
x,y,z = string.find("[Email]: tangyikejun@163.com","([^1-3])")          --> 1   1   [
x,y,z = string.find("[Email]: tangyikejun@163.com","([^%d])")           --> 1   1   [
x,y,z = string.find("[Email]: tangyikejun@163.com","([0-9][%d][%d])")   --> 22  24  163
x,y,z = string.find("[Email]: tangyikejun@163.com","([0-9]+)")          --> 22  24  163

使用特点:

  1. 每个字符集仅限定一个字符的范围。
  2. 连字符 - 用于限定字符的范围,值域根据字符在ASCII码中对应的值得出,例如 [0-7] 代表字符范围为 0-7。

    x,y,z = string.find("!\"#$%&0123","([$-1]+)") --> 4 8 $%&01

  3. 添加 ^ 表示对指定的字符范围取补集。[^%d] 等价于 [%D]
模式项作用
  •   | 匹配1个或多个字符,尽可能多地匹配
    
  •   | 匹配0个或多个字符,尽可能少地匹配
    
  •   | 匹配0个或多个字符,尽可能多地匹配
    

? | 匹配0个或1个字符,尽可能多地匹配

使用特点:

  1. 模式项都是针对前一个字符而言的。例如 abc- 作用于字符 c
---------------------0000000001
---------------------1234567890
x,y,z = string.find("aaaabbbccc","(%a+)")       --> 1   10  aaaabbbccc
x,y,z = string.find("bbbccc","(a+)")            --> nil nil nil
x,y,z = string.find("aaaabbbccc","(ab-c)")      --> 4   8   abbbc
-- x,y,z = string.find("aaaaccc","(ab-c)")      --> 4   5   ac
-- x,y,z = string.find("aaaaccc","(ab*c)")      --> 4   5   ac
-- x,y,z = string.find("aaaabccc","(ab?c)")     --> 4   6   abc
-- x,y,z = string.find("aaaabccc","(ba?c)")     --> 5   6   bc
---------------------000000000111 111111122
---------------------123456789012 345678901
x,y,z = string.find("tangyikejun\0 163.com","(%z%s%w+)")    --> 12  16  
x,y,z = string.find("tangyikejun\0163.com","(%z%d%w+)")     --> nil nil     nil 

注意:\0 后面不能跟数字。而且用 find 返回的匹配字符串无法输出 \0 之后的部分。

多个模式项组合形成模式

  • 模式的前面添加 ^ 表示匹配从目标字符串的起始位置开始。
  • 模式的末尾添加 $ 表示匹配目标字符串的末尾子串。
  • 其他位置的 ^$ 作为普通字符处理。
---------------------0000000001111111111222222222
---------------------1234567890123456789012345678
x,y,z = string.find("[Email]: tangyikejun@163.com","^(.%a+)")   -->1    6   [Email
x,y,z = string.find("[Email]: tangyikejun@163.com","(%a+)$")    -->26   28  com
()捕捉

捕捉是指将括号内的组合匹配结果保存起来,每个括号保存一个结果。

保存的数据的顺序按照左括号的顺序排列。

x,y,z,h,l = string.find("Tutu is a young man.\n His student number is _20230001_.\0","((%a+%s)(%a+%s)%b__)")    --> 35  54  number is _20230001_    number  is 

字符串模式匹配可参考Lua模式匹配

  • 字符串的下标从1开始。正数下标表示正向下标,负数表示反向下标(例如 -1 表示字符串最后一个字符)。
  • 函数均默认支持模式匹配。
  • 返回的匹配字符串无法输出 \0 之后的部分。

string.find(s,pattern[,init[,plain]])

查找字符串的子串,如果找到,返回子串的起始位置、结束位置;找不到返回 nil。

如果使用捕获(即对模式串用括号包裹起来),则一并返回匹配得到的字符串。

定义
string.find([字符串],[待查找字符串],[查找起始位置]=1,[禁用模式匹配]=false)

只有显式指定了 init 参数才能控制 plain 参数。

例子
x,y,z = string.find("1001 is a Robot", "Robot")
print(x,y,z)                                --> 11 15   nil
x,y,z = string.find("1001 is a Robot","1%d",1,true)
print(x,y,z)                                --> nil nil nil
x,y,z = string.find("1001 is a Robot","(%d+)",1,false)
print(x,y,z)                                --> 1   2   1001

string.match(s,pattern[,init])

string.find 类似,返回值不一样。string.match 查找字符串的子串,如果找到,返回子串;找不到返回 nil。

支持模式匹配。

定义

例子
x = string.match("1001 is a Robot","001")
print(x)                --> 001                             
x = string.match("1001 is a Robot","%d%d")
print(x)                --> 10      

返回一个迭代函数,该函数每执行一次,就返回下一个捕捉到的匹配(如果没有使用捕捉,就返回整个匹配结果)。

例子
for s in string.gmatch("I have a Dream.","%a+") do
    print(s)
end
--> I
--> have
--> a
--> Dream
t = {}
s = "name=tangyikejun, number=20250001"

-- 将捕获的两个子串分别作为键和值放到表t中
for k, v in string.gmatch(s, "(%w+)=(%w+)") do
    t[k] = v
end

-- 输出表t
for k,v in pairs(t) do
    print(k,v)
end

--> name    tangyikejun
--> number  20250001

string.format(formatstring,...)

返回格式化之后的字符串。

定义

例子
string.format("My name is %s", "tangyikejun")   --> My name is tangyikejun

返回字符串长度

string.lower(s)

返回小写字母的字符串

string.upper(s)

返回大写字母的字符串

string.rep(s,n)

对字符串进行重复

定义
string.rep([字符串],[重复次数])
例子
string.rep("Hello",4)   -- HelloHelloHelloHello

返回反转后的字符串。

string.sub(s,i[,j])

返回子字符串。

定义
string.sub([字符串],[开始字符下标],[结束字符下标]=-1)
例子
x = string.sub("tangyikejun",7)
print(x)                --> kejun
x = string.sub("tangyikejun",1,-6)
print(x)                --> tangyi

根据模式匹配对字符串中每一个匹配部分都做替换处理,返回替换后的字符串。

定义
string.gsub([字符串],[模式匹配],[替换字符],[最大替换次数] = 无限制)

repl 参数([替换字符])支持 字符串、表、函数。

如果 repl 是字符串,那么该字符串就是用于替换的字符串。同时支持 %n 转义符操作,n 的范围是 0-9。n 范围为 [1,9] 时表示第 n 个捕获的匹配字符串,%0 表示整个匹配的字符串,%% 表示替换为一个 %

如果 repl 是表,那么将捕获的第一个字符串作为键值(Key)进行查询(没有定义捕捉则以整个匹配的字符串为键值),查到的值作为替换的字符串。

如果 repl 是函数,那么每次匹配成功都会调用该函数,并以按序以所有捕捉作为参数传入函数。没有捕捉则以整个匹配的字符作为参数。

如果从表或函数得到是字符串或者是数字,就将其用于替换;如果得到的是 false 或 nil,那么匹配部分将不会发生变化。

例子

repl 为字符串

s = "Never say die."
x = string.gsub(s,"die","never")            --> Never say never.
x = string.gsub(s,"die","'%0'")             --> Never say 'die'.
x = string.gsub(s,"(%a+)%s%a+%s(%a+)","%2") --> die.

限制最大替换次数

s = "never say never."
x = string.gsub(s,"never","Never",1)    --> Never say never.

repl 是表

t = {name="Lua",version="5.1"}
x = string.gsub("$name-$version.tar.gz","$(%a+)",t) --> Lua-5.1.tar.gz

repl是函数

x = string.gsub("4+5 = $return 4+5$","%$(.-)%$",function(s)return loadstring(s)() end)  --> 4+5 = 9
x = string.gsub("23+45=$result", "((%d+)%+(%d+)=)%$%a+", function (s,a,b)
    sum = a+b
    return s..sum
end)    --> 23+45=68

注意:似乎只支持匿名函数。

从表或函数返回的是 false 或 nil

x = string.gsub("4+5 = $return 4+5$","%$(.-)%$",function(s)return nil end)  --> 4+5 = $return 4+5$
t = {name="Lua",version=false}
x = string.gsub("$name-$version.tar.gz","$(%a+)",t) --> Lua-$version.tar.gz

返回字符的 ASCII 码值。

定义
string.byte([字符串],[起始下标]=1,[结束下标]=[起始下标])
例子
x,y,z = string.byte("abc",2)    --> 98  nil nil
x,y,z = string.byte("abc",1,3)  --> 97  98  99

string.char(...)

根据传入的 ASCII 编码值([0-255])得到对应的字符,传入多少编码值就返回多长的字符串。

例子
x = string.char(98,99,100)  --> bcd

如果输入字符超限会编译报错。

string.dump(function)

返回函数的二进制表示(字符串形式),把这个返回值传给 loadingstring 可以获得函数的一份拷贝(传入的函数必须是没有上值的 Lua 函数)。

例子
function sum(a,b)
    return a + b
end

s = string.dump(sum)
x = loadstring(s)(4,4) -- 8

参考链接

BNF范式简介 (简要介绍 BNF)

Lua入门系列-果冻想(对Lua进行了较为全面的介绍)

Lua快速入门(介绍 Lua 中最为重要的几个概念,为 C/C++ 程序员准备)

Lua 5.1 中文手册(全面的 Lua5.1 中文手册)

Lua 5.3 中文手册(云风花了6天写的,天哪,我看都要看6天的节奏呀)

Lua迭代器和泛型for(介绍 Lua 迭代器的详细原理以及使用)

How do JavaScript closures work?——StackOverflow(详细介绍了 Javascript 中闭包的概念)

Lua模式匹配(参考了此文中对 %b 的使用)

LuaSocket(LuaSocket 官方手册)

Lua loadfile的用法, 与其他函数的比较(loadfile的介绍部分引用了此文)

Lua 的元表(对元表的描述比较有条理,通俗易懂,本文元表部分参考了此文)

设置函数环境——setfenv(解释了如何方便地设置函数的环境,以及为什么要那样设置)

lua5.1中的setfenv使用(介绍了该环境的设置在实际中的一个应用)