变量迭代自身 - 不同类型的不同行为

时间:2017-02-20 21:08:24

标签: vba for-loop vb6 language-lawyer variant

请查看帖子末尾的最新动态。

特别是,请参阅更新4:变体比较诅咒

我已经看到配偶撞到墙上以了解变体是如何工作的,但从未想过我会有自己的坏时刻。

我已成功使用以下VBA构造:

For i = 1 to i

i整数或任何数字类型时,这非常有效,从1迭代到i原始值。我在iByVal参数的情况下执行此操作 - 您可能会说懒惰 - 以免自己声明新变量。

然后当这个构造“停止”按预期工作时,我遇到了一个错误。经过一些艰难的调试后,我发现当i未声明为显式数字类型而是Variant时,它的工作方式不同。问题有两个:

1- ForFor Each循环的确切语义是什么?我的意思是编译器执行的操作顺序是什么,顺序是什么?例如,限制的评估是否在计数器的初始化之前?在循环开始之前,这个限制是否被复制并“修复”了?等等。同样的问题适用于For Each

2-如何解释变体和显式数字类型的不同结果?有人说变量是一个(不可变的)引用类型,这个定义可以解释观察到的行为吗?

我为涉及ForFor Each语句的不同(独立)方案准备了MCVE,并结合了整数,变体和对象。令人惊讶的结果促使明确定义 语义,或者至少检查这些结果是否符合定义的语义。

欢迎所有见解,包括解释一些令人惊讶的结果或其矛盾的部分见解。

感谢。

Sub testForLoops()
    Dim i As Integer, v As Variant, vv As Variant, obj As Object, rng As Range

    Debug.Print vbCrLf & "Case1 i --> i    ",
    i = 4
    For i = 1 To i
        Debug.Print i,      ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case2 i --> v    ",
    v = 4
    For i = 1 To v  ' (same if you use a variant counter: For vv = 1 to v)
        v = i - 1   ' <-- doesn't affect the loop's outcome
        Debug.Print i,          ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case3 v-3 <-- v ",
    v = 4
    For v = v To v - 3 Step -1
       Debug.Print v,           ' 4, 3, 2, 1
    Next

    Debug.Print vbCrLf & "Case4 v --> v-0 ",
    v = 4
    For v = 1 To v - 0
        Debug.Print v,          ' 1, 2, 3, 4
    Next

    '  So far so good? now the serious business

    Debug.Print vbCrLf & "Case5 v --> v    ",
    v = 4
    For v = 1 To v
        Debug.Print v,          ' 1      (yes, just 1)
    Next

    Debug.Print vbCrLf & "Testing For-Each"

    Debug.Print vbCrLf & "Case6 v in v[]",
    v = Array(1, 1, 1, 1)
    i = 1
    ' Any of the Commented lines below generates the same RT error:
    'For Each v In v  ' "This array is fixed or temporarily locked"
    For Each vv In v
        'v = 4
        'ReDim Preserve v(LBound(v) To UBound(v))
        If i < UBound(v) Then v(i + 1) = i + 1 ' so we can alter the entries in the array, but not the array itself
        i = i + 1
         Debug.Print vv,            ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case7 obj in col",
    Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
    For Each obj In obj
        Debug.Print obj.Column,    ' 1 only ?
    Next

    Debug.Print vbCrLf & "Case8 var in col",
    Set v = New Collection: For i = 1 To 4: v.Add Cells(i, i): Next
    For Each v In v
        Debug.Print v.column,      ' nothing!
    Next

    ' Excel Range
    Debug.Print vbCrLf & "Case9 range as var",
    ' Same with collection? let's see
    Set v = Sheet1.Range("A1:D1") ' .Cells ok but not .Value => RT err array locked
    For Each v In v ' (implicit .Cells?)
        Debug.Print v.Column,       ' 1, 2, 3, 4
    Next

    ' Amazing for Excel, no need to declare two vars to iterate over a range
    Debug.Print vbCrLf & "Case10 range in range",
    Set rng = Range("A1:D1") '.Cells.Cells add as many as you want
    For Each rng In rng ' (another implicit .Cells here?)
        Debug.Print rng.Column,     ' 1, 2, 3, 4
    Next
End Sub

更新1

一个有趣的观察,可以帮助理解其中的一些。关于案例7和案例8:如果我们对正在迭代的集合持有另一个引用,则行为完全改变:

    Debug.Print vbCrLf & "Case7 modified",
    Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
    Dim obj2: set obj2 = obj  ' <-- This changes the whole thing !!!
    For Each obj In obj
        Debug.Print obj.Column,    ' 1, 2, 3, 4 Now !!!
    Next

