是否需要将对象设置为“Nothing”?

80

我经常听说建议在使用完对象后将其设置为`Nothing`。但我通常只在表单内的函数中使用它们。

无论是否将对象设置为`Nothing`,当函数作用域结束时,引用都会丢失并释放内存,对吗?

也就是说,真的有必要这样做吗:

Set db = Nothing
Set record_set = Nothing

我意识到这个问题比建议的重复问题早了四年。然而,这个问题的被接受答案包含一个重要的缺陷,即它提到了VBA/VB6中的“垃圾回收器”。与VB.NET不同,在这些语言中没有垃圾回收器,对象会在超出作用域时立即被销毁,而这[会产生可观察的差异](请参见末尾的With块)。因此,我将这个问题标记为重复,而不是另一种方式。 - GSerg
1
@GSerg 对我来说很有道理。但是重复目标是专门针对Excel的措辞…这并不是非常相关,但是它使问题似乎不太通用。我们能否编辑它以使它成为一个更通用的规范问题? - StayOnTarget
8个回答

93

VB使用一种叫做“引用计数”的机制来确定一个对象是否可以被销毁。

当对象引用存储在变量中,例如使用Set关键字时,该对象的引用计数会增加。当变量超出范围时,引用计数将减少。

当计数器达到零时,对象就可以被销毁了。对象资源将在此时释放。

函数局部变量很可能引用一个引用计数不会超过1的对象,因此当函数结束时,对象资源将被释放。

当您将对象传递给其他函数或存储在寿命更长的对象中时,引用计数将超过1。

将变量设置为Nothing是显式减少引用计数的方法。

例如,您读取一个文件,并在file.ReadAll()调用后立即将文件对象变量设置为Nothing。文件句柄将立即释放,您可以花费时间处理内容。

如果您不设置为Nothing,则文件句柄可能比绝对必要的时间开放更久。

如果你不是处于“必须解除有价值资源”的情况下,让变量超出作用域是可以的。


21
虽然你写的一切都是真实的(而且说得很好),但问题被标记为MS Access,这意味着使用VBA。在Access中,使用VBA历史上存在更新引用计数不正确的问题,因此建议作为惯例明确清理对象变量。 - David-W-Fenton
6
就此而言,我不知道VBA和VB 6.0之间有什么值得一提的区别。我无法相信他们为了MS Access写了一个新的垃圾收集器和一个新的VB运行时。 - Tomalak
5
这篇知识库文章并没有提到 MS Access 中有不同的垃圾回收器。它所指的是 DAO 中的一种特殊情况,或者说是 Access 和 DAO 紧密连接所带来的一种独特现象,只有在将 Access 用作自动化服务器时才会显现出来。 - Tomalak
4
有没有权威的资料证实这一点?我确实相信,垃圾回收系统可能存在错误,导致它不能正确地识别一个对象是否适合进行垃圾回收。但问题是,VB中是否存在一个bug,使得变量离开作用域后没有适当地减少使用计数,但将变量设置为“nothing”可以适当地减少使用计数。我非常怀疑编程方法论中的一种方法: “好吧,编译器可能会生成不正确的代码,所以我要写一堆额外的代码以防万一。”这些代码应该放在哪里... - Jay
2
@Tomalak 我并不是在反驳你,而是在反驳这个帖子中的其他人。我的观点关于 y=x*3 并不涉及原始类型与对象之间的区别,而只是关于一些人在这里表达的想法,即你应该编写额外的代码以防编译器和/或运行时环境无法正常工作。如果你不信任垃圾回收,为什么要相信算术解析?就像我说的,如果有人能引用具体信息证明编译器存在需要我们编写代码来规避的错误,那好吧。如果没有,那么为了“以防万一”编写代码似乎对我来说非常低效。 - Jay
显示剩余10条评论

16

垃圾回收很少是完美的。即使在.NET中,有时您也强烈建议及早提示系统执行垃圾回收。

出于这个原因,我在完成记录集时明确地关闭并将其设置为


