关于vba:可变的自身迭代-不同类型的不同行为

Variable iterating on itself - different behavior with different types

请查看帖子末尾的最新更新。

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

我已经见过同伴撞墙撞头,以了解变体的工作原理,但从未想象过自己会遇到麻烦。

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

For i = 1 to i

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

然后,当此构造按预期"停止"工作时,我遇到了一个错误。经过一番艰苦的调试,我发现当i不是声明为显式数字类型,而是声明为Variant时,它的工作方式不同。问题是双重的:

1- ForFor Each循环的确切语义是什么?我的意思是编译器按照什么顺序执行动作序列?例如,限制的评估是否在计数器初始化之前进行?循环开始之前,此限制是否已复制并"固定"在某个地方?等等,同样的问题适用于For Each

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

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

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

谢谢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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:如果我们对要迭代的集合持有另一个引用,则行为将完全改变:

1
2
3
4
5
6
    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

这意味着在最初的情况7中,正好在将变量obj分配给集合的第一个元素之后,才对要迭代的集合进行垃圾回收(由于引用计数)。但是,这仍然很奇怪。编译器应该在要迭代的对象上保留一些隐藏的引用!将此与情况6进行比较,其中要迭代的数组被"锁定" ...

更新2

由MSDN定义的For语句的语义可以在此页面上找到。您可以看到明确声明了end-value应该只被评估一次,并且在执行循环之前进行评估。我们是否应该将此奇怪行为视为编译器错误?

更新3

有趣的案例7。 case7的违反直觉的行为不仅限于变量本身的(例如不寻常的)迭代。它可能发生在看似"无辜"的代码中,该代码错误地删除了要迭代的集合上的唯一引用,从而导致其垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
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),但忽略了-甚至没有保存伪引用-在集合上(既不在Dictionary上,我在ve也测试过)。

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

更新4:变式比较诅咒

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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变量,将它们分配给它们并进行比较,则比较将失败!

1
2
3
4
5
6
7
Sub Test4()
  Debug.Print 2 ="2"           ' True

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

End Sub


请在下面查看编辑!

好。

对于每个编辑,也在Edit2下添加了以下内容

好。

在Edit3上对ForEach和Collections进行更多编辑

好。

在Edit4上关于ForEach和集合的最后编辑

好。

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

好。

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

好。

简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值。但是,将值类型(例如Integer)压入directly,因此不会重新求值(并且其值不会更改)。如果控制变量是Integer,但是终止值是Variant,则在第一次迭代中,将Variant强制转换为Integer,并进行类似的推送。当终止条件是包含VariantInteger的表达式时,也会发生相同的情况-将其强制为Integer

好。

在此示例中:

好。

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

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

好。

考虑下一个示例:

好。

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

当终止条件是一个表达式,例如" v-0"时,该表达式将被求值并强制为正则整数,而不是变量,因此其硬值在运行时被压入堆栈。结果,该值不会在每次循环迭代时重新评估。

好。

另一个有趣的例子:

好。

1
2
3
4
5
6
7
Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
   v=i-1
   Debug.print i,
next

行为之所以如此,是因为控制变量是Integer,因此终止变量也被强制为整数,然后被压入堆栈进行迭代。

好。

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

好。

抱歉,这有点泥泞,但是有点晚了,但是我看到了,不禁为答案出手。希望这有道理。啊,好的VBA :)

好。

编辑:

好。

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

好。

The expressions [start-value], [end-value], and [step-increment] are evaluated once, in order, and prior to any of the following computations. If the value of [start-value], [end-value], and [step-increment] are not Let-coercible to Double, error 13 (Type mismatch) is raised immediately. Otherwise, proceed with the following algorithm using the original, uncoerced values.

Ok.

Execution of the [for-statement] proceeds according to the following
algorithm:

