【发布时间】:2017-07-10 07:16:11
【问题描述】:
请查看帖子末尾的最新更新。
特别是,请参阅更新 4:变体比较诅咒
我已经看到队友们用头撞墙以了解变体的工作原理,但从没想过我会遇到自己的糟糕时刻。
我已经成功使用了以下 VBA 构造:
For i = 1 to i
当i 是整数 或任何数字类型时,这非常有效,从1 迭代到i 的原始值。我在 i 是 ByVal 参数的情况下这样做 - 你可能会说懒惰 - 以免自己声明一个新变量。
然后,当这个构造“停止”按预期工作时,我遇到了一个错误。经过一番艰苦的调试,我发现当i 没有被声明为显式数字类型,而是Variant 时,它的工作方式并不相同。问题是双重的:
1- For 和 For Each 循环的确切语义是什么?我的意思是编译器执行的操作顺序是什么以及顺序是什么?例如,限制的评估是否先于计数器的初始化?在循环开始之前,这个限制是否被复制和“固定”?等等。同样的问题也适用于For Each。
2- 如何解释变体和显式数字类型的不同结果?有人说变体是(不可变的)引用类型,这个定义可以解释观察到的行为吗?
我为涉及For 和For 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
这意味着在最初的案例 7 中,在将变量 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:变体比较诅咒
Variants 在许多情况下表现出奇怪的行为。特别是,比较不同子类型的两个变体会产生不确定的结果。考虑这些简单的例子:
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
【问题讨论】:
-
Documentation 中的描述非常接近
Variant的“正式”定义。如果您想要比这更正式,请点击 MSDN 的链接。不过,我并不完全清楚问题中的数据类型、参数类型和循环退出条件之间的关系。 -
非常有趣的问题...虽然测试感觉“脏”,因为它们都在同一个范围内并且一遍又一遍地重用/重新分配相同的变量,这感觉不对- 每个测试用例有一个单独的测试方法会很好.. 使用单个
Assert调用 ...Rubberduck-style :-) -
提示:它与在退出条件中使用循环计数器有关。如果我是你,我会在心理上为金属级别的答案做好准备 =)
-
有点奇怪:如果你定义了一个函数
Echo(v),其唯一的代码行是Echo = v,并且在你的案例5中将For v = 1 To v替换为For v = 1 To Echo(v),那么它会打印出1,2,3 ,4 毕竟。 -
在现有答案之外,我只会验证运行时中的实际代码是否符合规范。我昨晚反汇编了运行时,但还没有时间深入研究它。它基本上归结为是指向
Variant结构 的指针被压入堆栈还是指向Variant数据区 的指针被压入堆栈。相关的事实是循环是通过 msvbvm6 库中的函数调用执行的,因此Variant将被装箱两次。
标签: vba for-loop vb6 language-lawyer variant