4
您的意思是.NET垃圾回收在设计上并不总是最优的,还是存在设计缺陷?您是否有任何参考资料可以解释在哪些情况下建议提前进行回收?谢谢。 - Brandon Moore
2
有一天,一位学生来找Moon说:“我知道如何制作更好的垃圾收集器。我们必须保持对每个cons指针的引用计数。” Moon耐心地告诉学生以下故事:“有一天,一位学生来找Moon说:‘我知道如何制作更好的垃圾收集器... - BIBD
我并不是说垃圾回收器永远不需要程序员干预,只是它并不完美,而且清理需要一定时间。当你不再需要引用时,明确地将其清除可以帮助垃圾回收器。 - BIBD
感谢澄清。你显然是正确的,尽管有人可能会争辩微观优化并非总是值得的。 - Brandon Moore
没错,我不会回头去审核/修复一整个有 100K 条链接的应用。但如果我在编写新代码或者本来就要修复其他东西,我会出于习惯这么做。 - BIBD
编写手动调用Close并将所有内容设置为Nothing的代码正确非常繁琐且容易出错。我敢打赌,你提到的代码只是在方法或循环结束时有这些行,而实际上当方法中先发生异常时,这些行甚至没有被调用。即使在方法中只有一个对象,这种情况也经常发生;当有10个对象,每个对象都应该在正确的时刻手动销毁,无论异常和执行流如何,它都变得完全不可管理。 - GSerg

15

在Microsoft DAO帮助和Access开发人员参考手册中,"Recordset.Close"的帮助主题的最后一行是这样的:

"关闭方法的替代方法是将对象变量的值设置为Nothing (Set dbsTemp = Nothing)。"

http://msdn.microsoft.com/en-us/library/bb243098.aspx

考虑到这一点,微软知识库中的本篇文章“如何在使用数据访问对象(DAO)后防止数据库膨胀”告诉你,如果不想让数据库膨胀,就应该明确地关闭。你会注意到,该文章对细节有些含糊不清;“原因”部分几乎是无意义的。

http://support.microsoft.com/kb/289562

症状:使用数据访问对象(DAO)打开记录集后,Microsoft Access数据库开始膨胀(或快速增长)。
原因:如果您在循环遍历记录集代码时不释放记录集的内存,则DAO可能会重新编译,使用更多内存并增加数据库的大小。
更多信息:当您在代码中创建Recordset(或QueryDef)对象时,请在完成后明确关闭该对象。 Microsoft Access在大多数情况下会自动关闭Recordset和QueryDef对象。但是,如果您在代码中明确关闭对象,则可以避免偶尔出现对象仍然保持打开状态的情况。
最后,我想补充一点,我已经使用Access数据库工作了15年,几乎总是让我的本地声明的记录集变量超出范围而不显式使用Close方法。我没有对此进行任何测试,但似乎并不重要。

4
当您使用ASP经典版(服务器端脚本)时,重要的是在完成对象使用后将所有对象设置为null,因为它们不会在[虚拟]服务器关闭之前超出范围。
因此,所有MS VB脚本示例都显示了对象被关闭并设置为null。这样,脚本摘录可以在像对象不超出范围的ASP经典版等环境中使用。
极少数情况下,您希望编写长时间运行的进程,在这种情况下,对象不会超出范围,如果不明确释放对象,则会耗尽物理内存。
如果您发现自己正在编写ASP经典版或以其他某种原因在全局范围内运行进程,则应明确释放对象。

3

当变量超出范围时,引用应该被清理。随着软件的更新,这一点可能已经得到了改善,但曾经是不可靠的。我认为明确将变量设置为 "Nothing" 仍然是一个好习惯。


2

通常我会在我的过程的结尾处加上这个代码,或者在使用模块级别的记录集时调用一个名为“CloseRecordSet”的子程序:

Private Sub Rawr()
On Error GoTo ErrorHandler

    'Procedural Code Here.

    ExitPoint:
        'Closes and Destroys RecordSet Objects.
        If Not Recset Is Nothing Then
            If Recset.State = 1 Then
                Recset.Close
                Conn.Close
            End If
            Set Recset = Nothing
            Set Conn = Nothing
        End If
        Exit Sub

    ErrorHandler:
        'Error Handling / Reporting Here.
        Resume ExitPoint
