Lua程序设计 | 协程与反射

本文深入探讨Lua语言中的协程概念,包括协程的基础、如何使用协程解决生产者-消费者问题,以及如何利用协程实现迭代器。此外,还介绍了Lua的调试库,涵盖自省机制和钩子函数,使开发者能够更有效地调试和优化代码。

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

在这里插入图片描述

From《Programming in Lua》 by Roberto Ierusalimschy


协程

我们并不经常需要用到协程,但是当需要的时候,协程会起到一种不可比拟的作用。协程可以颠倒调用者和被调用者的关系,而且这种灵活性解决了软件架构中被笔者称为“谁是老大”或者“谁拥有主循环”的问题。这正是对诸如事件驱动编程、通过构造器构建迭代器和协作式多线程等几个看上去并不相关的问题的泛化,而协程以简单和高效的方式解决了这些问题。

从多线程的角度看,协程与线程类似:协程是一系列的可执行语句,拥有自己的栈、局部变量和指令指针,同时协程又与其他协程共享了全局变量和其他几乎一切资源。线程与协程的主要区別在于,一个多线程程序可以并行运行
多个线程,而协程却需要彼此协作地运行,即在任意指定的时刻只能有一个协程运行,且只有当正在运行的协程显式地要求被挂起(suspend)时其执行才会暂停。

接下来,我们会学习Lua语言中的协程是如何运行的,同时也将学习如何使用协程来解决一系列的问题。

协程基础

Lua语言中协程相关的所有函数都被放在表coroutine中。函数create用于创建新协程,该函数只有一个参数,即协程要执行的代码的函数(协程体(body))。函数create返回一个"thread"类型的值,即新协程。通常,函数create的参数是一个匿名函数,例如:

co = coroutine.create(function () print("hi") end)
print(type(co))	--> thread

