转载

Lua 学习笔记(上)

1 简介

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

2 语法

采用基于 BNF 的语法规则。

2.1 语法约定

Lua 对大小写敏感。

2.1.1 保留关键字

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

and elseif function

in nil local not or

repeat then until

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

2.1.2 操作符

C 语言中没有的操作符:

^  ~=  //  -- 向下取整 

Lua 中没有的操作符:

+= -= 

2.1.3 字符串定义

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

字符表示

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

其他转义符表示

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

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

采用长括号:长括号内的所有内容都作为普通字符处理。

[[]]        -- 0级长括号 [==[]==]    -- 2级长括号 

2.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 函数控制数字向字符串的转换。

2.3 变量

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

函数外的变量默认为全局变量,除非用 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 语句 | statement

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

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

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 表达式

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 相同。

2.5.2 比较操作符

比较操作的结果是 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

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

2.5.4 连接符

..

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

2.5.5 取长度操作符

#

对于字符串,长度为字符串的字符个数。

对于表,通过寻找满足t[n] 不是 nil 而 t[n+1] 为 nil 的下标 n 作为表的长度。

~~对于其他类型呢?~~

例子

-- 字符串取长 print(#"abc/0")                         --> 4 -- 表取长 print(#{[1]=1,[2]=2,[3]=3,x=5,y=6})     --> 3 print(#{[1]=1,[2]=nil,[3]=3,x=5,y=6})   --> 1 

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 开始
  • 如果表中有相同的键,那么以靠后的那个值作为键对应的值

上面这两条的存在使得上面的例子中 c 1 的输出值为 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  

2.5.8 函数定义

函数是一个表达式,其值为 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 参数。这种形式是对 方法 的模拟

2.5.9 函数调用

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.6 可视规则

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

2.7 错误处理

[待补充]

2.8 元表 | Metatable

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

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

2.8.1 事件名与元方法

仅表(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 

2.8.3 事件的具体介绍

事先提醒 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

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 垃圾回收

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 中的函数和类型。

余下部分见Lua 学习笔记(下)

参考链接

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使用 (介绍了该环境的设置在实际中的一个应用)

正文到此结束
Loading...