VBA在If语句中选择错误的分支 - 严重的编译器错误?

11 浏览
0 Comments

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中进行了测试,结果也是一样的。

"证明":

"Proof"

我不知道是什么导致了这种行为,但这里有一些线索:

如果我们在子过程中将代码改写为:

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月修复。类似的错误变体仍然存在,请参见此处了解更多信息。

0
0 Comments

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位。

0
0 Comments

问题出现的原因:

这个问题在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语句将被执行。

注意:虽然非解决方案通常不被接受为答案,但如果一个问题尚未被解决,提供额外信息或进一步隔离或定义问题的部分答案是可以接受的,如此例。帮助我们通过研究问题找到解决方案,然后将你的研究结果和任何其他你尝试过的东西作为部分答案贡献出来。这样,即使我们无法解决问题,下一个人也有更多的线索。

0