【Lua 基础学习】
Lua 基础学习
文章目录
- Lua 基础学习
- Lua绑定
- Lua基础知识
- Lua 循环
- Lua 函数
- Lua 运算符
- 编译、执行和错误
- 模块与包
- Lua 元表(Metatable)
- Lua 面向对象编程
- 继承
- 多重继承
- 私有性
- 环境
- 全局变量的声明
- 非全局环境
- 使用`_ENV`
- 环境和模块
- `_ENV`和`load`
- 垃圾收集
- 弱引用表
- 记忆函数(Memorize Function)
- 回顾具有默认值的表
- 瞬表(Ephemeron Table)
- 析构器(Finalizer)
- 垃圾收集器
- Lua 协程(coroutine)
- 反射(Reflection)
- 自省机制(Introspective Facility)
- 钩子(Hook)
- 沙盒(Sandbox)
- C 语言 API 总览
Lua绑定
Lua 使用虚拟栈与 C 进行数据通信。
- 栈上的每个元素都是一个Lua值,nil 数字 字符串等
- 每次Lua调用C函数都得到一个新的栈,这个栈独立于 C 函数本身,也独立于之前的 Lua 栈。
- 栈中包括了 Lua 传递给 C 的所有参数,C 函数则需要把返回结果放入这个栈以返回给调用者
- C 可以操作栈的任何位置,Lua 每次只能操作栈顶部
- C 自己管理内存,Lua 自动垃圾回收。虚拟机知道在栈里的数据是否有被外部宿主程序使用,从而决定是否GC。
元方法**__gc
**,当GCObject
被回收时,触发__gc
元方法执行。需要注意:给对象设置一个没有__gc
域的元表,之后再给元表加上这个域,那么这个对象是没有被标记成需要触发__gc
元方法的。
存储C/C++
对象的两种方式:
lightuserdata
: 对应LUA_TLIGHTUSERDATA
轻量用户数据。是一个void*
指针,需要在 C 语言层创建对象。它没有独立的元表,不会被收集,有 C 语言层负责管理。userdata
:对应LUA_TUSERDATA
,属于 Lua 层的 GC 对象,Lua 会将其回收,可以绑定元表。
struct Player {int m_u64Ruid;
};static int NewPlayer(lua_State *L) {// lua_Integer luaL_checkinteger (lua_State *L, int arg); 检查第arg个参数是否是一个整数,或者能否转化为一个整数。int u64Ruid = luaL_checkinteger(L, 1);// void *lua_newuserdata (lua_State *L, size_t size); 这个函数分配一块指定大小的内存块, 把内存块地址作为一个完全用户数据压栈, 并返回这个地址。 宿主程序可以随意使用这块内存。Player *pPlayer = (Player *)lua_newuserdata(L, sizeof(Player));pPlayer->m_u64Ruid = u64Ruid;// 将注册表中对应的元表压栈,如果没有对应的元表,则将nil压栈luaL_getmetatable(L, "LuaTest.Player");// 将一张表出栈,并将其设为给定索引位置处值的元表// 因为lua_newuserdata把新结构体的地址压栈了,luaL_getmetatable将Player的元表也入栈了,这里其实是 将新创建的Player对象的元表设置为"LuaTest.Player"的值lua_setmetatable(L, -2);reutrn 1;
}static int SetRuid(lua_State *L)
{// void *luaL_checkudata (lua_State *L, int arg, const char *tname);检查函数的第 arg 个参数是否是一个类型为 tname 的用户数据。它会返回该用户数据的地址 。Player* pPlayer = (Player*) luaL_checkudata(L, 1, "LuaTest.Player");int u64Ruid = luaL_checkinteger(L, 2);pPlayer->m_u64Ruid = u64Ruid;return 0;
}
static int GetRuid(lua_State* L)
{Player* pPlayer = (Player*) luaL_checkudata(L, 1, "LuaTest.Player");lua_pushinteger(L, pPlayer->m_u64Ruid);return 1;
}static const struct luaL_Reg mylib_f[] = {{"NewPlayer", NewPlayer},{NULL, NULL}
};static const struct luaL_Reg mylib_m[] = {{"SetRuid", SetRuid},{"GetRuid", GetRuid},{NULL, NULL}
};extern "C" int luaopen_mylib(lua_State *L) {// int luaL_newmetatable (lua_State *L, const char *tname); luaL_newmetatable 创建一个新表,并加入__name = tname 键值对,这个表是为了用作其他表的元表的,新创建的表会位于栈顶。并将[tname] = new table 添加到注册表中。// MetaTable = {} -- 创建一个空表luaL_newmetatable(L, "LuaTest.Player");// void lua_pushvalue (lua_State *L, int index); 将指定索引index处的元素作为一个副本压栈// void lua_setfield (lua_State *L, int index, const char *k); 等价于 t[k] = v,t为给定索引处的值,在这里即为压入的MetaTable的副本,v是栈顶的值。同时,这个函数将把 v 弹出栈。// MetaTable.__index = MetaTablelua_pushvalue(L, -1);lua_setfield(L, -2, "__index");// void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l);// 这是一个 Lua 5.1 及之前的函数,当libname = NULL时,它简单的注册列表l中的所有函数到栈顶的表,在这里栈顶的表为MetaTable。当libname不为NULL时,它创建一个新的表,将他设置为libname的值,也就是说,通过libname可以索引到这个表,同时设置package.loaded[libname],并且列表l中的所有函数注册到这个新表中。// MetaTable.SetRuid = SetRuid; MetaTable.GetRuid = GetRuidluaL_register(L, NULL, mylib_m);// Player.NewPlayer = NewPlayerluaL_register(L, "Player", mylib_f);return 1;
}
Lua基础知识
最好不要使用下划线加大写字母的标识符,因为Lua的保留字也是这样的(_E _G)
一般约定,以下划线开头连接一串大写字母的名字(_VERSION)被用于Lua内部全局变量。
Lua 基本数据类型:nil boolean number string userdata function thread table
userdata:表示任意存储在变量中的C数据结构
Lua 中 function 为第一类值(first-class value)
#
输出的值是字符串所占的字节数
print(#"hello world")
11
print(#"你好,世界")
15
table 的索引,不能是数字和字符串,只能是普通变量
> tbl ={100 = "100"}
stdin:1: '}' expected near '='
> tbl ={"100" = "100"}
stdin:1: '}' expected near '='
> tbl = {a = "aa"}
> print(tbl["a"])
aa
> print(tbl.a)
aa
Lua 中的变量是全局变量,哪怕是语句块或是函数里,除非用local显式声明为局部变量,函数的参数列表的变量是局部变量。应该尽可能使用局部变量,有两个好处:
- 避免命名冲突
- 访问局部变量的速度比全局变量快
Lua 循环
可以使用类似于下面的语句实现continue
功能
for i = 10, 1, -1 dorepeatif i == 5 thenprint("continue code here")breakendprint(i, "loop code here")until true -- 直到指定条件为真时结束循环,这里true意味着,repeat循环将只执行一次
end
Lua 函数
多返回值函数在赋值时,仅仅只有放在所有逗号之后的那个函数会返回值展开。
function add()return 1,0
endlocal b,c,d,e = add(),add()print(b) -- 1
print(c) -- 1
print(d) -- 0
print(e) -- nil
Lua 运算符
运算符优先级, 认真学习运算符左右结合的问题,比如2^3^3等于多少
^ 右结合,2^3^3 = 2^27
not -
* / %
+ -
..
< > <= >= ~= ==
and
or
在lua中实现三目运算condition ? result1 : result2
, Lua中的三目运算符
a and b or c -- 这种形式当b=false时,有缺陷
(a and {b} or {c})[1]
编译、执行和错误
Lua为解释型语言,但Lua总是在运行代码前先预编译(precompile)源码为中间代码。
编译(compilation)阶段的存在听上去超出了解释型语言的范畴,但解释型语言的区别并不在于源码是否被编译,而是在于是否有能力(且轻易地)执行动态生成的代码。
dofile
从文件中加载并运行Lua代码loadfile
只编译文件中的代码,返回一个函数。load
从一个字符串或者函数中读取代码段,返回一个函数
i = 32
local i = 0
f = load("i = i + 1; print(i)")
g = function()i = i + 1; print(i)
end
f() -- 31
g() -- 1
函数g
像预期的一样操作局部变量,但是f
却是操作的全局变量
Lua语言中的函数定义是在运行时而不是编译时发生的一种赋值操作。
-- 文件"foo.lua"
function foo(x)print(x)
end-- 文件"main.lua"
f = loadfile("foo.lua") -- 加载代码,编译代码
print(foo) -- nil
f() -- 运行代码
print(foo) -- function: 07ea730
foo("ok") -- ok
pcall
(protected call)以一种保护模式来调用它的第1个参数,以便捕获该函数执行中的错误。当没有发生错误,pcall
返回true
和被调用函数的所有返回值;当发生错误时,返回false
和错误对象。
local status, err = pcall(function() error({code = 121}) end)
print(err.code) -- 121
error(message [, level])
level=1(默认):为调用error的位置(文件+行号)
level=2:指出调用error所在函数的函数
level=0:不添加错误位置信息
模块与包
如果一个模块名中包含连字符,那么函数require
就会用连字符之前的内容来创建luaopen_*
函数的名称。比如加载mod-v1
,那么require
会认为该模块的加载函数应该是luaopen_mod
,用这个加载函数来加载文件名为mod-v1
的文件,这样就实现了用一个加载函数加载同模块的两个不同版本。
Lua 语言中编写模块的基本方法:创建一个表,并将所有需要导出的函数放入其中,最后返回这个表。
除了发现由于失误而定义的全局变量时有一个技巧外,笔者(Lua程序设计作者)在编写模块时用的都是基本功能。
模块名中的点.
具有特殊含义。Lua支持具有层次结构的模块名,通过点来分割名称中的层次。一个包(package)是一棵由模块组成的完整的树,它是Lua语言中用于发行程序的单位。
Lua 元表(Metatable)
Lua语言中,我们只能为表设置元表;如果要为其他类型的值设置元表,则必须通过C代码或调试库完成(该限制存在的主要原因是为了防止过度使用对某种类型的所有值生效的元表。Lua语言老版本中的经验表明,这样的全局设置经常导致不可重用的代码)。字符串标准库为所有的字符串都设置了同一个元表,而其他类型在默认情况中都没有元表。
元表的查询会一直递归查下去,如果__index
包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。
Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio
> one = {foo = "one", foo1 = "one", foo2 = "one"}
> two = setmetatable({foo1 = "two", foo2 = "two"}, {__index = one})
> t = setmetatable({foo2 = "t"}, {__index = two})
> t.foo, t.foo1, t.foo2, t.foo3
one two t nil
Lua 查找一个表元素时的规则,三个步骤
- 在表中查找,如果找到,返回该元素,找不到继续
- 判断该表是否有元表,如果没有元表,返回nil,有元表则继续
- 判断元表有没有
__index
方法,如果__index
方法为nil,则返回nil;如果__index
方法是一个表,则重复1、2、3;如果__index
方法是一个函数,则用表和键作为参数调用该函数并返回该函数的返回值。
__index
如果是一个函数,在这个函数中仍用相同键检索此表,那么会造成无线递归
> t = setmetatable({}, {__index = function (t, k) return t[k] end})
> t
table: 0x156304820
> t[1]
stdin:1: C stack overflow
stack traceback:stdin:1: in metamethod 'index'stdin:1: in metamethod 'index'... (skipping 193 levels)stdin:1: in metamethod 'index'stdin:1: in main chunk[C]: in ?
rawget(t, i)
对表进行一次原始访问,即不考虑元表的情况下访问表 t,但是进行一次原始访问并不会加快代码执行(一次函数调用的开销就会抹杀用户所做的这些努力)。
__newindex
元方法用来对表进行更新,__index
则用来对表进行访问
__newindex
的两个规则
- 如果
__newindex
是一个函数,则在给table中不存在的字段赋值时,会调用这个函数,并且赋值不成功- 如果
__newindex
是一个table,则在给table中不存在的字段赋值时,会直接给__newindex
的table赋值
上面的第一个规则,__newindex
中如果是一个函数,但是在函数中仍然对原table赋值,会导致无线递归的发生,如下所示
> t = setmetatable({foo = "foo"}, {__newindex = function (t, k, v) t[k] = v end })
> t["foo"], t["foo1"]
foo nil
> t["foo1"] = "foo1"
stdin:2: C stack overflow
stack traceback:stdin:2: in metamethod '__newindex'stdin:2: in metamethod '__newindex'...stdin:2: in metamethod '__newindex'stdin:2: in metamethod '__newindex'stdin:1: in main chunk[C]: in ?
如果仍然要赋值,可以使用rawset
函数
> t = setmetatable({foo = "foo"}, {__newindex = function (t, k, v) rawset(t, k, v) end })
> t["foo"], t["foo1"]
foo nil
> t["foo1"] = "foo1"
> t["foo"], t["foo1"]
foo foo1
上面第二个规则,如果__newindex
是一个表,那么直接对表赋值,此时赋值虽然成功,但是table仍不能访问新加入的键值对。
> t = setmetatable({foo = "foo"}, {__newindex = {}})
> t["foo"], t["foo1"]
foo nil
> t["foo1"] = "foo1"
> t["foo"], t["foo1"]
foo nil
虽然可以用元表构建只读表,但是使用rawset(t, k, v)
仍然可以修改表。
函数setmetatable
和getmetatable
也用到了元方法,用于保护元表。假设想要保护某种表(比如集合Set
),那么要使得用户既不能看到也不能修改集合的元表。如果在元表中设置__metatable
字段,那么getmetatable
会返回这个字段的值,而setmetatable
则会引发一个错误。
mt.__metatable = "not your business"s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {})stdin:1: cannot change protected metatable
跟踪对表的访问。由于__index
和__newindex
都是在表中索引不存在时才有用,因此,捕获对一个表所有访问的唯一方式是保持表是空的。为真正的表创建一个代理(proxy)。
Lua 面向对象编程
.
与:
的区别::
定义的函数隐含self参数,:
调用函数会自动传入table与self参数绑定。
使用参数self是所有面向对象语言的核心点。大多数面向对象语言都向程序员隐藏了这个机制,从而使得程序员不必显示地声明这个参数。Lua语言同样可以使用冒号操作符(colon operator)隐藏该参数。
function Account:withdraw(v)self.balance = self.balance - v
end
冒号的作用是在一个方法调用中增加一个额外的实参,或在方法的定义中隐藏一个额外的形参。冒号只是一种语法机制,虽然很便利,但是没有引入新的东西。我们可以使用点分来定义一个函数,然后用冒号语法来调用它,反之亦然。
Account = {}
function Account.withdraw(self, v)self.balance = self.balance - v
endfunction Account:deposit (v)self.balance = self.balance + v
enda = {balance = 0}
setmetatable(a, {__index = Account})a.deposit(a, 200); print(a.balance) -- 200
a:withdraw(100); print(a.balance) -- 100
function Account:new (o) -- 隐藏了self参数o = o or {} self.__index = self -- 当使用冒号调用时,等价于 Account.__index = Accountsetmetatable(o, self) -- 设置元表return o
enda = Account:new{balance = 0}
继承
SpecialAccount = Account:new() -- 创建一个从基类继承了所有操作的空类-- 重新定义从基类继承的任意方法
function SpecialAccount:withdraw (v)if v > self.balance + self.getlimit() then error("insufficient funds") endself.balance = self.balance - v
endfunction SpecialAccount:getlimit ()return self.limit or 0
ends = SpecialAccount:new{limit=1000} -- 此时的self参数指向SpecialAccount
Lua语言中的对象有一个有趣的特性,就是不需要为了指定某一种新的行为而创建一个新类,只需要直接在对象中实现这个特定的行为就可以了。比如一个特殊的透支用户s
如下,可以针对s做特殊处理,此时也仅仅会调用s
中的getlimit
,而不是SpecialAccount
中的。
function s:getlimit()return self.balance * 0.10 -- 透支额度是余额的10%
end
多重继承
多重继承实现的关键是将一个函数作为__index
的元方法。这样,父类将是一个表,当在子类中找不到某个键时,就会调用这个函数,然后遍历父类表parents
,直到找到第一个符合条件的键返回或者返回nil。
createClass
来创建子类,它的参数为新类的所有超类。
在查找不存在的字段k
时,先查询o
的元表,此时为c
,从c
的__index
中查找,此时__index
是一个表c
,那么用k
在表c
中查找,此时也没有,进一步的c
也有元表,此时__index
是一个函数,这个函数可以搜索新类c
的所有超类查找字段k
。
local function search (k, plist)for i = 1, #plist dolocal v = plist[i][k]if v then return v endend
end
-- 创建一个有多个超类的新类
function createClass (...)local c = {} -- 新类local parents = {...} -- 父类列表-- 在父类中查找缺失的方法setmetatable(c, {__index = function(t, k)return search(k, parents)end})-- 将'c'作为其实例的元表c.__index = c-- 为新类定义一个新的构造函数function c:new (o)o = o or {}setmetatable(o, c)return oendreturn c -- 返回新类
end
搜索具有复杂性,因此多重继承性能不如单继承。一种改进方法如下,将访问过的保存下来,下次再访问就和访问局部变量一样快了,但是这种做法的缺点在于当系统开始运行后修改方法的定义就比较困难了,这是因为这些修改不会沿着继承层次向下传播。
-- 在父类中查找缺失的方法
setmetatable(c, {__index = function(t, k)local v = search(k, parents)t[k] = vreturn v
end})
私有性
Lua中实现私有性的几种方式:
- 如果不想访问一个对象内的内容,那就不要去访问就是了。
- 把私有名称的最后加上一个下划线,用来和全局名称进行区分。
- 通过两个表来表示一个对象。
通过两个表来表示一个对象:一个表用来保存对象的状态,另一个表用来保存对象的操作(或接口)。我们通过第二表来访问对象本身,即通过组成其接口的操作来访问。
function newAccount (initialBalance) local self = {balance = initialBalance} local withdraw = function (v) self.balance= self.balance - v end local deposit = function (v) self.balance= self.balance+ v endlocal getBalance = function () return self.balance end return {withdraw = withdraw,deposit = deposit,getBalance = getBalance}
end
此时返回的闭包中已经包括了self
了,由于没有了额外的参数,所以也就无需使用冒号运算符来访问方法了,而是可以像调用普通函数一样,使用点运算符来调用这些方法。
环境
Lua 这种动态语言无法区分常量和变量。像 Lua 这样的嵌入式语言更复杂:虽然全局变量是在整个程序中均可见的变量,但由于 Lua 语言是由宿主应用程序调用代码段(chunk)的,因此“程序”的概念不明确。
全局变量的声明
Lua 语言中的全局变量不需要声明就可以使用。这对小型程序比较方便,但是大型程序容易产生 Bug。由于 Lua 将全局变量存放在一个普通的表中,所以可以通过元表来发现访问不存在全局变量的情况。
一种方法是简单地检测所有对全局表中不存在键的访问:
setmetatable (_G, { __newindex = function (_, n) error("attempt to ite to undecla ed va iable " .. n, 2) end, __index = function (_, n) error("attempt to ead undecla ed va iable " .. n, 2) end,
})
此时可以用rawset
来声明一个新的变量。
另一种更简单的方法是把对新全局变量的赋值限制在仅能在函数内进行,而代码段外层的代码则被允许自由赋值。
__newindex = function (t, n, v) local w = debug.getinfo(2 ,"S") .what if w ~=”main ” and w ~= "(” thenerror("attempt to ite to undecla ed va iable " .. n, 2)endrawset(t, n ,v)
end
非全局环境
自由名称(free name)是指没有关联到显式声明上的名称,即它不出现在对应局部变量的范围内。
local z = 10 -- z不是自由变量
x = y + z -- x y是自由变量
Lua语言编译器将代码段中的所有自由名称x
转换为_ENV.x
。
Lua语言是在一个名为_ENV
的预定义上值(一个外部的局部变量,upvalue)存在的情况下编译所有代码段的。 因此,所有变量要么是绑定到了一个名称的局部变量,要么是_ENV
中的一个字段,而_ENV
本身是一个局部变量(一个上值)。
由于 Lua 语言把所有的代码段都当做匿名函数,所以,Lua 语言编译器实际上将原来的代码段编译为如下形式:
local _ENV = some value
return function (...)local z = 10_ENV.x = _ENV.y + z
end
_ENV
的初始值可以是任意的表(实际上也不用一定是表)。任何一个这样的表都被称为一个环境。为了维持全局变量存在的幻觉,Lua 语言在内部维护了一个表来用作全局环境(global environment)。通常,当加载一个代码段时,函数 load 会使用预定义的上值来初始化全局环境。因此,原始代码段等价于:
local _ENV = the global environment
return function (...)local z = 10_ENV.x = _ENV.y + z
end
Lua语言中处理全局变量的方式:
- 编译器在编译所有代码段之前,在外层创建局部变量
_ENV
- 编译器将所有自由名称
var
变换为_ENV.var
- 函数
load
(或者loadfile
)使用全局环境初始化代码段的第一个上值,即Lua语言内部维护的一个普通的表。
使用_ENV
一种把旧环境装入新环境的方式是使用继承:
a = 1
local newwgt = {} -- 创建新环境
setmetatable(newgt, {__index = G})
_ENV = newgt -- 设置新环境
print(a) --> 1
上面的任何赋值都会发生在新表中,但是仍然能通过修改_G
来修改全局环境中的变量。
hive
中import
加载一个文件其实就是用的上面这个方法。
如果定义一个名为_ENV
的局部变量,那么对自由名称的引用将会绑定到这个新变量上:
a = 2
dolocal _ENV = {print = print, a = 14}print(a) --> 14
end
print(a) --> 2 (回到原始的_ENV中)
在闭包中都会使用自己的外部变量(作为上值)来访问其自由名称,这就是factory
内部能保存不同状态的实现原因了。
环境和模块
模块的缺点之一在于很容易污染全局空间,例如在私有声明中忘记加 local 关键字。
环境为解决这个问题提供了一个有趣的方式。一旦模块的主程序块有一个独占的环境,则不仅该模块所有的函数共享了这个环境,该模块的全局变量也进入到了这个环境中。我们可以将所有的公有函数声明为全局变量,这样他们就会自动地进入分开的环境中。
这时,在模块中调用模块中的其他函数时不需要任何前缀。这就是为什么当修改 hive 的测试代码时,./hive/bin/test.lua 这个文件的 require 改为 import,并且不将其其返回值赋给变量时,仍然可以打印出 log,因为log_debug
是被加入到了_G
中,log_debug
又在 log.lua 这个文件中,那么当然可以调用模块中的任何名称了。
但是书的作者仍然推荐最原始的方法,认为其不容易出错,代码更加清晰。原始的基本方法就是将模块内名称定义为local
,然后根据需要返回一张表即可。
_ENV
和load
有时想重复运行一段代码数次,每次使用一个不同的环境。
-
使用调试库的函数
debug.setupvalue
。允许改变任何制定函数的上值,如f = load ("b = 10; return a") env = {a = 20} -- 指定的函数 -- 上值的索引,对于这种用法,上值索引永远是 1 -- 新的上值 debug. setupvalue(f, 1, env) print(f()) --> 20 print(env.b) --> 10
-
在要加载的代码段前加入
_ENV = ...
垃圾收集
弱引用表(week table)、析构器(finalizer)和函数collectgarbage
是在Lua语言中用来辅助垃圾收集器的主要机制。
- 弱引用表允许收集Lua语言中还可以被程序访问的对象
- 析构器允许收集不在垃圾收集器直接控制下的外部对象
- 函数
collectgarbage
允许我们控制垃圾收集器的步长
弱引用表
*弱引用表是一种用来告知Lua语言一个引用不应阻止对一个对象回收的机制。所谓弱引用(weak reference)***是一种不在垃圾收集器考虑范围内的对象引用。如果对一个对象的所有引用都是弱引用,那么垃圾收集器将会回收这个对象并删除这些弱引用。
Lua语言通过弱引用表实现弱引用,弱引用表就是元素均为弱引用的表,这意味着如果一个对象只被一个弱引用表持有,那么Lua语言最终会回收这个对象。
三种类型弱引用表:
- 具有弱引用键的表
- 具有弱引用值的表
- 同时具有弱引用键和值的表
不论哪种类型的弱引用表,只要有一个键或者值被回收了,那么对应的整个键值对都会被从表中删除。
一个表是否为弱引用表由其元表中的__mode
字段决定。
k
:键是弱引用v
:值是弱引用kv
:键和值是弱引用
> a = {}
> setmetatable(a, {__mode = 'k'}) -- 键是弱引用的
table: 0x5653d0de26e0
> key = {}
> key
table: 0x5653d0de31c0
> a[key] = 1
> key = {}
> key
table: 0x5653d0de3de0
> a[key] = 2
> for k, v in pairs(a) do print(k, v) end
table: 0x5653d0de31c0 1
table: 0x5653d0de3de0 2
> collectgarbage() -- 此时对第一个key对应的键值都将回收
0
> for k, v in pairs(a) do print(k, v) end
table: 0x5653d0de3de0 2
字符串、数值、布尔值是值不是对象,所以除非他联系的键或者值是弱引用且被删除,不然是不会被回收的。
记忆函数(Memorize Function)
在后续使用相同参数再次调用该函数时直接返回之前记忆的结果,来加快运行速速。
local results = {}
function mem_loadst ing (s) local res = results[s]if res == nil then -- 已有结果吗?res = assert(load(s)) -- 计算新结果results[s] = res -- 保存结果以便后续重用end return res
end
但可能导致不易察觉的资源浪费。虽然有些命令重复出现,但也有很多命令只出现一次。渐渐地,表results
会堆积上服务器收到的所有命令及编译结果;长时间会耗尽服务器的内存。
弱引用表为解决这个问题提供了一种简单的方案。如果results
具有弱引用的值,那么每个垃圾收集器周期都会删除所有那个时刻未使用的编译结果(基本上就是全部)
回顾具有默认值的表
- 使用一个弱引用表来映射每一个表和它的默认值,键是弱引用的
- 对不同默认是使用不同的元表,在遇到重复的默认值时会复用相同的元表,值是弱引用的
瞬表(Ephemeron Table)
一种棘手的情况是:一个具有弱引用键的表中的值又引用了对应的键。
do local mem = {} -- 记忆表setmetatable(mem, {__mode =”k”}) function factory(o)local res = mem [o] if not res then res = (function () return o end) mem[o] = res end return resend
end
上面弱引用键中的值又回引了弱引用键。但是直观的理解是,值中的引用将是一个强引用。
Lua语言同瞬表来解决这个问题。
瞬表:一个具有弱引用键和强引用值的表
瞬表中,键的可访问性控制着值的可访问性。
瞬表中的元素(k, v),v 指向的引用只有当某些指向 k 的外部引用存在时才是强引用,否则,即使 v 直接或间接的引用了 k,垃圾收集器最终会收集 k 并把 (k,v)从表中移除。
也就是说,v 所指向的引用必须能被瞬表外部的引用通过k获得才是强引用。
析构器(Finalizer)
析构器是一个与对象关联的函数,当该对象即将被收回时,该函数会被调用。
Lua 的析构器通过元表的元方法__gc
实现。
> o = {"hi", "hello", "world"}
> setmetatable(o, {__gc = function(o) for k, v in pairs(o) do print(k, v) end end});
> o = nil
> collectgarbage()
1 hi
2 hello
3 world
0 -- 这是collectgarbage执行成功的返回值
在设置元表后再定义__gc
元方法,对于之前设置的对象将不起作用。
> o = {"hi", "hello", "world"}
> mt = {}
> setmetatable(o, mt);
> mt.__gc = function(o) for k, v in pairs(o) do print(k, v) end end
> o = nil
> collectgarbage()
0
如果一定要之后再设置__gc
元方法的行为,那么在设置元表时需要给__gc
元方法一个占位符,这样也就能标记对象是需要进行析构处理的。
> o = {"hi", "hello", "world"}
> mt = {__gc = true}
> setmetatable(o, mt);
> mt.__gc = function(o) for k, v in pairs(o) do print(k, v) end end
> o = nil
> collectgarbage()
1 hi
2 hello
3 world
0
当垃圾收集器在同一个周期中析构多个对象时,他会按照对象被标记为需要析构处理的顺序逆序调用这些对象的析构器。
复苏(resurrection):当一个析构器被调用时,它的参数是正在被析构的对象。因此,这个对象至少会在析构期间重新变成活跃的。
同时,在执行析构器期间,无法阻止析构器把该对象存储在全局变量中,若进行了这样的操作,那么对象在析构器返回后仍可以访问,这成为永久复苏。
复苏必须是可传递的
> A = {x = "this is A"}
> B = {f = A}
> setmetatable(B, {__gc = function (o) print(o.f.x) end});
> A, B = nil
> collectgarbage()
this is A
0
由于复苏的存在,Lua语言会在两个阶段中回收具有析构器的对象
- 当垃圾收集器首次发现某个具有析构器的对象不可达时,垃圾收集器把这个对象复苏并将其**放入等待被析构的队列中。**一旦析构器开始执行,Lua语言就将该对象标记为已被析构。
- 当下一次垃圾收集器又发现这个对象不可达时,他就将这个对象删除。
如果想保证我们程序中的所有垃圾都被真正的释放,那么必须调用collectgarbage
两次,第二次调用才会删除第一次调用中被析构的对象。
这两个阶段,总结就是
- 阶段一:放入析构队列,执行析构器,标记已被析构。==注意:==此时对象还没有被删除,可以被别的析构器中的进行引用.
- 阶段二:删除被标记析构的对象。
在每个垃圾收集周期内,垃圾收集器会在调用析构器前清理弱引用表中的值,在调用析构器之后再清理键
垃圾收集器
垃圾收集器周期由四个阶段组成:
- 标记(mark)
- 清理(cleaning)
- 清除(sweep)
- 析构(finalization)
Lua 协程(coroutine)
方法 | 描述 |
---|---|
create(f) | 创建一个协程,参数是一个函数,当和resume配合使用的时候就是唤醒函数调用 |
resume(co [, value]) | 重启coroutine,和create搭配使用 |
yield([返回值]) | 挂起coroutine |
status(co) | 查看coroutine状态,dead suspend running |
wrap(f) | 创建coroutine,返回一个函数,一旦调用这个函数,就进入coroutine,和create功能重复 |
running() | 返回正在运行的coroutine,一个coroutine就是一个线程,当使用running时,就是返回coroutine的线程号。 |
yield
将返回值返回给resume
调用者,resume
调用者传入数据,作为yield
执行后的结果。
function foo()print("协同程序 foo 开始执行")local value = coroutine.yield("暂停 foo 的执行") -- value是由resume传入的数据print("协同程序 foo 恢复执行,传入的值为: " .. tostring(value))print("协同程序 foo 结束执行")
end-- 创建协程
local co = coroutine.create(foo)-- 启动协程
local status, result = coroutine.resume(co)
print(result) -- 输出: 暂停 foo 的执行-- 恢复协程的执行,并传入一个值
status, result = coroutine.resume(co, 42)
print(result) -- 输出: 协同程序 foo 恢复执行,传入的值为: 42
running()
这个函数的最大作用,我觉得就是获得当前的线程号。
coroutine
的底层实现是一个线程。当create
一个coroutine
时就是在新线程中注册了一个事件。当resume
触发事件时,create
的coroutine
函数就被执行了,当遇到yield
的时候就代表挂起当前线程,等resume
再次触发事件。
反射(Reflection)
反射是程序用来检查和修改其自身某些部分的能力。Lua支持的几种反射机制:
- 环境允许运行时观察全局变量
- type、pairs这样的函数允许运行时检查和遍历未知数据结构
- load、require这样的函数允许程序在自身中追加代码或更改新代码。
需要调试库(debug library)填补的缺失有:
- 检查局部变量
- 跟踪代码的执行
- 函数被谁调用
调试库由两类函数组成:自省函数(introspective function)和钩子(hook)。
自省函数允许我们检查一个正在运行中的程序的几个方面,例如活动函数的栈、当前正在执行的代码行、局部变量的名称和值。
钩子则允许我们跟踪一个程序的执行。
自省机制(Introspective Facility)
钩子(Hook)
有四种事件能够触发一个钩子:
- 每当调用一个函数时产生的
call
事件 - 每当函数返回时产生的
return
事件 - 每当开始执行一行新代码时产生的
line
事件 - 执行完指定数量的指令后产生的
count
事件。(这里的指令指的是内部操作码)
沙盒(Sandbox)
C 语言 API 总览
Lua 是一种嵌入式语言(embedded language),这就意味着 Lua 并不是一个独立运行的应用,而是一个库,他可以链接到其他应用程序,将 Lua 的功能融入这些应用。
[!TIP]
先去读 Lua 解释器,即可执行的 lua。这个可执行文件是一个小应用,大概有 600 行代码,它是用 Lua 标准库实现的独立解释器(stand-alone interpreter)。
Lua 语言程序可以在 Lua 环境中注册新的函数,比如用 C 语言(或其他语言)实现的函数,从而增加一些无法直接用 Lua 语言编写的功能,因为 Lua 语言也是一种可扩展的语言(extensible language)。
C API 包括:
- 读写 Lua 全局变量的函数
- 调用 Lua 函数的函数
- 运行 Lua 代码段的函数
- 注册 C 函数(以便于其后可被 Lua 代码调用)的函数
Lua 使用一个虚拟栈和 C 互传值。栈上的每个元素都是一个Lua值。可以用索引来操作栈上的值。
**无论何时Lua调用C,被调用的函数都得到一个新的栈,这个栈独立于C函数本身的栈,也独立于之前的Lua栈。**它里面包含了Lua传递给C函数的所有参数,而C函数则把要返回的结果放入这个栈以返回给调用者(参见
lua_CFunction
)。
// 如果给定索引处的值是一个完全用户数据, 函数返回其内存块的地址。 如果值是一个轻量用户数据, 那么就返回它表示的指针。 否则,返回 NULL 。
void *lua_touserdata (lua_State *L, int index);// 检查 cond 是否为真。 如果不为真,以标准信息形式抛出一个错误
void luaL_argcheck (lua_State *L, int cond, int arg, const char *extramsg);// 检查函数在 arg 位置是否有任何类型(包括 nil)的参数
void luaL_checkany (lua_State *L, int arg);// 创建一张新的表,并把列表 l 中的函数注册进去。它是用下列宏实现的:
// (luaL\_newlibtable(L,l), luaL\_setfuncs(L,l,0))
// 数组 l 必须是一个数组,而不能是一个指针。
void luaL_newlib (lua_State *L, const luaL_Reg l[]);
要在 C 中区别不同类型的用户数据,一种常用的方法是为每种类型创建唯一的元表。每次创建用户数据时,用相应的元表进行标记;每当获取用户数据时,检查其是否有正确的元表。由于 Lua 代码不能改变用户数据的元表,因此不能绕过这些检查。
// 创建一张新表(被用作元表),将其压入栈顶。同时在表中加入字段 __name = tname,并将该表与注册表中 tname 字段联系起来,即[tname] = new table
int luaL_newmetatable(lua_State *L, const char *tname);
// 在注册表中查询 tname 对应的元表,查到了将其压栈,没找到,压入 nil
void luaL_getmetatable(lua_State *L, const char *tname);
// 检查栈中制定位置上的对象是否是与指定名称 tname 的元表匹配的用户数据。如果该对象不是用户数据或者没有正确的元表,引发错误,否则,返回这个用户数据的地址。
void *luaL_checkudata(lua_State *L, int index, const char *tname);