VBA在If语句中选择错误的分支 - 严重的编译器错误?
VBA在If语句中选择错误的分支 - 严重的编译器错误?
标题中的问号只是因为我非常不愿称任何问题为编译器错误,但在这种情况下,如果有人能以其他方式解释这种行为,我会感到惊讶。
复现问题的代码非常简单。在一个标准模块中,我们有以下内容:
Sub CompilerBug() Dim oClass As cClass Set oClass = New cClass If False Then Debug.Print "This doesn't print, as it shouldn't." End If If Falsee(oClass.Clone) Then Debug.Print "This does print, although it shouldn't!" End If End Sub Public Function Falsee(oClass As cClass) As Boolean Falsee = False End Function
并且我们有一个类(`cClass`)在名为`cClass`的类模块中定义,其中包含以下代码:
Public Function Clone() As cClass Dim oClass As cClass Set oClass = New cClass Set Clone = oClass End Function Private Sub Class_Terminate() End Sub
代码很容易理解。尽管名为`Falsee`的函数返回`False`,但第二个if语句仍会被执行!当类中的函数被替换为类似的`Public Property Get`时,也会得到相同的结果。
为了复现问题,我在我的Office 365 Excel中(64位,版本2011,构建13426.20274)获得了这种行为,这是excel的最新版本。我还使用完全相同的代码在我的Word VBA IDE中进行了测试,结果也是一样的。
"证明":
我不知道是什么导致了这种行为,但这里有一些线索:
如果我们在子过程中将代码改写为:
Sub CompilerBug() Dim oClass As cClass Set oClass = New cClass Dim bFalse As Boolean bFalse = Falsee(oClass.Clone) If bFalse Then Debug.Print "This doesn't print, as it shouldn't." End If End Sub
(为简洁起见,省略了第一个if语句。)代码将按预期执行,因此在if语句的条件中直接调用函数非常重要(这通常不会有差异)。
下一个有趣的线索是(假设我们再次使用带有`If Falsee(oClass.Clone)`的有错误的子过程代码):
如果我们从类模块中删除以下内容:
Private Sub Class_Terminate() End Sub
if语句将按预期工作,不会输出任何内容!因此,在if语句的评估过程中执行终止事件会导致问题,但`Class_Terminate()`子过程甚至不包含任何代码!这是下一个不应该有差异但确实有差异的事情!
这个想法得到了进一步的支持,当我们在模块中通过在顶部添加`Public poClass As cClass`来声明一个公共变量,并将函数代码改写为:
Public Function Falsee(oClass As cClass) As Boolean Set poClass = oClass Falsee = False End Function
现在在执行if语句期间将不会调用终止事件,因为类的实例在if语句执行期间不会超出范围,结果if语句会正确评估 - 该行不会被打印。
显然,终止事件在if语句的评估过程中不能解释全部情况,因为这种情况经常发生。它似乎与被终止的对象的范围以及参数传递给函数的方式有关。例如,以下情况不会产生相同的行为:
模块代码:
Sub CompilerBug() Dim oClass As cClass Set oClass = New cClass If Falsee(oClass.CreateAndDestroyObject) Then Debug.Print "This doesn't print, as it shouldn't." End If End Sub Public Function Falsee(lng As Variant) As Boolean Falsee = False End Function
类模块中的代码:
Public Function CreateAndDestroyObject() As Long Dim oClass2 As cClass Set oClass2 = New cClass Set oClass2 = Nothing End Function Private Sub Class_Terminate() End Sub
总结一切,当满足以下条件时,会出现这种行为:
类的方法返回同一类的实例,并且该方法在if语句的条件中作为函数的参数进行调用,而且该类的实例(由该方法创建)在该函数中超出了范围,并且类的终止事件被调用(并且作为代码存在)。在这种情况下,无论函数的返回值如何,if语句都会被执行。
对我来说,仍然有许多问题...为什么终止事件在这种情况下会产生差异?我的任何代码是否应该产生未定义的行为,还是这实际上是一个错误?是否存在其他情况下if语句的工作方式与预期不同?到底是什么导致了这个错误?
这个问题似乎在32位版本的Excel中不存在。
注意:
本文描述的错误已在2019年11月修复。类似的错误变体仍然存在,请参见此处了解更多信息。
VBA在If语句中选择了错误的分支 - 严重的编译器错误?
这是VBA7 64位版本的一个bug。下面是一个更简单的例子:
SomeClass.cls:
Private Sub Class_Terminate() End Sub
Main.bas:
Function ReturnFalse(o As Object) As Boolean ReturnFalse = False End Function Sub test() Debug.Print ReturnFalse(New SomeClass) If ReturnFalse(New SomeClass) Then Debug.Print "True" Else Debug.Print "False" End If End Sub
在32位的VBA上打印:
False False
在64位的VBA上打印:
False True
我在一个将近三年前的用户反馈中找到了这个错误的报告,链接在这里:https://excel.uservoice.com/forums/304921-excel-for-windows-desktop-application/suggestions/35735881-fix-inlined-member-calls-on-user-objects-on-64-bi,但是自那以后还没有任何反应。而且,显然没有用户可访问的“真正”方法向微软报告VBA的bug。这一切都非常令人沮丧,因为我们有几个VBA项目由于内存需求的问题急需迁移到64位。
问题出现的原因:
这个问题在32位系统中不存在,但在64位VBA应用程序中存在(已经尝试过Excel、Word和AutoCAD)。
由于问题已经涵盖了如果对象未被终止或没有Class_Terminate事件会发生什么,下面的示例都使用了一个肯定会超出范围的对象,并且我们也假设有一个被调用的Class_Terminate事件。
解决方法:
Option Explicit #If Win64 Then Sub Bug() ' We don't really need a Clone method to reproduce the bug If Falsee(New cClass) Then Debug.Print "This does print, although it shouldn't!" End If ' If we add a logical operator and a second method call then the bug disappears: If Falsee(New cClass) Or Falsee(New cClass) Then Debug.Print "This doesn't print, as it shouldn't." End If ' It could be any other method. The order of the methods also doesn't matter If Falsee(New cClass) Or Sin(0) Then Debug.Print "This doesn't print, as it shouldn't." End If ' The above workaround does not work if we simply use a boolean value after the method call If Falsee(New cClass) Or False Then Debug.Print "This does print, although it shouldn't!" End If ' But it does work if we add the boolean before the method call: If False Or Falsee(New cClass) Then Debug.Print "This doesn't print, as it shouldn't." End If If True And Falsee(New cClass) Then Debug.Print "This doesn't print, as it shouldn't." End If End Sub Function Falsee(oClass As cClass) As Boolean Falsee = False End Function #End If
有趣的是,这使问题变得更糟!而且,如果让函数返回其他值,例如一个字符串,并在比较中使用它,问题也会出现:在函数内部:Falsee = "Falsee"
,在if语句中:If Falsee(New cClass) = "True" Then
-> if语句将被执行。
注意:虽然非解决方案通常不被接受为答案,但如果一个问题尚未被解决,提供额外信息或进一步隔离或定义问题的部分答案是可以接受的,如此例。帮助我们通过研究问题找到解决方案,然后将你的研究结果和任何其他你尝试过的东西作为部分答案贡献出来。这样,即使我们无法解决问题,下一个人也有更多的线索。