概念
反射是程序用来检查和修改其自身某些部分的能力
Lua中几种反射机制
- 环境允许运行时观察全局变量;
- 诸如
type
和pairs
这样的函数允许运行时检查和遍历未知数据结构; - 诸如
load
和require
这样的函数允许程序在自身中追加代码或更新代码
调试库
- 自省函数:允许我们检查一个正在运行中的程序的几个方面,例如活动函数的栈、当前正在执行的代码行、局部变量的名称和值。
- 钩子:则允许我们跟踪一个程序的执行。
自省机制
自省函数:getinfo
该函数的第一个参数可以是一个函数或一个栈层次。
当为某个函数foo调用debug.getinfo(foo)
时,该函数会返回一个包含与该函数有关的一些数据的表。这个表可能具有以下字段。
source
:该字段用于说明函数定义的位置。如果函数定义在一个字符串中(通过调用load ),那么source就是这个字符串;如果函数定义在一个文件中,那么source就是使用@作为前缀的文件名。short_src
:该字段是source的精简版本(最多60个字符),对于错误信息十分有用。linedefined
:该字段是该函数定义在源代码中第一行的行号。lastlinedefined
:该字段是该函数定义在源代码中最后一行的行号。what
:该字段用于说明函数的类型。如果foo是一个普通的Lua函数,则为"Lua";如果是一个C函数,则为"C";如果是一个Lua语言代码段的主要部分,则为"main"。name
:该字段是该函数的一个适当的名称,例如保存该函数的全局变量的名称。namewhat
:该字段用于说明上一个字段R的含义,可能是"global" 、"local"、" method"、"field"或""(空字符串)。空字符串表示Lua语言找不到该函数的名称。nups
:该字段是该函数的上值的个数。nparams
:该字段是该函数的参数个数。isvararg
:该字段表明该函数是否为可变长参数函数(一个布尔值)。activelines
:该字段是一个包含该函数所有活跃行的集合。活跃行是指除空行和只包含注释的行外的其他行(该字段的典型用法是用于设置断点。大多数调试器不允许在活跃行外设置断点,因为非活跌行是不可达的)。func
:该字段是该函数本身。
当foo是一个C函数时,Lua语言没有多少关于该函数的信息。对于这种函数,只有字段what、name、namewhat、nups和 func是有意义的。
当使用一个数字n作为参数调用函数debug.getinfo(n)
时,可以得到有关相应栈层次上活跃函数的数据。如果n大于栈中活跃函数的数量,那么函数debug.getinfo
返回nil。
当通过带有栈层次的debug.getinfo
查询一个活跃函数时,返回的表中还有两个额外字段:
currentline
:表示当前该函数正在执行的代码所在的行;istailcall
(一个布尔值):如果为真则表示函数是被尾调用所调起(在这种情况下,函数的真实调用者不再位于栈中)。
函数getinfo有一个可选的第二参数,该参数用于指定希望获取哪些信息。这个参数是一个字符串其中每个字母代表选择一组字段
字符 | 代表的字段 |
---|---|
n | 选择name 和namewhat |
f | 选择func |
S | 选择source 、short_src 、what 、linedefined 和lastlinedefined |
l | 选择currentline |
L | 选择activelines |
u | 选择nup 、nparams 和isvararg |
例:打印出了活跃栈的栈回溯:
function traceback ()
for level = 1, math.huge do
local info = debug.getinfo(level, "Sl")
if not info then break end
if info.what == "C" then --是否是C函数?
print(string.format("%d\tC function", level))
else -- Lua函数
print(string.format("%d\t[%s ]:%d ", level, info.short_src, info.currentline))
end
end
end
print(debug.traceback())
访问局部变量
函数:debug.getlocal来检查任意活跃函数的局部变量。
有两个参数:一个是要查询函数的栈层次,另一个是变量的索引
返回值:变量名和变量。如果变量索引大于活跃变量的数量,那么函数getlocal返回nil。如果栈层次无效,则会抛出异常(我们可以使用函数debug.getinfo来检查栈层次是否有效)。
Lua语言按局部变量在函数中的出现顺序对它们进行编号,但编号只限于在函数当前作用域中活跃的变量。
例如,考虑如下的代码:
function foo (a, b)
local x
do local c = a - b end
local a = 1
while true do
local name, value = debug.getlocal(1, a)
if not name then break end
print(name, value)
a = a + 1
end
end
foo(10, 20)
--[[
a 10
b 20
x nil
a 4
]]
索引为1的变量是a(第一个参数),索引为2的变量b,索引为3的变量是x,索引为4的变量是内层的a。在
getlocal
被调用的时候,c已经离开了作用域,而name和value还未出现于作用域内(请注意,局部变量只在初始化后才可见)。
可以通过函数debug.setlocal
改变局部变量的值,该函数的前两个参数与getlocal
相同,分别是栈层次和变量索引,而第三个参数是该局部变量的新值。该函数的返回值是变量名,如果变量索引超出了范围则返回nil。
访问非局部变量
函数:getupvalue
第一个参数是函数(也可称为闭包),第二个参数是变量索引
也可用于更新非布局变量的值,也就是第三个参数
总的来说有三个参数:闭包、变量索引、新值
function getvarvalue (name,level, isenv)
local value
local found = false
level = (level or 1) + 1
--尝试局部变量
for i = 1, math.huge do
local n, v = debug.getlocal(level, i)
if not n then break end
if n == name then
value = v
found = true
end
end
if found then return "local" , value end
--尝试非局部变量
local func = debug.getinfo(level,"f" ).func
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then break end
if n == name then return "upvalue ", v end
end
if isenv then return "noenv" end --避免循环
--没找到;从环境中获取值
local _, env = getvarvalue("_ENV" , level,true)
if env then
return "global", env[name]
else --没有有效的_ENV
return "noenv"
end
end
local a = 4;
print(getvarvalue("a")) --> local
a = "xx";
print(getvarvalue("a")) --> global xx
参数
level
指明在哪个栈层次中寻找函数, 默认为1
该函数首先查找局部变量。如果有多个局部变量的名称与给定的名称相同,则获取具有最大索引的那个局部变量。因此,函数必须执行完整个循环。如果找不到指定名称的局部变量,那么就查找非局部变量。为了遍历非局部变量,该函数使用debug.getinfo
函数获取调用闭包,然后遍历非局部变量。最后,如果还是找不到指定名字的非局部变量,就检索全局变量:该函数递归地调用自己来访问合适的__ENV
变量并在相应环境中查找指定的名字。
参数isenv
避免了一个诡异的问题。该参数用于说明我们是否处于一个从__ENV
变量中查询全局名称的递归调用中。一个不使用全局变量的函数可能没有上值__ENV
。在这种情况下,如果我们试图把__ENV
当作全局变量来查询,那么由于我们需要__ENV
来得到其自身的值,所以可能会陷入无限递归循环。因此,当isenv
为真且函数getvarvalue
找不到局部变量或上值时,getvarvalue
就不应该再尝试全局变量。
访问其它协程
采用自省函数,可接受一个可选的协程作为第一个参数
co = coroutine.create(function ()
local x = 10
coroutine.yield()
error("some error")
end)
coroutine.resume(co)
print(debug.traceback(co))
--[[
stack traceback :
[C]: in function 'yield'
temp: 3: in function <temp : 1>
]]
-- 再次唤醒协程
print(coroutine.resume(co)) --> false temp :4: some error
--输出栈回溯
--[[
stack traceback :
[C]: in function 'error'
temp :4: in function <temp : 1>
]]
--可检查局部变量
print(debug.getlocal(co, 1, 1)) --> x 10
钩子
调试库中的钩子机制允许用户注册一个钩子函数,这个钩子函数会在程序运行中某个特定事件发生时被调用。
四种事件能够触发一个钩子:
- 每当调用一个函数时产生的call事件;
- 每当函数返回时产生的return事件;
- 每当开始执行一行新代码时产生的line事件;
- 执行完指定数量的指令后产生的count事件。
要注册一个钩子,需要用两个或三个参数来调用函数debug.sethook
:
- 第一个参数是钩子函数
- 第二个参数是描述要监控事件的掩码字符串
- 第三个参数是一个用于描述以何种频度获取count事件的可选数字。
如果要监控call
、return
和line事件
,那么需要把这几个事件的首字母(c、r或1)放入掩码字符串。
如果要监控count事件,则只需要在第三个参数中指定一个计数器。
如果要关闭钩子,只需不带任何参数地调用函数sethook
即可。
debug.sethook(print, "l") -- 为print安装构造函数,在line事件发生时调用它
例:使用函数getinfo获取当前文件名并添加到输出中:
function trace (event,line)
local s = debug.getinfo(2).short_src
print(s .. ":" .. line)
end
debug.sethook(trace, "l")
例:使用函数debug.debug,提供一个能够执行任意Lua语言命令的提示符
function debug1 ()
while true do
io.write(" debug> ")
local line = io.read()
if line == "cont" then break end
assert(load(line)) ()
end
end
当用户输入“命令”cont时,函数返回。