End Sub

无论过程以何种方式结束(无论是正常结束还是由于错误而结束),都可以通过这种方式清理对象并释放资源。
这样做相当安全,因为您只需将其插入即可,它只会执行有关关闭或销毁记录集/连接对象的必要操作,以防它已经关闭(由于运行时错误或提前关闭,这只是确保)。
这真的不费什么事,最好在完成对象后立即清理它们以立即释放资源,无论程序中发生了什么。

这是ADO代码,对吧?ADO记录集缺少State属性,并且您不使用连接对象。ADO没有DAO的引用计数问题,因此您无需进行清理。无论如何,在Access应用程序中不应该使用太多ADO -- 除了少数几件事情,DAO是首选的数据访问库,除非是在ADP之外,ADO才更好。 - David-W-Fenton
它被声明为ADODB.Recordset,并且具有状态属性,用于定义它当前是否打开。基本上,它会检查它是否已经设置为Nothing,如果没有,则首先使用状态属性检查它是否仍处于打开状态(如果没有,则关闭它),然后在之后将其设置为Nothing。这完全确保其被完全和干净地关闭,并且可以在过程中的任何时间使用Recordset,无论其是否已经打开,或者是nothing还是not。 - BobT
1
我的观点是,从VBA使用的ADO没有DAO所具有的任何引用问题。你正在清理VBA会可靠地为你清理的东西。当然,这是假定在第一次使用ADO时有某些正当理由的情况下。而很多情况下并没有这样的正当理由。 - David-W-Fenton

1

Eric Lippert(长期担任微软编程语言设计师)在2004年就此问题写过文章。这篇文章提到了 VBScript、VBA 和 VB6 混合在一起使用,他的结论似乎适用于所有这些语言。

简而言之: 他得出以下结论:

  • 语言或编译器/解释器中没有缺陷。通常情况下,在 VB✶ 语言中不需要显式设置对象为“nothing”。
  • 但是在程序逻辑复杂的特定情况下,可能需要这样做。程序员已经将这种实践泛化了。

以下是关键点的摘要。

首先,他引用了一些听起来合理但不正确的理由:

解释#1:虚假)也许某个早期版本的VB需要这样做。

...

解释#2:虚假)VB6垃圾回收器不清除循环引用。

两者都是不正确的假设。

接下来,对于为什么一些程序员可能过度采用这种做法的演绎推理:

解释#3:及早丢弃昂贵的资源是一个好主意。也许人们过度概括了这个规则?

...

我可以看出如何过度应用这个良好的设计原则会导致这种编程实践。... 尽管如此,我仍然不相信这就是整个故事。

在我看来,最有说服力的论点是最后一个,它引用了一个已知的ADO问题,实际上需要明确的对象清理:

解释 #4:……在变量超出范围之前自己清除变量和让作用域终结器为您清除变量之间存在区别。

……

如果两个对象有一些复杂的交互,并且其中一个对象存在错误,必须在另一个对象之前关闭它,那么作用域终结器可能会选择错误的对象!

……

唯一解决错误的方法是在变量超出范围之前以正确的顺序显式清理对象。

事实上,有广泛使用的 ADO 对象存在这种错误。谜团得到了解决。

他接着提到,即使是微软的文档也可能鼓励使用 Set = Nothing 的做法,这影响了许多程序员,他们可能不知道这种做法的真正原始需求。

他也正确地批评了事情发展到了这个地步:

对我来说真正奇怪的是这种编码实践是如此顽固。好吧,有些对象存在缺陷,有时你可以通过编写一些本来不必要的代码来解决bug。逻辑上的结论是“总是编写不必要的代码,以防将来发生某些bug”吗?有些人称之为“防御性编程”,我称之为“大量过度概括”。

-2

试试这个

If Not IsEmpty(vMyVariant) Then
    Erase vMyVariant
    vMyVariant = Empty
End If

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接