这意味着在初始case7中,在将变量obj分配给集合的第一个元素之后,迭代的集合被垃圾收集(由于引用计数)。但这仍然很奇怪。编译器应该对正在迭代的对象持有一些隐藏的引用!?将此与案例6进行比较,其中迭代的数组被“锁定”......

更新2

可以找到MSDN定义的For语句的语义on this page。您可以看到,明确声明end-value只应评估一次,然后才能执行循环。我们应该将这种奇怪的行为视为编译器错误吗?

更新3

又一个有趣的案例7。 case7的反直觉行为不仅限于变量本身的(例如异常)迭代。它可能发生在看似“无辜”的代码中,错误地删除了正在迭代的集合的唯一引用,导致其垃圾收集。

Debug.Print vbCrLf & "Case7 Innocent"
Dim col As New Collection, member As Object, i As Long
For i = 1 To 4: col.Add Cells(i, i): Next
Dim someCondition As Boolean ' say some business rule that says change the col
For Each member In col
    someCondition = True
    If someCondition Then Set col = Nothing ' or New Collection
    ' now GC has killed the initial collection while being iterated
    ' If you had maintained another reference on it somewhere, the behavior would've been "normal"
    Debug.Print member.Column, ' 1 only
Next

通过直觉,人们期望在集合中保留一些隐藏的引用以在迭代期间保持活跃。不仅没有,而且程序运行顺畅,没有运行时错误,可能导致硬错误。虽然规范没有规定任何关于在迭代下操纵对象的规则,但实现恰好保护并且锁定迭代的数组(情况6)但忽略 - 甚至不包含虚拟引用 - 在集合上(在字典上,我也测试了它。)

程序员有责任关心引用计数,这不是VBA / VB6的“精神”和引用计数背后的架构动机。

更新4:变体比较诅咒

Variant在许多情况下表现出奇怪的行为。特别是,比较两个不同子类型的变体会产生不确定的结果。请考虑以下简单示例:

Sub Test1()
  Dim x, y: x = 30: y = "20"
  Debug.Print x > y               ' False !!
End Sub

Sub Test2()
  Dim x As Long, y: x = 30: y = "20"
  '     ^^^^^^^^
  Debug.Print x > y             ' True
End Sub

Sub Test3()
  Dim x, y As String:  x = 30: y = "20"
  '        ^^^^^^^^^
  Debug.Print x > y             ' True
End Sub

正如您所看到的,当两个变量(数字和字符串)都是声明的变体时,比较是未定义的。当显式键入其中至少一个时,比较成功。

比较平等时也是如此!例如,?2="2"返回True,但如果您定义了两个Variant变量,请为它们分配并比较它们,比较失败!

Sub Test4()
  Debug.Print 2 = "2"           ' True

  Dim x, y:  x = 2:  y = "2"
  Debug.Print x = y             ' False !

End Sub

1 个答案:

答案 0 :(得分:16)

请参阅以下编辑内容!

对于每个编辑,也在Edit2下添加

有关Edit3的ForEach和Collections的更多编辑

关于Edit4的ForEach和Collections的最后一次编辑

关于Edit5的迭代行为的最后说明

当用作循环控制变量或终止条件时,变量评估语义中这种奇怪行为的一部分细微之处。

简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值。然而,类型(例如Integer)被推送directly,因此不会重新评估(并且其值不会发生变化)。如果控制变量是Integer,但终止值是Variant,则Variant在第一次迭代时被强制转换为Integer,并且推送类似。当终止条件是涉及VariantInteger的表达式时,会出现同样的情况 - 它被强制转换为Integer

在这个例子中:

Dim v as Variant
v=4
for v= 1 to v
  Debug.print v,
next

变量v的整数值为1,循环终止条件为重新计算,因为终止变量是变量 - 运行时识别变量引用的存在并强制重新每次迭代评估。结果,由于环路内重新分配,循环完成。由于变量现在的值为1,因此满足循环终止条件。

考虑下一个例子:

Dim v as variant
v=4
for v=1 to v-0
   Debug.Print v,
next 

当终止条件是表达式时,例如&#34; v - 0&#34;,表达式为 评估 强制常规整数,而不是变体,因此它的硬值在运行时被推送到堆栈。因此,在每次循环迭代时都不会重新评估该值。

另一个有趣的例子:

Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
   v=i-1
   Debug.print i,
next

的行为与它的行为一样,因为控制变量是一个整数,因此终止变量也被强制转换为整数,然后被推送到堆栈进行迭代。

我不能发誓这些是语义,但我相信终止条件或值只是被压入堆栈,因此整数值,或推送Variant的对象引用,从而在编译器实现变量保持终止值时触发重新评估。当变量在循环中重新分配,并且在循环完成时重新查询该值时,将返回新值,并且循环终止。