Ok.

  • If the data value of [step-increment] is zero or a positive number,
    and the value of [bound-variable-expression] is greater than
    the value of [end-value], then execution of the [forstatement]
    immediately completes; otherwise, advance to Step 2.

    Ok.

  • If the data value of [step-increment] is a negative number, and the
    value of [bound-variable-expression] is less than the value of
    [end-value], execution of the [for-statement] immediately completes;
    otherwise, advance to Step 3.

    Ok.

  • The [statement-block] is executed. If a [nested-for-statement] is
    present, it is then executed. Finally, the value of
    [bound-variable-expression] is added to the value of [step-increment]
    and Let-assigned back to [bound-variable-expression]. Execution then
    repeats at step 1.

    Ok.

  • 我从中得到的是,目的是要一次评估一次终止条件值。如果我们看到有证据表明更改该值会从其初始状态改变循环的行为,则几乎可以肯定是由于它是变体,因此可以非正式地称为意外重新评估。如果不是故意的,我们可能只能使用轶事证据来预测其行为。

    好。

    如果在运行时评估循环的开始/结束/步骤值,并将这些表达式的"值"压入堆栈,则Variant值会将" byref扳手"扔到进程中。如果运行时不首先识别变体,请对其进行评估,然后将该值作为终止条件,那么几乎可以肯定会发生奇怪的行为(如您所示)。正如其他人所建议的那样,在这种情况下,VBA究竟如何处理变体将是pcode分析的一项艰巨任务。

    好。

    编辑2:前瞻

    好。

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

    好。

    在进行以下任何计算之前,都会对表达式[collection]进行一次评估。

    好。

  • 如果[collection]的数据值是一个数组:

    好。

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

    好。

    如果数组的声明类型为Object,则
    [bound-variable-expression]被设置为> array中的第一个元素。否则,将[bound-variable-expression]分配给数组中的> first元素。

    好。

    设置[bound-variable-expression]后,执行[statement-block]>。如果存在[嵌套声明],则将其执行。

    好。

    一旦[statement-block]和(如果存在)[nested-for-statement]已完成执行,就将[bound-variable-expression]分配给>数组中的下一个元素(或者Set-assigned它是> Object的数组)。当且仅当数组中没有其他元素时,[for-each-statement]的执行立即完成。否则,再次执行> [statement-block],如果> present,则执行[nested-forstatement],然后重复此步骤。

    好。

    [for-each-statement]完成执行后,> [bound-variable-expression]的值是> array的最后一个元素的数据值。

    好。

  • 如果[collection]的数据值不是数组:

    好。

    [collection]的数据值必须是对>外部对象的对象引用,该对象支持实现定义的枚举>接口。 [bound-variable-expression]以> implementation-> defined的方式被Let分配或Set分配给[collection]中的第一个元素。

    好。

    设置[bound-variable-expression]后,执行[statement-block]>。如果存在[嵌套声明],则将其执行。

    好。

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

    好。

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

    好。

  • 以此为基础,我认为很明显,在此示例中,分配给变量的Variant然后变为bound-variable-expression会生成" Array is locked"错误:

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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

    使用'v'作为[bound-variable-expression]会创建一个向V的Let-assignment,它在运行时被阻止,因为它是正在进行枚举的目标,以支持ForEach循环本身。也就是说,运行时将锁定变体,从而避免循环为变体分配不同的值(这必然会发生)。

    好。

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

    好。

    关于基于范围的分配/迭代,请注意非对象元素的单独语义;"外部对象"提供特定于实现的枚举行为。 excel Range对象具有_Default属性,仅在通过对象名称引用时才被调用,在这种情况下,当用作ForEach的迭代目标时,它不会使用隐式锁定(因此不会生成锁定错误,因为它的语义不同于Variant变量):

    好。

    1
    2
    3
    4
    5
    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对象库来标识_Default属性)。

    好。

    EDIT3:集合

    好。

    涉及集合的代码变得有趣并且有些毛躁:)

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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完全相同。然后,在经过几次测试后重新启动例程,然后将代码恢复为其原始形式(如此处所示),后一种行为随即开始匹配其上方的基于对象的前任行为!只有在我停止Excel并重新启动它之后,才返回后者循环的原始行为(不打印任何内容)。除了编译器错误外,实际上没有其他方法可以解释。

    好。

    EDIT4具有变体的可复制行为

    好。

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

    好。

    考虑以下原始代码:

    好。

    1
    2
    3
    4
    5
    6
    7
    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循环无需一次迭代即可终止。现在,取消注释" Set vv = v"行,然后重新运行:现在,For Each将迭代一次。我认为毫无疑问,我们在VB运行时的Variant评估机制中发现了一些非常(非常!)细微的错误;等于循环变量的另一个"变量"的任意设置将强制执行一个在For Each评估中不会发生的评估-我怀疑这与以下事实有关:Collection在Variant中表示为Variant / Object / Collection 。添加这个伪造的"集合"似乎可以解决问题,并使循环像基于对象的版本那样运行。

    好。

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

    好。

    这可能是我对该答案的最后修改,但是我不得不强迫自己确保观察到奇数循环行为时将变量用作" bound-variable-expression",并且极限表达式为特别是在涉及"变量"时,有时是通过迭代来更改"绑定变量表达式"的内容而引起的。也就是说,如果您具有:

    好。

    1
    2
    3
    4
    5
    6
    7
    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中,通过迭代会更改在'v'中保留的'bound-variable-expression'。也就是说,当我们开始循环时,v拥有一个Collection,并且枚举开始。但是,当该枚举开始时,v的内容现在就是该枚举的乘积-在这种情况下,是一个Range对象(来自Cell)。可以在调试器中看到此行为,因为您可以看到" v"从Collection到Range;表示迭代中的下一个踢返回的是Range对象的枚举上下文所提供的内容,而不是" Collection"。

    好。

    这是一项很棒的研究,我感谢您的反馈。它帮助我比我想象的更好地理解了事情。除非对此有更多评论或问题,否则我怀疑这将是我对答案的最后修改。

    好。

    好。