虽然 Lua C API 中提供了弱引用,但 Lua 语言本身中没有提供标准支持。本说明针对 Lua 中的弱引用提出一个接口,描述一个实现,并提供一些实际使用示例:表对象的 safe 析构函数事件和对象缓存。
-- creation ref = weakref(obj) -- dereference obj = ref()也就是说,使用名为“weakref”的新全局函数来创建对对象的弱引用。可以使用函数调用运算符取消引用弱引用。返回 nil 的取消引用表示对象已被垃圾回收。由于 nil 具有此特殊含义,因此不允许对 nil 对象本身进行弱引用。
我们的 weakref 函数需要调用 lua_ref() 并返回一个保存结果引用 ID 的对象。取消引用(使用引用对象上的函数调用标记方法实现)只需调用 lua_getref()。最后,当引用对象本身被回收时,有必要释放引用,因此使用垃圾回收 (gc) 标记方法来调用 lua_unref()。
用户数据类型是引用对象的自然选择,因为它是唯一提供 gc 事件的类型。此外,由于只需要一个整数,因此可以将状态存储为用户数据指针本身,从而消除了动态内存分配。
此实现的源代码作为官方 Lua 4.0 发行版的补丁提供,可在此处获取。使用补丁实用程序按如下方式应用
cd <lua distrubution directory> patch -p1 < weakrefs.patch该补丁包括测试目录“weakref.lua”的新增内容,其中显示了该扩展的一个简单示例。
建议将此实现添加到 Lua 的“baselib”标准库中,原因有以下几点:弱引用具有普遍的实用性;该实现简单,并且已得到 C API 的支持;并且由于只需要一个新的 Lua 函数,因此为其目的创建单独的库将是多余的。
但是,在某些情况下,对象会拥有未自动释放的某些系统资源,例如文件句柄、图形缓冲区等。一个有点繁琐的解决方案是用 C 从 userdata 类型实现此类对象,该类型确实具有 gc 事件。弱引用为此提供了一种优雅的替代方案,允许从 Lua 的舒适环境中对表对象进行安全的垃圾回收事件。
该实现使用包含弱引用/析构函数对的表。当引用的对象被回收时,将调用相应的析构函数。这些析构函数是安全的,因为它们无法访问正在销毁的对象。析构函数所需的任何信息(例如资源句柄)都必须独立于对象访问。这对于 Lua 来说是一项相当轻松的工作,这要归功于一流函数对象。
需要一个小型接口来管理表,该接口包含一个将析构函数绑定到对象的功能和一个检查已回收对象的功能。以下是 Lua 中的实现
------------------------------------------ -- Destructor manager local destructor_table = { } function RegisterDestructor(obj, destructor) %destructor_table[weakref(obj)] = destructor end function CheckDestructors() local delete_list = { } for objref, destructor in %destructor_table do if not objref() then destructor() tinsert(delete_list, objref) end end for i = 1, getn(delete_list) do %destructor_table[delete_list[i]] = nil end end与其在某个时间间隔手动调用 CheckDestructors(),自然而然的想法是将其链接到 Lua 的垃圾回收周期。Lua 虚拟机通过在周期结束时调用 nil 类型的 gc 标记方法来支持此操作。
作为安全析构函数使用的示例,考虑一个用于将程序消息记录到文件中的对象。当对象被垃圾回收时,我们希望日志文件被关闭。(此示例很琐碎,因为文件句柄在程序终止时关闭。但是,该方法很容易应用于其他类型的资源。)
------------------------------------------ -- example object using safe destructor function make_logobj(filename) local id = openfile(filename, "w") assert(id) local obj = { file = id, write = function(self, message) write(self.file, message) end, } local destructor = function() closefile(%id) end RegisterDestructor(obj, destructor) return obj end
一种补救措施是仅缓存最近访问的n页,但由于未考虑数据大小,这不会充分利用可用内存。一种改进方法是缓存最近访问的x千字节生成数据。除了增加程序复杂性之外,这里出现的问题是找到一个合适的x值。这类似于垃圾收集器面临的问题:应该多久进行一次循环以及在使用多少内存后进行循环?
通过对缓存使用弱引用,在将内存使用问题留给垃圾收集器的情况下,程序复杂性保持较低。缓存表不是存储生成的页面对象,而是包含对这些对象的弱引用。当垃圾回收周期发生时,当前未使用的页面对象将被回收。
这里有一个实现,假设一个函数 GeneratePage() 给定其“名称”来生成一个页面对象。需要 CleanCache() 函数来删除已收集对象的表项,该函数又应该链接到 Lua 的 gc 周期。
------------------------------------------ -- Page cache local cache_table = { } function GetPage(name) local ref = %cache_table[name] local obj = ref and ref() if not obj then obj = GeneratePage(name) %cache_table[name] = weakref(obj) end return obj end function CleanCache() local delete_list = { } for name, ref in %cache_table do if not ref() then tinsert(delete_list, name) end end for i = 1, getn(delete_list) do %cache_table[delete_list[i]] = nil end end