LUA教程(游戏UI制作)二

本文深入解析Lua语言的关键特性,包括操作符、优先级、表格构造、函数调用与定义、可视规则、错误处理等方面。同时介绍了metatable机制、垃圾回收、协同程序等高级特性,帮助读者全面掌握Lua语言。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.5.5 - 取长度操作符
取长度操作符写作一元操作 #。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)。
table t 的长度被定义成一个整数下标 n 。它满足 t[n] 不是 nil 而 t[n+1] 为 nil;此外,如果 t[1] 为 nil ,n 就可能是零。对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。如果数组有一个“空洞” (就是说,nil 值被夹在非空值之间),那么 #t 可能是任何一个是 nil 值的位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。
2.5.6 - 优先级
Lua 中操作符的优先级写在下表中,从低到高优先级排序:
     or
     and
     <     >     <=    >=    ~=    ==
     ..
     +     -
     *     /     %
     not   #     - (unary)
     ^
通常,你可以用括号来改变运算次序。连接操作符 ('..') 和幂操作 ('^') 是从右至左的。其它所有的操作都是从左至右。
2.5.7 - Table 构造
table 构造子是一个构造 table 的表达式。每次构造子被执行,都会构造出一个新的 table 。构造子可以被用来构造一个空的 table,也可以用来构造一个 table 并初始化其中的一些域。一般的构造子的语法如下
        tableconstructor ::= `{′ [fieldlist] `}′
        fieldlist ::= field {fieldsep field} [fieldsep]
        field ::= `[′ exp `]′ `=′ exp | Name `=′ exp | exp
        fieldsep ::= `,′ | `;′