一个协程有以下四种状态,即挂起(suspended \运行(running)、正常(normal)和死亡(dead)。我们可以通过函数coroutine.status来检查协程的状态:

print(coroutine.status(co))	--> suspended

当一个协程被创建时,它处于挂起状态,即协程不会在被创建时自动运行。函数coroutine.resume用于启动或再次启动一个协程的执行,并将其状态由挂起改为运行:

coroutine.resume(co) --> hi

如果在交互模式下运行上述代码,最好在最后一行加上一个分号来阻止输岀函数resume的返回值。在上例中,协程体只是简单地打印了”hi”后便终止了,然后协程就变成了死亡状态:

print(coroutine.status(co))	--> dead

到目前为止,协程看上去也就是一种复杂的调用函数的方式。协程的真正强大之处在于函数yield,该函数可以让一个运行中的协程挂起自己,然后在后续恢复运行。例如下面这个简单的示例:

co = coroutine.create(function ()
    for i = 1, 10 do
      print("co", i)
      coroutine.yield()
    end
  end)

其中,协程进行了一个循环,在循环中输出数字并在每次打印后挂起。当唤醒协程后,它就会开始执行直到遇到第一个yield:

coroutine.resume(co)	--> co 1

此时,如果我们查看协程状态,会发现协程处于挂起状态,因此可以再次恢复运行:

print(coroutine.status(co))	--> suspended

从协程的角度看,在挂起期间发生的活动都发生在协程调用yield期间。当我们唤醒协程时,函数yield才会最终返回,然后协程会继续执行直到遇到下一个yield或执行结束:

coroutine.resume(co)		--> co	2
coroutine.resume(co)		--> co	3
	···
coroutine.resume(co)		--> co	10
coroutine.resume(co)		-- 不输出任何数据



在最后一次调用resume时,协程体执行完毕并返回,不输出任何数据。如果我们试图再次唤醒它,函数resume将返回false及一条错误信息:

print(coroutine.resume(co))
--> false cannot resume dead coroutine

请注意,像函数pcall 一样,函数resume也运行在保护模式中。因此,如果协程在执行中出错,Lua语言不会显示错误信息,而是将错误信息返回给函数resume。

当协程A唤醒协程B时,协程A既不是挂起状态(因为不能唤醒协程A),也不是运行状态(因为正在运行的协程是B)。所以,协程A此时的状态就被称为正常状态。

Lua语言中一个非常有用的机制是通过一对resume-yield来交换数据。第一个resume函数(没有对应等待它的yield )会把所有的额外参数传递给协程的主函数:

co = coroutine.create(function (a, b, c)
    print("co", a, b, c + 2)
  end)
coroutine.resume(co, 1, 2, 3)	--> co 1 2 5

在函数coroutine.resume的返回值中,第一个返回值为true时表示没有错误,之后的返回值对应函数yield的参数:

co = coroutine.create(function (a,b)
    coroutine.yield(a + b, a - b)
  end)
print(coroutine.resume(co, 20, 10)) 	--> true 30 10

与之对应的是,函数coroutine.yield的返回值是对应的resume的参数:

co = coroutine.create (function (x)
    print("co1", x)
    print("co2", coroutine.yield)
  end)
coroutine.resume(co, "hi")		--> co1 hi
coroutine.resume(co, 4, 5)	 	--> co2 4 5

最后,当一个协程运行结束时,主函数所返回的值都将变成对应函数resume的返回值:

co = coroutine.create(function ()
    return 6, 7
  end)
print(coroutine.resume(co))	--> true 6 7

我们很少在同一个协程中用到所有这些机制,但每种机制都有各自的用处。

虽然协程的概念很容易理解,但涉及的细节其实很多。我们有必要在进行进一步学习前先理清一些细节。

Lua语言提供的是所谓的非对称协程,也就是说需要两个函数来控制协程的执行,一个用于挂起协程的执行,另一个用于恢复协程的执行。而其他一些语言提供的是对称协程,只提供一个函数用于在一个协程和另一个协程之间切换控制权。

一些人将非对称协程称为semi-coroutines。然而,其他人则用相同的术语半协程表示协程的一种受限制版实现。在这种实现中,一个协程只能在它没有调用其他函数时才可以挂起,即在调用栈中没有挂起的调用时。换句话说,只有这种半协程的主函数才能让出执行权(Python中的generator正是这种半协程的一个例子)。

与对称协程和非对称协程之间的区别不同,协程与generator (例如Python中的)之间的区别很大。generator比较简单,不足以实现某些最令人关心的代码结构,而这些代码结构可以使用完整功能的协程实现。Lua语言提供了完整的、非对称的协程。对于那些更喜欢对称协程的用户而言,可以基于非对称协程实现对称协程。

哪个协程占据主循环

有关协程的最经典示例之一就是生产者-消费者问题。在生产者-消费者问题中涉及两个函数,一个函数不断地产生值(比如,从一个文件中读取),另一个函数不断地消费这些值(比如,将值写入另一个文件中)。这两个函数可能形式如下:

function producer ()
  while true do
    local x = io.read()			-- 产生新值
    send(x)						-- 发给消费者
  end
end

function consumer ()
  while true do
    local x = receive()			-- 接收来自生产者的值
    io.write(x, "\n")			-- 消费
  end
end   

为了简化这个示例,生产者和消费者都是无限循环的;不过,可以很容易地将其修改为没有数据需要处理时退出循环。这里的问题在于如何将send与receive匹配起来,也就是“谁占据主循环(who-has-the-main-loop)”问题的典型实例。其中,生产者和消费者都处于活跃状态,它们各自具有自己的主循环,并且都将对方视为一个可调用的服务。对于这个特定的示例,可以很容易地修改其中一个函数的结构,展开它的循环使其成为一个被动的代理。不过,在其他的真实场景下,这样的代码结构改动可能会很不容易。

由于成对的resume-yield可以颠倒调用者与被调用者之间的关系,因此协程提供了一种无须修改生产者和消费者的代码结构就能匹配它们执行顺序的理想工具。当一个协程调用函数Yield时,它不是进入了一个新函数,而是返回一个挂起的调用(调用的是函数resume)。同样地,对函数resume的调用也不会启动一个新函数,而是返回一个对函数yield的调用。这种特性正好可以用于匹配send和receive,使得双方都认为自己是主动方而对方是被动。因此,receive唤醒生产者的执行使其能生成一个新值,然后send则让出执行权,将生成的值传递给消费者:

function receive ()
  local status, value = coroutine.resume(producer)
  return value
end

function send (x)
	coroutine.yield(x)
end

当然,生产者现在必须运行在一个协程里:

producer = coroutine.create(producer)

在这种设计中,程序通过调用消费者启动。当消费者需要新值时就唤醒生产者,生产者向消费者返回新值后挂起,直到消费者再次将其唤醒。因此,我们将这种设计称为消费者驱动式的设计。另一种方式则是使用生产者驱动式的设计,其中消费者是协程。虽然上述两种设计思路看上去是相反的,但实际上它们的整体思想相同。

我们可以使用过滤器来扩展上述设计。过滤器位于生产者和消费者之间,用于完成一些对数据进行某种变换的任务。过滤器(filter)既是一个消费者又是一个生产者,它通过唤醒一个生产者来获得新值,然后又将变换后的值传递给消费者。例如,我们可以在前面代码中添加一个过滤器以实现在每行的起始处插入行号。

使用过滤器的生产者和消费者参见下例:

function receive (prod)
  local status, value = coroutine.resume(prod)
  return value
end

function send (x)
  coroutine.yield(x)
end

function producer ()
  return coroutine.create(function ()
      while true do
        local x = io.read() -- 产生新值
        send(x)
      end
    end)
end

function filter (prod)
  return coroutine.create(function ()
      for line = 1, math.huge do
        local x = receive(prod) -- 接收新值
        x = string.format("%5d %s", line, x)
        send(x) -- 发送给消费者
      end
    end)
end

function consumer (prod)
  while true do
    local x = receive(prod)
    io.write(x, "\n")
  end
end

consumer(filter(producer()))

代码的最后一行只是简单地创建出所需的各个组件,将这些组件连接在一起,然后启动消费者。

将协程用作迭代器

我们可以将循环迭代器视为生产者-消费者模式的一种特例:一个迭代器会生产由循环体消费的内容。因此,用协程来实现迭代器看上去就很合适。的确,协程为实现这类任务提供了一种强大的工具。同时,协程最关键的特性是能够颠倒调用者与被调用者之间的关系。

有了这种特性,我们在编写迭代器时就无须担心如何保存连续调用之间的状态了。

为了说明这类用途,让我们来编写一个遍历指定数组所有排列的迭代器。要直接编写这种迭代器并不容易,但如果要编写一个递归函数来产生所有的排列则不是很难。思路很简单,只要依次将每个数组元素放到最后一个位置,后递归地生成其余元素的所有排列即可。代码参见下例子:

function permgen (a, n)
  n = n or #a				-- 'n'的默认大小是'a'
  if n <= 1 then 			-- 只有一种组合
    printResult(a)
  else
    for i = 1, n do
      -- 把第i个元素当做最后一个
      a[n], a[i] = a[i], a[n]
      
      -- 生成其余元素的所有排列
      permgen(a, n - 1)
      
      -- 恢复第i个元素
      a[n], a[i] = a[i], a[n]
    end
  end
end

还需要定义一个合适的函数printResult来输出结果,并使用恰当的参数调用permgen:

function printResult (a)
  for i = 1, #a do io.write(a[i], " ") end
  io.write("\n")
end

permgen({1, 2, 3, 4})

--> 2 3 4 1 
--> 3 2 4 1 
--> 3 4 2 1
···
--> 2 1 3 4 
--> 1 2 3 4 

当有了生成器后,将其转换为迭代器就很容易了。首先,我们把printResult改为yield:

function permgen (a, n)
  n = n or #a
	if n <= 1 then
    coroutine.yield(a)
  else
    -- 同前···

然后,我们定义一个将生成器放入协程运行并创建迭代函数的工厂。迭代器只是简单地唤醒协程,让其产生下一个排列:

function permutations (a)
  local co = coroutine.create(function () permgen(a) end)
  return function ()	--迭代函数
    local code, res = coroutine.resume(co)
    return res
  end
end

有了上面的这些,在for循环中遍历一个数组的所有排列就非常简单了 :

for p in permutations{"a", "b", "c"} do
  printResult(p)
end

--> b c a 
--> c b a 
--> c a b 
--> a c b 
--> b a c 
--> a b c

函数permutations使用了 Lua语言中一种常见的模式,就是将唤醒对应协程的调用包装在一个函数中。由于这种模式比较常见,所以Lua语言专门提供了一个特殊的函数coroutine.wrap来完成这个功能。

与函数create类似,函数wrap也用来创建一个新的协程。但不同的是,函数wrap返回的不是协程本身而是一个函数,当这个函数被调用时会唤醒协程。与原始的函数resume不同,该函数的第一个返回值不是错误代码,当遇到错误时该函数会抛出异常。我们可以使用函数wrap改写permutations:

function permutations (a)
  return coroutine.wrap(function () permgen(a) end)
end

通常,函数coroutine.wrap比函数coroutine.create更易于使用。它为我们提供了对于操作协程而言所需的功能,即一个唤醒协程的函数。不过,该函数缺乏灵活性,我们无法检查通过函数wrap所创建的协程的状态,也无法检查运行时的异常。

反射

反射是程序用来检查和修改其自身某些部分的能力。像Lua语言这样的动态语言支持几种反射机制:环境允许运行时观察全局变量;诸如type和pairs这样的函数允许运行时检查和遍历未知数据结构;诸如load和require这样的函数允许程序在自身中追加代码或更新代码。不过,还有很多方面仍然是缺失的:程序不能检查局部变量,开发人员不能跟踪代码的执行,函数也不知道是被谁调用的,等等。调试库填补了上述的缺失。

调试库由两类函数组成:自省函数(introspective fimction )和钩子(hook)。自省函数允许我们检查一个正在运行中的程序的几个方面,例如活动函数的栈、当前正在执行的代码行、局部变量的名称和值。钩子则允许我们跟踪一个程序的执行。

虽然名字里带有“调试“的字眼,但调试库提供的并不是Lua语言的调试器(debugger)。不过,调试库提供了编写我们自己的调试器所需要的不同层次的所有底层机制。

调试库与其他库不同,必须被慎重地使用。首先,调试库中的某些功能的性能不高。其次,调试库会打破语言的一些固有规则,例如不能从一个局部变量的词法定界范围外访问这个局部变量。虽然调试库作为标准库直接可用,但建议在使用调试库的代码段中显式地加载调试库。

自省机制

调试库中主要的自省函数是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:该字段用于说明上一个字段①的含义,可能是"global"、“local”、“method”、“field"或”"(空字符串)。空字符串表示Lua语言找不到该函数的名称。
  • nups:该字段是该函数的上值的个数。
  • nparams:该字段是该函数的参数个数。
  • isvararg:该字段表明该函数是否为可变长参数函数(一个布尔值)。
  • activelines:该字段是一个包含该函数所有活跃行的集合。活跃行(active line)是指除空行和只包含注释的行外的其他行(该字段的典型用法是用于设置断点。大多数调试器不允许在活跃行外设置断点,因为非活跃行是不可达的)。
  • func:该字段是该函数本身。

当foo是一个C函数时,Lua语言没有多少关于该函数的信息。对于这种函数,只有字段 what、name、namewhat、nups 和 func 是有意义的。

当使用一个数字n作为参数调用函数debug.getinfo(n)时,可以得到有关相应栈层次上活跃函数的数据。栈层次是一个数字,代表某个时刻上活跃的特定函数。调用getinfo的函数A的层次是1,而调用A的函数的层次是2,以此类推(层次0是C函数getinfo自己)。如果n大于栈中活跃函数的数量,那么函数debug.getinfo返回nil。

当通过带有栈层次的debug.getinfo查询一个活跃函数时,返回的表中还有两个额外字段:currentline,表示当前该函数正在执行的代码所在的行;istailcall (一个布尔值),如果为真则表示函数是被尾调用所调起(在这种情况下,函数的真实调用者不再位于栈中)。

字段name有些特殊。请注意,由于函数在Lua语言中是第一类值,因此函数既可以没有名称也可以有多个名称。Lua语言会通过检查调用该函数的代码来看函数是如何被调用的,进而尝试找到该函数的名称。这种方法只有在以一个数字为参数调用getinfo时才会起作用,即我们只能获取关于某一具体调用的信息。

函数getinfo的效率不高。Lua语言以一种不影响程序执行的形式来保存调试信息,至于获取这些调试信息的效率则是次要的。为了实现更好的性能,函数getinfo有一个可选的第二参数,该参数用于指定希望获取哪些信息。通过这个参数,函数getinfo就不会浪费时间去收集用户不需要的数据。这个参数是一个字符串,其中每个字母代表选择一组字段,如下表所示:

z含义
n选择 name 和 namewhat
f选择func
S选择 source、short_src、what、linedefined 和 lastlinedefined
l选择 currentline
L选择 activelines
u选择 nup、nparams 和 isvararg

下面这个函数演示了函数debug.getinfo的用法,它打印出了活跃栈的栈回溯:

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

要改进这个函数并不难,只需要让函数getinfo返回更多数据即可。事实上,调试库也提供了这样一个改进版本,即函数traceback。与我们的版本不同的是,函数debug.traceback不会打印结果,而是返回一个(可能会很长的)包含栈回溯的字符串:

print(debug. traceback)
  -- stack traceback:
  -- stdin:1: in main chunk
  -- [C]: in ?

访问局部变量

我们可以通过函数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还未出现于作用域内(请注意,局部变量只在初始化后才可见)。

从Lua5.2开始,值为负的索引获取可变长参数函数的额外参数,索引-1指向第一个额外参数。此时,变量的名称永远是"(*vararg)"。

我们还可以通过函数debug.setlocal改变局部变量的值,该函数的前两个参数与getlocal相同,分别是栈层次和变量索引,而第三个参数是该局部变量的新值。该函数的返回值是变量名,如果变量索引超出了范围则返回nil

访问非局部变量

调试库还提供了函数getupvalue,该函数允许我们访问一个被Lua函数所使用的非局部变量。与局部变量不同,被一个函数所引用的非局部变量即使在弓I用它的函数已经不活跃的情况下也会一直存在(毕竟这就是闭包的实质)。因此,函数getupvalue的第一个参数不是栈层次,而是一个函数(更确切地说,是一个闭包)。函数getupvalue的第二个参数是变量索引,Lua语言按照函数引用非局部变量的顺序对它们编号,但由于一个函数不能用同一名称访问两个非局部变量,所以这个顺序是无关紧要的。

我们还可以通过函数debug.setupvalue更新非局部变量的值。该函数有三个参数:一个闭包、一个变量索引和一个新值。与函数setlocal一样,该函数返回变量名,如果变量索引超出范围则返回nil。

下例演示了如何通过变量名访问一个函数中变量的值。

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   4
> a = "xx"; print(getvarvalue("a"))				--> global  xx

参数level指明在哪个栈层次中寻找函数,1 (默认值)意味着直接的调用者。代码中多加的1将层次纠正为包括getvarvalue自己。我们稍后会解释参数isenv。

该函数首先查找局部变量。如果有多个局部变量的名称与给定的名称相同,则获取具有最大索引的那个局部变量。因此,函数必须执行完整个循环。如果找不到指定名称的局部变量,那么就査找非局部变量。为了遍历非局部变量,该函数使用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))