很抱歉,如果这有点浑浊,但现在有点迟了,但是我看到了这个并且无法帮助,但是却得到答案。希望它有一定道理。啊,好的&#39; VBA:)

编辑:

从MS的VBA语言规范中找到一些实际信息:

  

表达式[start-value],[end-value]和[step-increment]按顺序评估一次,并且在以下任何计算之前进行评估。如果[start-value],[end-value]和[step-increment]的值不是Let-coercible to Double,则立即引发错误13(类型不匹配)。否则,使用原始的未校正值继续执行以下算法。

     

[for-statement]的执行按照以下步骤进行   算法:

     
      
  1. 如果[step-increment]的数据值为零或正数,   并且[bound-variable-expression]的值大于   [end-value]的值,然后执行[forstatement]   立即完成;否则,请进入第2步。

  2.   
  3. 如果[step-increment]的数据值是负数,那么   [bound-variable-expression]的值小于。的值   [end-value],[for-statement]的执行立即完成;   否则,请进入第3步。

  4.   
  5. 执行[statement-block]。如果是[nested-for-statement]   现在,然后执行。最后,价值   [bound-variable-expression]被添加到[step-increment]的值中   并将其分配回[bound-variable-expression]。然后执行   在第1步重复。

  6.   

我从中收集到的是 intent 用于终止条件值一次只评估。如果我们看到证据表明改变该值会使循环的行为从其初始条件发生变化,那么几乎可以肯定的是,非正式地称为意外重新评估,因为它是一种变体。如果它是无意的,我们可能只能使用非传统证据来预测其行为。

如果运行时评估循环的开始/结束/步长值,并按下&#34;值&#34;将这些表达式放入堆栈中,Variant值会抛出一个&#34; byref扳手&#34;进入过程。如果运行时没有首先识别变体,评估它,并将 值作为终止条件,那么好奇的行为(正如您所示)几乎肯定会随之发生。正如其他人所建议的那样,在这种情况下,VBA如何处理变体对于pcode分析来说是一项伟大的任务。

EDIT2:FOREACH

VBA规范再次提供了对集合和数组上的ForEach循环评估的深入见解:

  