每个形如 [exp1] = exp2 的域向 table 中增加新的一项,其键值为 exp1 而值为 exp2。形如 name = exp 的域等价于 ["name"] = exp。最后,形如 exp 的域等价于 = exp , 这里的 i 是一个从 1 开始不断增长的数字。这这个格式中的其它域不会破坏其记数。举个例子:
     a = { [f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45 }
等价于
     do
       local t = {}
       t[f(1)] = g
       t[1] = "x"         -- 1st exp
       t[2] = "y"         -- 2nd exp
       t.x = 1            -- t["x"] = 1
       t[3] = f(x)        -- 3rd exp
       t[30] = 23
       t[4] = 45          -- 4th exp
       a = t
     end
如果表单中最后一个域的形式是 exp ,而且其表达式是一个函数调用或者是一个可变参数,那么这个表达式所有的返回值将连续的进入列表(参见 §2.5.8)。为了避免这一点,你可以用括号把函数调用(或是可变参数)括起来(参见 §2.5)。
初始化域表可以在最后多一个分割符,这样设计可以方便由机器生成代码。
2.5.8 - 函数调用
Lua 中的函数调用的语法如下:
        functioncall ::= prefixexp args
函数调用时,第一步,prefixexp 和 args 先被求值。如果 prefixexp 的值的类型是 function,那么这个函数就被用给出的参数调用。否则 prefixexp 的元方法 "call" 就被调用,第一个参数就是 prefixexp 的值,跟下来的是原来的调用参数(参见 §2.8)。
这样的形式
        functioncall ::= prefixexp `:′ Name args
可以用来调用 "方法"。这是 Lua 支持的一种语法糖。像 v:name(args) 这个样子,被解释成 v.name(v,args),这里 v 只会被求值一次。
参数的语法如下:
        args ::= `(′ [explist1] `)′
        args ::= tableconstructor
        args ::= String
所有参数的表达式求值都在函数调用之前。这样的调用形式 f{fields} 是一种语法糖用于表示 f({fields});这里指参数列表是一个单一的新创建出来的列表。而这样的形式 f'string' (或是 f"string" 亦或是 f[[string]])也是一种语法糖,用于表示 f('string');这里指参数列表是一个单独的字符串。
因为表达式语法在 Lua 中比较自由,所以你不能在函数调用的 '(' 前换行。这个限制可以避免语言中的一些歧义。比如你这样写
     a = f
     (g).x(a)
Lua 将把它当作一个单一语句段, a = f(g).x(a) 。因此,如果你真的想作为成两个语句段,你必须在它们之间写上一个分号。如果你真的想调用 f,你必须从 (g) 前移去换行。
这样一种调用形式:return functioncall 将触发一个尾调用。 Lua 实现了适当的尾部调用(或是适当的尾递归):在尾调用中,被调用的函数重用调用它的函数的堆栈项。因此,对于程序执行的嵌套尾调用的层数是没有限制的。然而,尾调用将删除调用它的函数的任何调试信息。注意,尾调用只发生在特定的语法下,这时, return 只有单一函数调用作为参数;这种语法使得调用函数的结果可以精确返回。因此,下面这些例子都不是尾调用:
     return (f(x))        -- 返回值被调整为一个
     return 2 * f(x)
     return x, f(x)       -- 最加若干返回值
     f(x); return         -- 无返回值
     return x or f(x)     -- 返回值被调整为一个
2.5.9 - 函数定义
函数定义的语法如下:
        function ::= function funcbody
        funcbody ::= `(′ [parlist1] `)′ block end
另外定义了一些语法糖简化函数定义的写法:
        stat ::= function funcname funcbody
        stat ::= local function Name funcbody
        funcname ::= Name {`.′ Name} [`:′ Name]
这样的写法:
     function f () body end
被转换成
     f = function () body end
这样的写法:
     function t.a.b.c.f () body end
被转换成
     t.a.b.c.f = function () body end
这样的写法:
     local function f () body end
被转换成
     local f; f = function () body end
注意,并不是转换成
     local f = function () body end
(这个差别只在函数体内需要引用 f 时才有。)
一个函数定义是一个可执行的表达式,执行结果是一个类型为 function 的值。当 Lua 预编译一个 chunk 的时候, chunk 作为一个函数,整个函数体也就被预编译了。那么,无论何时 Lua 执行了函数定义,这个函数本身就被实例化了(或者说是关闭了)。这个函数的实例(或者说是 closure(闭包))是表达式的最终值。相同函数的不同实例有可能引用不同的外部局部变量,也可能拥有不同的环境表。
形参(函数定义需要的参数)是一些由实参(实际传入参数)的值初始化的局部变量:
        parlist1 ::= namelist [`,′ `...′] | `...′
当一个函数被调用,如果函数没有被定义为接收不定长参数,即在形参列表的末尾注明三个点 ('...'),那么实参列表就会被调整到形参列表的长度,变长参数函数不会调整实参列表;取而代之的是,它将把所有额外的参数放在一起通过变长参数表达式传递给函数,其写法依旧是三个点。这个表达式的值是一串实参值的列表,看起来就跟一个可以返回多个结果的函数一样。如果一个变长参数表达式放在另一个表达式中使用,或是放在另一串表达式的中间,那么它的返回值就会被调整为单个值。若这个表达式放在了一系列表达式的最后一个,就不会做调整了(除非用括号给括了起来)。
我们先做如下定义,然后再来看一个例子:
     function f(a, b) end
     function g(a, b, ...) end
     function r() return 1,2,3 end
下面看看实参到形参数以及可变长参数的映射关系:
     CALL            PARAMETERS
    
     f(3)             a=3, b=nil
     f(3, 4)          a=3, b=4
     f(3, 4, 5)       a=3, b=4
     f(r(), 10)       a=1, b=10
     f(r())           a=1, b=2
    
     g(3)             a=3, b=nil, ... -->  (nothing)
     g(3, 4)          a=3, b=4,   ... -->  (nothing)
     g(3, 4, 5, 8)    a=3, b=4,   ... -->  5  8
     g(5, r())        a=5, b=1,   ... -->  2  3
结果由 return 来返回(参见 §2.4.4)。如果执行到函数末尾依旧没有遇到任何 return 语句,函数就不会返回任何结果。
冒号语法可以用来定义方法,就是说,函数可以有一个隐式的形参 self。因此,如下写法:
     function t.a.b.c:f (params) body end
是这样一种写法的语法糖:
     t.a.b.c.f = function (self, params) body end
2.6 - 可视规则
Lua 是一个有词法作用范围的语言。变量的作用范围开始于声明它们之后的第一个语句段,结束于包含这个声明的最内层语句块的结束点。看下面这些例子:
     x = 10                -- 全局变量
     do                    -- 新的语句块
       local x = x         -- 新的一个 'x', 它的值现在是 10
       print(x)            --> 10
       x = x+1
       do                  -- 另一个语句块
         local x = x+1     -- 又一个 'x'
         print(x)          --> 12
       end
       print(x)            --> 11
     end
     print(x)              --> 10  (取到的是全局的那一个)
注意这里,类似 local x = x 这样的声明,新的 x 正在被声明,但是还没有进入它的作用范围,所以第二个 x 指向的是外面一层的变量。
因为有这样一个词法作用范围的规则,所以可以在函数内部自由的定义局部变量并使用它们。当一个局部变量被更内层的函数中使用的时候,它被内层函数称作 upvalue(上值),或是 外部局部变量。
注意,每次执行到一个 local 语句都会定义出一个新的局部变量。看看这样一个例子:
     a = {}
     local x = 20
     for i=1,10 do
       local y = 0
       a = function () y=y+1; return x+y end
     end
这个循环创建了十个 closure(这指十个匿名函数的实例)。这些 closure 中的每一个都使用了不同的 y 变量,而它们又共享了同一份 x。
2.7 - 错误处理
因为 Lua 是一个嵌入式的扩展语言,所有的 Lua 动作都是从宿主程序的 C 代码调用 Lua 库(参见 lua_pcall)中的一个函数开始的。在 Lua 编译或运行的任何时候发生了错误,控制权都会交还给 C ,而 C 可以来做一些恰当的措施(比如打印出一条错误信息)。
Lua 代码可以显式的调用 error 函数来产生一条错误。如果你需要在 Lua 中捕获发生的错误,你可以使用 pcall 函数。
2.8 - Metatable(元表)
Lua 中的每个值都可以用一个 metatable。这个 metatable 就是一个原始的 Lua table ,它用来定义原始值在特定操作下的行为。你可以通过在 metatable 中的特定域设一些值来改变拥有这个 metatable 的值的指定操作之行为。举例来说,当一个非数字的值作加法操作的时候, Lua 会检查它的 metatable 中 "__add" 域中的是否有一个函数。如果有这么一个函数的话,Lua 调用这个函数来执行一次加法。
我们叫 metatable 中的键名为 事件 (event) ,把其中的值叫作 元方法 (metamethod)。在上个例子中,事件是 "add" 而元方法就是那个执行加法操作的函数。
你可以通过 getmetatable 函数来查询到任何一个值的 metatable。
你可以通过 setmetatable 函数来替换掉 table 的 metatable 。你不能从 Lua 中改变其它任何类型的值的 metatable (使用 debug 库例外);要这样做的话必须使用 C API 。
每个 table 和 userdata 拥有独立的 metatable (当然多个 table 和 userdata 可以共享一个相同的表作它们的 metatable);其它所有类型的值,每种类型都分别共享唯一的一个 metatable。因此,所有的数字一起只有一个 metatable ,所有的字符串也是,等等。
一个 metatable 可以控制一个对象做数学运算操作、比较操作、连接操作、取长度操作、取下标操作时的行为, metatable 中还可以定义一个函数,让 userdata 作垃圾收集时调用它。对于这些操作,Lua 都将其关联上一个被称作事件的指定健。当 Lua 需要对一个值发起这些操作中的一个时,它会去检查值中 metatable 中是否有对应事件。如果有的话,键名对应的值(元方法)将控制 Lua 怎样做这个操作。
metatable 可以控制的操作已在下面列出来。每个操作都用相应的名字区分。每个操作的键名都是用操作名字加上两个下划线 '__' 前缀的字符串;举例来说,"add" 操作的键名就是字符串 "__add"。这些操作的语义用一个 Lua 函数来描述解释器如何执行更为恰当。
这里展示的用 Lua 写的代码仅作解说用;实际的行为已经硬编码在解释器中,其执行效率要远高于这些模拟代码。这些用于描述的的代码中用到的函数( rawget , tonumber ,等等。)都可以在 §5.1 中找到。特别注意,我们使用这样一个表达式来从给定对象中提取元方法
     metatable(obj)[event]
这个应该被解读作
     rawget(getmetatable(obj) or {}, event)
这就是说,访问一个元方法不再会触发任何的元方法,而且访问一个没有 metatable 的对象也不会失败(而只是简单返回 nil)。
?        "add": + 操作。
下面这个 getbinhandler 函数定义了 Lua 怎样选择一个处理器来作二元操作。首先,Lua 尝试第一个操作数。如果这个东西的类型没有定义这个操作的处理器,然后 Lua 会尝试第二个操作数。
     function getbinhandler (op1, op2, event)
       return metatable(op1)[event] or metatable(op2)[event]
     end
通过这个函数, op1 + op2 的行为就是
     function add_event (op1, op2)
       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
?        "sub": - 操作。 其行为类似于 "add" 操作。
?        "mul": * 操作。 其行为类似于 "add" 操作。
?        "div": / 操作。 其行为类似于 "add" 操作。
?        "mod": % 操作。 其行为类似于 "add" 操作,它的原生操作是这样的 o1 - floor(o1/o2)*o2
?        "pow": ^ (幂)操作。 其行为类似于 "add" 操作,它的原生操作是调用 pow 函数(通过 C math 库)。
?        "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
?        "concat": .. (连接)操作,
?             function concat_event (op1, op2)
?               if (type(op1) == "string" or type(op1) == "number") and
?                  (type(op2) == "string" or type(op2) == "number") then
?                 return op1 .. op2  -- 原生字符串连接
?               else
?                 local h = getbinhandler(op1, op2, "__concat")
?                 if h then
?                   return h(op1, op2)
?                 else
?                   error(???)
?                 end
?               end
?             end
?        "len": # 操作。
?             function len_event (op)
?               if type(op) == "string" then
?                 return strlen(op)         -- 原生的取字符串长度
?               elseif type(op) == "table" then
?                 return #op                -- 原生的取 table 长度
?               else
?                 local h = metatable(op).__len
?                 if h then
?                   -- 调用操作数的处理器
?                   return h(op)
?                 else  -- 没有处理器:缺省行为
?                   error(???)
?                 end
?               end
?             end
关于 table 的长度参见 §2.5.5 。
?        "eq": == 操作。 函数 getcomphandler 定义了 Lua 怎样选择一个处理器来作比较操作。元方法仅仅在参于比较的两个对象类型相同且有对应操作相同的元方法时才起效。
?             function getcomphandler (op1, op2, event)
?               if type(op1) ~= type(op2) then return nil end
?               local mm1 = metatable(op1)[event]
?               local mm2 = metatable(op2)[event]
?               if mm1 == mm2 then return mm1 else return nil end
?             end
"eq" 事件按如下方式定义:
     function eq_event (op1, op2)
       if type(op1) ~= type(op2) then  -- 不同的类型?
         return false   -- 不同的对象
       end
       if op1 == op2 then   -- 原生的相等比较结果?
         return true   -- 对象相等
       end
       -- 尝试使用元方法
       local h = getcomphandler(op1, op2, "__eq")
       if h then
         return h(op1, op2)
       else
         return false
       end
     end
a ~= b 等价于 not (a == b) 。
?        "lt": < 操作。
?             function lt_event (op1, op2)
?               if type(op1) == "number" and type(op2) == "number" then
?                 return op1 < op2   -- 数字比较
?               elseif type(op1) == "string" and type(op2) == "string" then
?                 return op1 < op2   -- 字符串按逐字符比较
?               else
?                 local h = getcomphandler(op1, op2, "__lt")
?                 if h then
?                   return h(op1, op2)
?                 else
?                   error(???);
?                 end
?               end
?             end
a > b 等价于 b < a.
?        "le": <= 操作。
?             function le_event (op1, op2)
?               if type(op1) == "number" and type(op2) == "number" then
?                 return op1 <= op2   -- 数字比较
?               elseif type(op1) == "string" and type(op2) == "string" then
?                 return op1 <= op2   -- 字符串按逐字符比较
?               else
?                 local h = getcomphandler(op1, op2, "__le")
?                 if h then
?                   return h(op1, op2)
?                 else
?                   h = getcomphandler(op1, op2, "__lt")
?                   if h then
?                     return not h(op2, op1)
?                   else
?                     error(???);
?                   end
?                 end
?               end
?             end
a >= b 等价于 b <= a 。注意,如果元方法 "le" 没有提供,Lua 就尝试 "lt" ,它假定 a <= b 等价于 not (b < a) 。
?        "index": 取下标操作用于访问 table[key] 。
?             function gettable_event (table, key)
?               local h
?               if type(table) == "table" then
?                 local v = rawget(table, key)
?                 if v ~= nil then return v end
?                 h = metatable(table).__index
?                 if h == nil then return nil end
?               else
?                 h = metatable(table).__index
?                 if h == nil then
?                   error(???);
?                 end
?               end
?               if type(h) == "function" then
?                 return h(table, key)      -- 调用处理器
?               else return h[key]          -- 或是重复上述操作
?               end
?             end
?        "newindex": 赋值给指定下标 table[key] = value 。
?             function settable_event (table, key, value)
?               local h
?               if type(table) == "table" then
?                 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
?        "call": 当 Lua 调用一个值时调用。
?             function function_event (func, ...)
?               if type(func) == "function" then
?                 return func(...)   -- 原生的调用
?               else
?                 local h = metatable(func).__call
?                 if h then
?                   return h(func, ...)
?                 else
?                   error(???)
?                 end
?               end
?             end
2.9 - 环境
类型为 thread ,function ,以及 userdata 的对象,除了 metatable 外还可以用另外一个与之关联的被称作它们的环境的一个表,像 metatable 一样,环境也是一个常规的 table ,多个对象可以共享同一个环境。
userdata 的环境在 Lua 中没有意义。这个东西只是为了在程序员想把一个表关联到一个 userdata 上时提供便利。
关联在线程上的环境被称作全局环境。全局环境被用作它其中的线程以及线程创建的非嵌套函数(通过 loadfile , loadstring 或是 load )的缺省环境。而且它可以被 C 代码直接访问(参见 §3.3)。
关联在 C 函数上的环境可以直接被 C 代码访问(参见 §3.3)。它们会作为这个 C 函数中创建的其它函数的缺省环境。
关联在 Lua 函数上的环境用来接管在函数内对全局变量(参见 §2.3)的所有访问。它们也会作为这个函数内创建的其它函数的缺省环境。
你可以通过调用 setfenv 来改变一个 Lua 函数或是正在运行中的线程的环境。而想操控其它对象(userdata、C 函数、其它线程)的环境的话,就必须使用 C API 。
2.10 - 垃圾收集
Lua 提供了一个自动的内存管理。这就是说你不需要关心创建新对象的分配内存操作,也不需要在这些对象不再需要时的主动释放内存。 Lua 通过运行一个垃圾收集器来自动管理内存,以此一遍又一遍的回收死掉的对象(这是指 Lua 中不再访问的到的对象)占用的内存。 Lua 中所有对象都被自动管理,包括: table, userdata、 函数、线程、和字符串。
Lua 实现了一个增量标记清除的收集器。它用两个数字来控制垃圾收集周期: garbage-collector pause 和 garbage-collector step multiplier 。
garbage-collector pause 控制了收集器在开始一个新的收集周期之前要等待多久。随着数字的增大就导致收集器工作工作的不那么主动。小于 1 的值意味着收集器在新的周期开始时不再等待。当值为 2 的时候意味着在总使用内存数量达到原来的两倍时再开启新的周期。
step multiplier 控制了收集器相对内存分配的速度。更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。小于 1 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。缺省值为 2 ,这意味着收集器将以内存分配器的两倍速运行。
你可以通过在 C 中调用 lua_gc 或是在 Lua 中调用 collectgarbage 来改变这些数字。两者都接受百分比数值(因此传入参数 100 意味着实际值 1 )。通过这些函数,你也可以直接控制收集器(例如,停止或是重启)。
2.10.1 - 垃圾收集的元方法
使用 C API ,你可以给 userdata (参见 §2.8)设置一个垃圾收集的元方法。这个元方法也被称为结束子。结束子允许你用额外的资源管理器和 Lua 的内存管理器协同工作(比如关闭文件、网络连接、或是数据库连接,也可以说释放你自己的内存)。
一个 userdata 可被回收,若它的 metatable 中有 __gc 这个域 ,垃圾收集器就不立即收回它。取而代之的是,Lua 把它们放到一个列表中。最收集结束后,Lua 针对列表中的每个 userdata 执行了下面这个函数的等价操作:
     function gc_event (udata)
       local h = metatable(udata).__gc
       if h then
         h(udata)
       end
     end
在每个垃圾收集周期的结尾,每个在当前周期被收集起来的 userdata 的结束子会以它们构造时的逆序依次调用。也就是说,收集列表中,最后一个在程序中被创建的 userdata 的结束子会被第一个调用。
2.10.2 - Weak Table(弱表)
weak table 是一个这样的 table,它其中的元素都被弱引用。弱引用将被垃圾收集器忽略掉,换句话说,如果对一个对象的引用只有弱引用,垃圾收集器将回收这个对象。
weak table 的键和值都可以是 weak 的。如果一个 table 只有键是 weak 的,那么将运行收集器回收它们的键,但是会阻止回收器回收对应的值。而一个 table 的键和值都是 weak 时,就即允许收集器回收键又允许收回值。任何情况下,如果键和值中任一个被回收了,整个键值对就会从 table 中拿掉。 table 的 weak 特性可以通过在它的 metatable 中设置 __mode 域来改变。如果 __mode 域中是一个包含有字符 'k' 的字符串时, table 的键就是 weak 的。如果 __mode 域中是一个包含有字符 'v' 的字符串时, table 的值就是 weak 的。
在你把一个 table 当作一个 metatable 使用之后,就不能再修改 __mode 域的值。否则,受这个 metatable 控制的 table 的 weak 行为就成了未定义的。
2.11 - Coroutine (协同例程)
Lua 支持 coroutine ,这个东西也被称为协同式多线程 (collaborative multithreading) 。 Lua 为每个 coroutine 提供一个独立的运行线路。然而和多线程系统中的线程不同,coroutine 只在显式的调用了 yield 函数时才会挂起。
创建一个 coroutine 需要调用一次 coroutine.create 。它只接收单个参数,这个参数是 coroutine 的主函数。 create 函数仅仅创建一个新的 coroutine 然后返回它的控制器(一个类型为 thread 的对象);它并不会启动 coroutine 的运行。
当你第一次调用 coroutine.resume 时,所需传入的第一个参数就是 coroutine.create 的返回值。这时,coroutine 从主函数的第一行开始运行。接下来传入 coroutine.resume 的参数将被传进 coroutine 的主函数。在 coroutine 开始运行后,它讲运行到自身终止或是遇到一个 yields 。
coroutine 可以通过两种方式来终止运行:一种是正常退出,指它的主函数返回(最后一条指令被运行后,无论有没有显式的返回指令); 另一种是非正常退出,它发生在未保护的错误发生的时候。第一种情况中, coroutine.resume 返回 true ,接下来会跟着 coroutine 主函数的一系列返回值。第二种发生错误的情况下, coroutine.resume 返回 false ,紧接着是一条错误信息。
coroutine 中切换出去,可以调用 coroutine.yield。当 coroutine 切出,与之配合的 coroutine.resume 就立即返回,甚至在 yield 发生在内层的函数调用中也可以(就是说,这不限于发生在主函数中,也可以是主函数直接或间接调用的某个函数里)。在 yield 的情况下,coroutine.resume 也是返回 true,紧跟着那些被传入 coroutine.yield 的参数。等到下次你在继续同样的 coroutine ,将从调用 yield 的断点处运行下去。断点处 yield 的返回值将是 coroutine.resume 传入的参数。
类似 coroutine.create , coroutine.wrap 这个函数也将创建一个 coroutine ,但是它并不返回 coroutine 本身,而是返回一个函数取而代之。一旦你调用这个返回函数,就会切入 coroutine 运行。所有传入这个函数的参数等同于传入 coroutine.resume 的参数。 coroutine.wrap 会返回所有应该由除第一个(错误代码的那个布尔量)之外的由 coroutine.resume 返回的值。和 coroutine.resume 不同, coroutine.wrap 不捕获任何错误;所有的错误都应该由调用者自己传递。
看下面这段代码展示的一个例子:
     function foo (a)
       print("foo", a)
       return coroutine.yield(2*a)
     end
    
     co = coroutine.create(function (a,b)
           print("co-body", a, b)
           local r = foo(a+1)
           print("co-body", r)
           local r, s = coroutine.yield(a+b, a-b)
           print("co-body", r, s)
           return b, "end"
     end)
           
     print("main", coroutine.resume(co, 1, 10))
     print("main", coroutine.resume(co, "r"))
     print("main", coroutine.resume(co, "x", "y"))
     print("main", coroutine.resume(co, "x", "y"))
当你运行它,将得到如下输出结果:
     co-body 1       10
     foo     2
    
     main    true    4
     co-body r
     main    true    11      -9
     co-body x       y
     main    true    10      end
     main    false   cannot resume dead coroutine
3 - 程序接口(API)
这个部分描述了 Lua 的 C API ,也就是宿主程序跟 Lua 通讯用的一组 C 函数。所有的 API 函数按相关的类型以及常量都声明在头文件 lua.h 中。
虽然我们说的是“函数”,但一部分简单的 API 是以宏的形式提供的。所有的这些宏都只使用它们的参数一次(除了第一个参数,也就是 lua 状态机),因此你不需担心这些宏的展开会引起一些副作用。
在所有的 C 库中,Lua API 函数都不去检查参数的有效性和坚固性。然而,你可以在编译 Lua 时加上打开一个宏开关来开启 luaconf.h 文件中的宏 luai_apicheck 以改变这个行为。
3.1 - 堆栈
Lua 使用一个虚拟栈来和 C 传递值。栈上的的每个元素都是一个 Lua 值(nil,数字,字符串,等等)。
无论何时 Lua 调用 C,被调用的函数都得到一个新的栈,这个栈独立于 C 函数本身的堆栈,也独立于以前的栈。(译注:在 C 函数里,用 Lua API 不能访问到 Lua 状态机中本次调用之外的堆栈中的数据)它里面包含了 Lua 传递给 C 函数的所有参数,而 C 函数则把要返回的结果也放入堆栈以返回给调用者(参见 lua_CFunction)。
方便起见,所有针对栈的 API 查询操作都不严格遵循栈的操作规则。而是可以用一个索引来指向栈上的任何元素:正的索引指的是栈上的绝对位置(从一开始);负的索引则指从栈顶开始的偏移量。更详细的说明一下,如果堆栈有 n 个元素,那么索引 1 表示第一个元素(也就是最先被压入堆栈的元素)而索引 n 则指最后一个元素;索引 -1 也是指最后一个元素(即栈顶的元素),索引 -n 是指第一个元素。如果索引在 1 到栈顶之间(也就是,1 ≤ abs(index) ≤ top)我们就说这是个有效的索引。
3.2 - 堆栈尺寸
当你使用 Lua API 时,就有责任保证其坚固性。特别需要注意的是,你有责任控制不要堆栈溢出。你可以使用 lua_checkstack 这个函数来扩大可用堆栈的尺寸。
无论何时 Lua 调用 C ,它都只保证 LUA_MINSTACK 这么多的堆栈空间可以使用。 LUA_MINSTACK 一般被定义为 20 ,因此,只要你不是不断的把数据压栈,通常你不用关心堆栈大小。
所有的查询函数都可以接收一个索引,只要这个索引是任何栈提供的空间中的值。栈能提供的最大空间是通过 lua_checkstack 来设置的。这些索引被称作可接受的索引,通常我们把它定义为:
     (index < 0 && abs(index) <= top) ||
     (index > 0 && index <= stackspace)
注意,0 永远都不是一个可接受的索引。(译注:下文中凡提到的索引,没有特别注明的话,都指可接受的索引。)
3.3 - 伪索引
除了特别声明外,任何一个函数都可以接受另一种有效的索引,它们被称作“伪索引”。这个可以帮助 C 代码访问一些并不在栈上的 Lua 值。伪索引被用来访问线程的环境,函数的环境,注册表,还有 C 函数的 upvalue (参见 §3.4)。
线程的环境(也就是全局变量放的地方)通常在伪索引 LUA_GLOBALSINDEX 处。正在运行的 C 函数的环境则放在伪索引 LUA_ENVIRONINDEX 之处。
你可以用常规的 table 操作来访问和改变全局变量的值,只需要指定环境表的位置。举例而言,要访问全局变量的值,这样做:
     lua_getfield(L, LUA_GLOBALSINDEX, varname);
3.4 - C Closure
当 C 函数被创建出来,我们有可能会把一些值关联在一起,也就是创建一个 C closure ;这些被关联起来的值被叫做 upvalue ,它们可以在函数被调用的时候访问的到。(参见 lua_pushcclosure)。
无论何时去调用 C 函数,函数的 upvalue 都被放在指定的伪索引处。我们可以用 lua_upvalueindex 这个宏来生成这些伪索引。第一个关联到函数的值放在 lua_upvalueindex(1) 位置处,依次类推。任何情况下都可以用 lua_upvalueindex(n) 产生一个 upvalue 的索引,即使 n 大于实际的 upvalue 数量也可以。它都可以产生一个可接受但不一定有效的索引。
3.5 - 注册表
Lua 提供了一个注册表,这是一个预定义出来的表,可以用来保存任何 C 代码想保存的 Lua 值。这个表可以用伪索引 LUA_REGISTRYINDEX 来定位。任何 C 库都可以在这张表里保存数据,为了防止冲突,你需要特别小心的选择键名。一般的用法是,你可以用一个包含你的库名的字符串做为键名,或者可以取你自己 C 代码中的一个地址,以 light userdata 的形式做键。
注册表里的整数健被用于补充库中实现的引用系统的工作,一般说来不要把它们用于别的用途。
3.6 - C 中的错误处理
在内部实现中,Lua 使用了 C 的 longjmp 机制来处理错误。(如果你使用 C++ 的话,也可以选择换用异常;参见 luaconf.h 文件。)当 Lua 碰到任何错误(比如内存分配错误、类型错误、语法错误、还有一些运行时错误)它都会产生一个错误出去;也就是调用一个 long jump 。在保护环境下,Lua 使用 setjmp 来设置一个恢复点;任何发生的错误都会激活最近的一个恢复点。
几乎所有的 API 函数都可能产生错误,例如内存分配错误。但下面的一些函数运行在保护环境中(也就是说它们创建了一个保护环境再在其中运行),因此它们不会产生错误出来: lua_newstate, lua_close, lua_load, lua_pcall, and lua_cpcall。
在 C 函数里,你也可以通过调用 lua_error 产生一个错误。
3.7 - 函数和类型
在这里我们按字母次序列出了所有 C API 中的函数和类型。
________________________________________
lua_Alloc
typedef void * (*lua_Alloc) (void *ud,
                             void *ptr,
                             size_t osize,
                             size_t nsize);
Lua 状态机中使用的内存分配器函数的类型。内存分配函数必须提供一个功能类似于 realloc 但又不完全相同的函数。它的参数有 ud ,一个由 lua_newstate 传给它的指针; ptr ,一个指向已分配出来或是将被重新分配或是要释放的内存块指针; osize ,内存块原来的尺寸; nsize ,新内存块的尺寸。如果且只有 osize 是零时,ptr 为 NULL 。当 nsize 是零,分配器必须返回 NULL;如果 osize 不是零,分配器应当释放掉 ptr 指向的内存块。当 nsize 不是零,若分配器不能满足请求时,分配器返回 NULL 。当 nsize 不是零而 osize 是零时,分配器应该和 malloc 有相同的行为。当 nsize 和 osize 都不是零时,分配器则应和 realloc 保持一样的行为。 Lua 假设分配器在 osize >= nsize 时永远不会失败。
这里有一个简单的分配器函数的实现。这个实现被放在补充库中,由 luaL_newstate 提供。
     static void *l_alloc (void *ud, void *ptr, size_t osize,
                                                size_t nsize) {
       (void)ud;  (void)osize;  /* not used */
       if (nsize == 0) {
         free(ptr);
         return NULL;
       }
       else
         return realloc(ptr, nsize);
     }
这段代码假设 free(NULL) 啥也不影响,而且 realloc(NULL, size) 等价于 malloc(size)。这两点是 ANSI C 保证的行为。
________________________________________
lua_atpanic
lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf);
设置一个新的 panic (恐慌) 函数,并返回前一个。
如果在保护环境之外发生了任何错误, Lua 就会调用一个 panic 函数,接着调用 exit(EXIT_FAILURE),这样就开始退出宿主程序。你的 panic 函数可以永远不返回(例如作一次长跳转)来避免程序退出。
panic 函数可以从栈顶取到出错信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值