对函数traceback的调用作用在协程co上,结果如下:

stack traceback:
        [C]: in function 'coroutine.yield'
        test.lua:346: in function

由于协程和主程序运行在不同的栈上,所以回溯没有跟踪到对函数resume的调用。当协程引发错误时并不会进行栈展开,这就意味着可以在错误发生后检査错误。继续上面的示例,如果再次唤醒协程,它会提示引起了一个错误:

print(coroutine.resume(co))
	--> false   test.lua:347: some error

现在,如果输出栈回溯,会得到这样的结果:

stack traceback:
        [C]: in function 'error'
        test.lua:347: in function

即使在错误发生后,也可以检查协程中的局部变量:

print(debug.getlocal(co, 1, 1))				--> x			10

钩子

调试库中的钩子机制允许用户注册一个钩子函数,这个钩子函数会在程序运行中某个特定事件发生时被调用。有四种事件能够触发一个钩子:

  • 每当调用一个函数时产生的call事件
  • 每当函数返回时产生的return事件
  • 每当开始执行一行新代码时产生的line事件line
  • 执行完指定数量的指令后产生的count事件。(这里的指令指的是内部操作码)

Lua语言用一个描述导致钩子函数被调用的事件的字符串为参数来调用钩子函数,包括"caII"(或"tailcall")、“return”、“line"或"count”。对于line事件来说,还有第二个参数,即新行号。我们可以在钩子函数内部调用函数debug.getinfo来获取更多的信息。

要注册一个钩子,需要用两个或三个参数来调用函数debug.sethook:第一个参数是钩子函数,第二个参数是描述要监控事件的掩码字符串,第三个参数是一个用于描述以何种频度获取count事件的可选数字。如果要监控call、return和line事件,那么需要把这几个事件的首字母(c、r或I)放入掩码字符串。如果要监控count事件,则只需要在第三个参数中指定一个计数器。如果要关闭钩子,只需不带任何参数地调用函数sethook即可。

作为一个简单的示例,以下代码安装了一个简单的跟踪器,它会输出解释器执行的每一行代码:

debug.sethook(print, "l")

这句调用只是简单地把函数print安装为一个钩子函数,并告诉Lua语言在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时,函数返回。这种标准的实现十分简单,并且在全局环境中运行命令,位于正在被调试代码的定界范围之外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZY-JIMMY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值