在任何&gt;以下计算之前评估表达式[collection]。

     
      
  1. 如果[collection]的数据值是数组:

         

    如果数组没有元素,则执行[for-each-statement]   马上完成。

         

    如果声明的数组类型是Object,那么   [bound-variable-expression]设置为分配给&gt;数组中的第一个元素。否则,[bound-variable-expression]被Let-assigned给数组中的&gt;第一个元素。

         

    在设置了[bound-variable-expression]之后,执行[statement-block]&gt;。如果存在[nested-for-statement],则执行它。

         

    一旦[statement-block]和(如果存在)[nested-for-statement]&gt;已经完成执行,[bound-variable-expression]将被分配给&gt;数组中的下一个元素(或者如果它是一个&gt;对象的数组,则设置为已分配。当且仅当数组中没有更多元素时,> [for-each-statement]的执行立即完成。否则,再次执行&gt; [statement-block],如果&gt;存在,则执行[nested-forstatement],然后重复此步骤。

         

    当[for-each-statement]完成执行时,&gt; [bound-variable-expression]的值是&gt;数组的最后一个元素的数据值。

  2.   
  3. 如果[collection]的数据值不是数组:

         

    [collection]的数据值必须是对支持实现定义的枚举&gt;接口的&gt;外部对象的对象引用。 [bound-variable-expression]可以是Let-assigned或&gt; Set-assign到[collection]中的第一个元素,采用&gt; implementation-&gt;定义的方式。

         

    在设置了[bound-variable-expression]之后,执行[statement-block]&gt;。如果存在[nested-for-statement],则执行它。

         

    一旦[statement-block]和(如果存在)[nested-for-statement]&gt;已完成执行,[bound-variable-expression]将被设置为&gt; [collection]中的下一个元素以实现定义的方式。如果&gt; [collection]中没有更多元素,则[for-each-&gt;语句]的执行立即完成。否则,再次执行[statement-block],然后再执行[nested-for-statement],并重复此&gt;步骤。

         

    当[for-each-statement]完成执行时,&gt; [bound-variable-expression]的值是&gt; [collection]中最后一个元素的数据值。

  4.   

使用它作为基础,我认为很明显,分配给变量然后变为bound-variable-expression的Variant会生成&#34;数组被锁定&#34;此示例中的错误:

    Dim v As Variant, vv As Variant
v = Array(1, 1, 1, 1)
i = 1
' Any of the Commented lines below generates the same RT error:
For Each v In v  ' "This array is fixed or temporarily locked"
'For Each vv In v
    'v = 4
    'ReDim Preserve v(LBound(v) To UBound(v))
    If i < UBound(v) Then v(i + 1) = i + 1 ' so we can alter the entries in the array, but not the array itself
    i = i + 1
     Debug.Print vv,            ' 1, 2, 3, 4
Next

使用&#39; v&#39;因为[bound-variable-expression]创建了一个回运给V的Let-assignment,它被运行时阻止,因为它是正在进行的枚举的目标,以支持ForEach循环本身;也就是说,运行时会锁定变量,从而阻止循环为变量分配不同的值,这必然会发生。

这也适用于&#39; Redim Preserve&#39; - 调整大小或更改数组,从而更改变量的赋值,将违反循环初始化时放置在枚举目标上的锁定。

关于基于范围的赋值/迭代,请注意非对象元素的单独语义启动; &#34;外部对象&#34;提供特定于实现的枚举行为。 excel Range对象具有 _Default属性,仅在对象名称引用时被调用,如本例所示,当用作迭代目标时,它不会采用隐式锁定ForEach(因此它不会产生锁定错误,因为它具有与Variant类别不同的​​语义):

Debug.Print vbCrLf & "Case10 range in range",
Set rng = Range("A1:D1") '.Cells.Cells add as many as you want
For Each rng In rng ' (another implicit .Cells here?)
    Debug.Print rng.Column,     ' 1, 2, 3, 4
Next

(可以通过检查VBA对象浏览器中的Excel对象库,通过突出显示Range对象,右键单击并选择&#34; Show Hidden Members&#34;)来识别_Default属性。 / p>

EDIT3:收藏

涉及集合的代码变得有趣而且有点毛茸茸:)

Debug.Print vbCrLf & "Case7 obj in col",
Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
For Each obj In obj
    Debug.Print obj.Column,    ' 1 only ?
Next

Debug.Print vbCrLf & "Case8 var in col",
Set v = New Collection: For i = 1 To 4: v.Add Cells(i, i): Next
For Each v In v
    Debug.Print v.column,      ' nothing!
Next

这里只需要考虑一个真正的错误。当我第一次在VBA调试器中运行这两个样本时,它们就像初始问题中提供的OP一样运行。然后,在几次测试之后重新启动例程,然后将代码恢复到其原始形式(如此处所示),后者的行为随意地开始匹配其上面的基于 object 的前导的行为!只有在我停止Excel并重新启动它之后,才执行后一循环的原始行为(不打印任何内容),返回。除了编译器错误之外,真的没办法解释它。

EDIT4使用变体的可重现行为

在注意到我在调试器中完成了某事以强制基于变体的迭代通过Collection循环至少一次(就像它与Object版本一样),我终于找到了一种代码可重现的改变行为的方式

考虑这个原始代码:

Dim v As Variant, vv As Variant

Set v = New Collection: For x = 1 To 4: v.Add Cells(x, x): Next x
'Set vv = v
For Each v In v
   Debug.Print v.Column
Next

这基本上是OP的原始情况,并且ForEach循环在没有单次迭代的情况下终止。现在,取消注释&quot; Set vv = v&#39; line,并重新运行:现在For Each将迭代一次。我认为毫无疑问,我们在VB运行时的Variant评估机制中发现了一些非常(非常!)的微妙错误;任意设置另一个变种&#39;等于循环变量强制在For Each评估中不进行评估 - 我怀疑这与Collection在变体中表示为Variant / Object / Collection的事实有关。添加这个虚假的设置&#39;似乎强制问题并使循环像基于对象的版本那样运行。

EDIT5:关于迭代和集合的最终想法

这可能是我对这个答案的最后编辑,但有一件事我必须强迫自己确保在观察奇数循环行为时识别出变量被用作&#39; bound-variable-expression& #39;并且限制表达式是,特别是当涉及变体时,有时行为是通过迭代改变“绑定 - 变量 - 表达”的内容而引起的。&#39;也就是说,如果你有:

Dim v as Variant
Dim vv as Variant
Set v = new Collection(): for x = 1 to 4: v.Add Cells(x,x):next
Set vv = v ' placeholder to make the loop "kinda" work
for each v in v
   'do something
Next

至关重要的是要记住(至少对我而言)记住在For Each中,&#39; bound-variable-expression&#39;在&#39; v&#39;通过迭代获得已更改。也就是说,当我们开始循环时,v拥有一个Collection,枚举开始。但是当枚举开始时,v的内容现在是枚举的产物 - 在这种情况下,是一个Range对象(来自Cell)。在调试器中可以看到此行为,因为您可以观察到&#39; v&#39;从Collection到Range;这意味着迭代中的下一步将返回Range对象的枚举上下文所提供的内容,而不是&#39; Collection。&#39;

这是一项很棒的研究,我很欣赏这些反馈意见。它比我想象的更能帮助我理解事物。除非有更多的评论或问题,我怀疑这将是我对答案的最后编辑。