析构器——Finalizer

本文涉及的产品
图片翻译,图片翻译 100张
语种识别,语种识别 100万字符
文本翻译,文本翻译 100万字符
简介: 析构器——Finalizer

虽然垃圾收集器的目标是回收对象,但是它也可以帮助程序员来释放外部资源。出于这种目的,几种编程语言提供了析构器。析构器是一个与对象关联的函数,当该对象即将被回收时该函数会被调用。


Lua 语言通过元方法 __gc 实现析构器,如下所示:

o = {x = "hi"}
setmetatable(o, {
  __gc = function (o)
    print(o.x)
  end
})
o = nil
collectgarbage()    --> hi


在上例中,我们首先创建一个带有 __gc 元方法的元表的表。然后,抹去与这个表的唯一联系(全局变量),在强制进行一次完整的垃圾回收。在垃圾回收期间, Lua 语言发现表已经不再是可访问的了,因此调用表的析构器,也就是元方法 __gc


Lua 语言中,析构器的一个微妙之处在于“将一个对象标记为需要析构”的概念。通过给对象设置一个具有非空 __gc 元方法的元表,就可以把一个对象标记为需要进行析构处理。如果不标记对象,那么对象就不会被析构。我们编写的大多数代码会正常运行,但会发生某些奇怪的行为,比如:

o = {x = "hi"}
mt = {}
setmetatable(o, mt)
mt.__gc = function (o) print(o.x) end
o = nil
collectgarbage()    --> (print nothing)


这里,我们确实给对象 o 设置了元表,但是这个元表没有 _gc 元方法,因此对象没有被标记为需要进行析构处理。即使我们后续给元表增加了元方法 __gcLua 语言也发现不了这种赋值的特殊之处,因此不会把对象标记为需要进行析构处理。


正如我们所提到的,这很少会有问题。在设置元表后,很少会改变元方法。如果真的需要在后续设置元方法,那么可以给字段 __gc 先赋一个任意值作为占位符:

o = {x = "hi"}
mt = {__gc = true}
setmetatable(o, mt)
mt.__gc = function (o) print(o.x) end
o = nil
collectgarbage()    --> hi


现在,由于元表有了 __gc 字段,因此对象会被正确地标记为需要析构器处理。如果后续再设置元方法也不会有问题,只要元方法是一个正确的函数, Lua 语言就能够调用它。


当垃圾收集器在同一个周期中析构多个对象时,它会按照对象被标记为需要析构处理的顺序逆序调用这些对象的析构器。请考虑如下示例,该示例创建了一个由带有析构器的对象所组成的链表:

mt = {__gc = function (o) print(o[1]) end}
list = nil
for i = 1, 3 do
  list = setmetatable({i, link = list}, mt)
end
list = nil
collectgarbage()
--> 3
--> 2
--> 1


第一个被析构的对象是 3 ,也就是最后一个被标记的对象。


一种常见的误解是认为正在被回收的对象之间的关联会影响对象析构的顺序。例如,有些人可能认为上例的对象 2 必须在对象 1 之前被析构,因为存在从 21 的关联。但是,关联会形成环。所以,关联并不会影响析构器执行的顺序。


有关析构器的另一个微妙之处是复苏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


B 的析构器访问了 A ,因此 AB 析构前不能被回收, Lua 语言在运行析构器之前必须同时复苏 BA


由于复苏的存在, Lua 语言会在两个阶段中回收具有析构器的对象。当垃圾收集器首次发现某个具有析构器的对象不可达时,垃圾收集器就把这个对象复苏并将其放入等待被析构的队列中。一旦析构器开始执行, Lua 语言就将该对象标记为已被析构。当下一次垃圾收集又发现这个对象不可达时,它就将这个对象删除。如果想保证我们程序中的所有垃圾都被真正释放了的话,那么必须调用 collectgarbage 两次,第二次调用才会删除第一次调用中被析构的对象。


由于 Lua 语言在被析构对象上设置的标记,每一个对象的析构器都会精确地运行一次。如果一个对象知道程序运行结束还没有被回收,那么 Lua 语言就会在整个 Lua 虚拟机关闭后调用它的析构器。这种特性在 Lua 语言中实现了某种形式的 atexit 函数,即在程序终结前立即运行的函数。我们所要做的就是创建一个带有析构器的表,然后把它锚定在某处,例如锚定到全局表中:

local t = {__gc = function ()
  -- 'atexit' 的代码位于此
  print("finishing lua program")
end}
setmetatable(t, t)
_G["*AA*"] = t


另外一个有趣的技巧会允许程序在每次完成垃圾回收后调用指定的函数。由于析构器只运行一次,所以这种技巧是让每个析构器创建一个用来运行下一个析构器的新对象,如下所示:

do
  local mt = {__gc = function (o)
    -- 要做的工作
    print("new cycle")
    -- 为下一次垃圾收集创建对象
    setmetatable({}, getmetatable(o))
  end}
  -- 创建第一个对象
  setmetatable({}, mt)
end
collectgarbage()    --> 一次垃圾收集
collectgarbage()    --> 一次垃圾收集
collectgarbage()    --> 一次垃圾收集


具有析构器的对象和弱引用表之间的交互也有些微妙。在每个垃圾收集周期内,垃圾收集器会在调用析构器前清理弱引用表中的值,在调用析构器之后在清理键。这种行为的原理在与我们经常使用带有弱引用键的表来保存对象的属性(详见:对象属性)。因此,析构器可能需要访问那些属性。不过,我们也会使用具有弱引用值的表来重用活跃的对象,在这种情况下,正在被析构的对象就不再有用了。

目录
相关文章
|
5月前
|
编译器 数据库连接 C++
31析构函数
31析构函数
26 0
|
11月前
|
C++
析构函数
析构函数是一种特殊的函数,用于在对象的生命周期结束时清理资源。它与构造函数相反,通常用于释放在对象创建期间分配的资源(如内存、文件句柄等)。析构函数的名称通常以一个波浪线(~)开头,如 ~MyClass()。
78 6
|
4月前
|
C++ 开发者
什么是析构函数?
正确地使用析构函数是C++资源管理的关键。开发者应当确保所有资源在不再需要时能够被及时和正确地释放。通过合理设计析构函数,可以大大增强程序的稳定性和效率。希望本文的介绍能帮助你更好地理解和使用C++中的析构函数,写出更健壮、更可靠的代码。
38 0
|
4月前
|
编译器 C语言 C++
【C++】:构造函数和析构函数
【C++】:构造函数和析构函数
40 0
|
5月前
|
C++
C++程序中的析构函数
C++程序中的析构函数
30 2
|
5月前
|
编译器 C++
【c++】构造函数和析构函数
【c++】构造函数和析构函数
【c++】构造函数和析构函数
|
5月前
|
C++
C++中的 虚析构 与 纯虚析构
C++中的 虚析构 与 纯虚析构
|
5月前
this函数和析构函数
this函数和析构函数
24 0
|
5月前
|
存储 编译器 Linux
构造函数与析构函数的问题总结
构造函数与析构函数的问题总结
62 0
【C++】 --- 虚析构和纯虚析构
【C++】 --- 虚析构和纯虚析构
44 0
【C++】 --- 虚析构和纯虚析构