垃圾收集器不能猜测我们认为那些是垃圾。一个典型的例子就是栈,栈通常由一个数组和一个指向栈顶的索引实现。我们知道,数组的有效部分总是向顶部扩展的,但 Lua
语言却不知道。如果弹出一个元素时只是简单的递减听不索引,那么这个仍然留在数组中的对象对于 Lua
语言来说并不是垃圾。同理,即使是程序不会再用到的、存储在全局变量中的对象,对于 Lua
语言来说也不是垃圾。在这两种情况下,都需要我们的代码将这些对象所在的位置赋值为 nil
,以便这些位置不会锁定可释放的对象。
不过,简单地清除引用可能还不够。在有些情况下,还需要程序和垃圾收集器之间的协作。一个典型的例子是:当我们要保存某些类型(例如:文件)的活跃对象的列表时。这个需求看上去很简单,我们只需要把每个对象插入数组即可;但是,一旦一个对象成为了数组的一部分,它就再也无法被回收了!虽然已经没有任何地方在引用它,但数组依然在引用它。除非我们告诉 Lua
语言数组对该对象的引用不应该阻碍对此对象的回收,否则 Lua
语言本身是无从知晓的。
弱引用表就是这样一种用来专门告诉 Lua
语言一个引用不应该阻止对一个对象回收的机制。所谓弱引用是一种不在垃圾收集器考虑范围内的对象引用。如果对一个对象的所有引用都是弱引用,那么垃圾收集器将会回收这个对象并删除这些弱引用。 Lua
语言通过弱引用表实现弱引用,弱引用表就是元素均为弱引用的表,这意味着如果一个对象只被一个弱引用表持有,那么 Lua
语言最终会回收这个对象。
表由键值对组成,其两者都可以容纳任意类型的对象。在正常情况下,垃圾收集器不会回收一个在可访问的表中作为键或值的对象。也就是说,键和值都是强引用,它们会阻止对其所指向对象的回收。在一个弱引用表中,键和值都可以是弱引用的。这就意味着有三种类型的弱引用表,即具有弱引用键的表、具有弱引用值的表及同时具有弱引用键和值的表。不论是那种类型的弱引用表,只要有一个键或值被回收了,那么对应的整个键值对都会被从表中删除。
一个表是否为弱引用表是有其元表中的 __mode
字段所决定的。当这个字段存在时,其值应为一个字符串:
- 如果这个字符串是
"k"
,那么这个表的键是弱引用的。 - 如果这个字符串是
"v"
,那么这个表的值是弱引用的。 - 如果这个字符串是
"kv"
,那么这个表的键和值都是弱引用的。
下面这个示例虽然有些刻意,但演示了弱引用表的基本行为:
a = {} mt = {__mode = "k"} setmetatable(a, mt) -- 现在'a'的键是弱引用的了 key = {} -- 创建第一个键 a[key] = 1 key = {} -- 创建第二个键 a[key] = 2 collectgarbage() -- 强制进行垃圾回收 for k, v in pairs(a) do print(v) end --> 2
在本例中,第二句赋值 key = {}
覆盖了指向第一个键的索引。调用 collectgarbage
强制垃圾收集器进行一次完整的垃圾收集。由于已经没有指向第一个键的其他引用,因此 Lua
语言会回收这个键并从表中删除对应的元素。然而,由于第二个键仍然被变量 key
所引用,因此 Lua
不会回收它。
注意
只有对象可以从弱引用表中被移除,而像数字和布尔这样的"值"是不可回收的。例如,我们在表 a
中插入一个数值类型的键,那么垃圾收集器永远不会回收它。当然,如果在一个值为弱引用的弱引用表中,一个数值类型键相关联的值被回收了,那么整个元素都会从这个弱引用表中被删除。
字符串在这里表现了一些细微的差别,虽然从实现的角度看字符串是可回收的,但字符串又与其他的可回收对象不同。其他的对象,例如表和闭包,都是被显式创建的。例如,当 Lua
语言对表达式 {}
求值时会创建一个新表。然而,当对表达式 "a".."b"
求值时, Lua
语言会创建一个新字符串吗?如果当前系统中已有了一个字符串 "ab"
会怎么样? Lua
语言会创建一个新的字符串吗?编译器会在运行程序前先创建这个字符串吗?其实,这些都无关紧要,因为他们都是实现上的细节。从程序员的角度看,字符串是值而不是对象。所以,字符串就像数值和布尔值一样,对于一个字符串类型的键来说,除非它对应的值被回收,否则是不会从弱引用表中被移除的。