Variable iterating on itself - different behavior with different types
请查看帖子末尾的最新更新。
特别是,请参阅更新4:变体比较诅咒
我已经见过同伴撞墙撞头,以了解变体的工作原理,但从未想象过自己会遇到麻烦。
我已成功使用以下VBA构造:
当
然后,当此构造按预期"停止"工作时,我遇到了一个错误。经过一番艰苦的调试,我发现当
1-
2-如何解释变量和显式数值类型的不同结果?有人说变量是(不可变的)引用类型,这个定义可以解释观察到的行为吗?
我已经为涉及
欢迎所有见解,包括部分见解,这些见解可以解释某些令人惊讶的结果或其矛盾。
谢谢。
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中,正好在将变量
更新2
由MSDN定义的
更新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:变式比较诅咒
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 |
如您所见,当两个变量(数字和字符串)都声明为变量时,比较是不确定的。当至少其中之一被显式键入时,比较成功。
比较相等时也会发生同样的情况!例如,
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的迭代行为的最后说明
好。
当用作循环控制变量或终止条件时,变体评估的语义中这种奇怪行为的部分微妙之处。
好。
简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值。但是,将值类型(例如
好。
在此示例中:
好。
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]中最后一个元素的数据值。
好。
blockquote>
以此为基础,我认为很明显,在此示例中,分配给变量的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"。
好。
这是一项很棒的研究,我感谢您的反馈。它帮助我比我想象的更好地理解了事情。除非对此有更多评论或问题,否则我怀疑这将是我对答案的最后修改。
好。
好。