A.S*_*S.H 32 vb6 vba for-loop variant language-lawyer
请查看帖子末尾的最新更新.
特别是,请参阅更新4:变体比较诅咒
我已经看到配偶撞到墙上以了解变体是如何工作的,但从来没有想到我会有自己的糟糕时刻.
我已成功使用以下VBA构造:
For i = 1 to i
当这工作完全i
是一个整数或者任何数值类型,从1迭代到原始值的i
.我这样做的地方i
是一个ByVal
参数 - 你可能会说懒惰 - 让我自己声明一个新的变量.
然后当这个结构"停止"按预期工作时,我遇到了一个错误.经过一些艰难的调试后,我发现当它没有i
被声明为显式数字类型时,它的工作方式不同,但是a Variant
.问题有两个:
1- For
和For Each
循环的确切语义是什么?我的意思是编译器执行的操作顺序是什么,顺序是什么?例如,限制的评估是否在计数器的初始化之前?在循环开始之前,这个限制是否被复制并"修复"了?等等.同样的问题适用于For Each
.
2-如何解释变体和显式数字类型的不同结果?有人说变体是一个(不可变的)引用类型,这个定义可以解释观察到的行为吗?
我已经为涉及和语句的不同(独立)场景准备了MCVE,并结合了整数,变体和对象.令人惊讶的结果促使明确定义语义,或者至少检查那些结果是否符合定义的语义.For
For Each
欢迎所有见解,包括解释一些令人惊讶的结果或其矛盾的部分见解.
谢谢.
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
Run Code Online (Sandbox Code Playgroud)
更新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
Run Code Online (Sandbox Code Playgroud)
这意味着在初始case7中,在将变量obj
分配给集合的第一个元素之后,迭代的集合被垃圾收集(由于引用计数).但这仍然很奇怪.编译器应该对正在迭代的对象持有一些隐藏的引用!?将此与案例6进行比较,其中迭代的数组被"锁定"...
更新2
For
可以在此页面上找到MSDN定义的语句的语义.您可以看到明确声明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
Run Code Online (Sandbox Code Playgroud)
通过直觉,人们期望在集合中保留一些隐藏的引用以在迭代期间保持活跃.不仅没有,而且程序运行顺畅,没有运行时错误,可能导致硬错误.虽然规范没有规定任何关于在迭代下操纵对象的规则,但是实现恰好保护和锁定迭代的数组(案例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
Run Code Online (Sandbox Code Playgroud)
如您所见,当两个变量(数字和字符串)都是声明的变体时,比较是未定义的.当显式键入其中至少一个时,比较成功.
比较平等时也是如此!例如,?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
Run Code Online (Sandbox Code Playgroud)
Dav*_*d W 16
请看下面的编辑!
对于每个编辑,也在Edit2下面添加
有关Edit3的ForEach和Collections的更多编辑
关于Edit4的ForEach和Collections的最后一次编辑
关于Edit5的迭代行为的最后一点说明
当用作循环控制变量或终止条件时,变量评估的语义中这种奇怪行为的微妙部分.
简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值.然而,诸如a之类的值类型Integer
被推送directly
,因此不被重新评估(并且其值不会改变).如果控制变量是a Integer
,但是终止值是a Variant
,Variant
则Integer
在第一次迭代时强制转换为a ,并且类似地推送.当终止条件是涉及a Variant
和Integer
- 被强制转换为a的表达式时,会出现相同的情况Integer
.
在这个例子中:
Dim v as Variant
v=4
for v= 1 to v
Debug.print v,
next
Run Code Online (Sandbox Code Playgroud)
变量v被赋予整数值1,并且循环终止条件被重新评估,因为终止变量是变体 - 运行时识别Variant引用的存在并强制每次迭代重新评估.结果,由于环路内重新分配,循环完成.由于变量现在的值为1,因此满足循环终止条件.
考虑下一个例子:
Dim v as variant
v=4
for v=1 to v-0
Debug.Print v,
next
Run Code Online (Sandbox Code Playgroud)
当终止条件是表达式(例如"v-0")时,表达式被计算并强制转换为常规整数,而不是变量,因此其硬值在运行时被推送到堆栈.结果,在每次循环迭代时不重新评估该值.
另一个有趣的例子:
Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
v=i-1
Debug.print i,
next
Run Code Online (Sandbox Code Playgroud)
因为控制变量是一个整数,所以行为与它一样,因此终止变量也被强制转换为整数,然后被推送到堆栈进行迭代.
我不能发誓这些是语义,但我相信终止条件或值只是被推入堆栈,因此推送整数值,或推送Variant的对象引用,从而在编译器实现变量时触发重新评估持有终止价值.当变量在循环中重新分配,并且在循环完成时重新查询该值时,将返回新值,并且循环终止.
对不起,如果那有点浑浊,但是有点晚了,但是我看到了这一点,并且忍不住拍了一个答案.希望它有一定道理.啊,好的'VBA :)
编辑:
从MS的VBA语言规范中找到一些实际信息:
表达式[start-value],[end-value]和[step-increment]按顺序评估一次,并且在以下任何计算之前进行评估.如果[start-value],[end-value]和[step-increment]的值不是Let-coercible to Double,则立即引发错误13(类型不匹配).否则,使用原始的未校正值继续执行以下算法.
执行[for-statement]按照以下算法进行:
如果[step-increment]的数据值为零或正数,并且[bound-variable-expression]的值大于[end-value]的值,则[forstatement]的执行立即完成; 否则,进入第2步.
如果[step-increment]的数据值是负数,并且[bound-variable-expression]的值小于[end-value]的值,则[for-statement]的执行立即完成; 否则,进入第3步.
[statement-block]被执行.如果存在[nested-for-statement],则执行该语句.最后,将[bound-variable-expression]的值添加到[step-increment]的值中,并将Let-assigned赋值回[bound-variable-expression].然后在步骤1重复执行.
我从中收集到的意图是终止条件值仅被评估一次和一次.如果我们看到证据表明改变该值会改变循环从其初始条件的行为,那么几乎可以肯定的是,由于它是一种变体,因此可能被称为非正式的意外重新评估.如果它是无意的,我们可能只能使用非科学证据来预测其行为.
如果运行时计算循环的开始/结束/步长值,并将这些表达式的"值"推入堆栈,则Variant值会将"byref wrench"抛出到进程中.如果运行时没有首先识别变量,对其进行评估,并将该值作为终止条件,那么好奇的行为(正如您所示)几乎肯定会随之发生.正如其他人所建议的那样,在这种情况下,VBA如何处理变体将是pcode分析的一项重要任务.
EDIT2:FOREACH
VBA规范再次提供了对集合和数组上的ForEach循环的评估:
表达[collection]在任何>以下计算之前被评估一次.
如果[collection]的数据值是一个数组:
如果数组没有元素,则[for-each-statement]的执行立即完成.
如果声明的数组类型是Object,则[bound-variable-expression]被设置为>分配给>数组中的第一个元素.否则,[bound-variable-expression]被Let-assigned给数组中的> first元素.
设置[bound-variable-expression]后,执行[statement-block]>.如果存在[nested-for-statement],则执行该语句.
一旦[statement-block]和[nested-for-statement]>完成执行,[bound-variable-expression]就被赋予>数组中的下一个元素(或者set-assigned如果它是一个> Object的数组.当且仅当数组中没有更多元素时,> [for-each-statement]的执行立即完成.否则,再次执行> [statement-block],如果>存在,则执行[nested-forstatement],然后重复此步骤.
当[for-each-statement]完成执行时,> [bound-variable-expression]的值是> array的最后一个元素的数据值.
如果[collection]的数据值不是数组:
[collection]的数据值必须是对>支持实现定义的枚举>接口的外部对象的对象引用.[bound-variable-expression]是以> implementation->定义的方式将Let-assigned或> Set-assigned分配给[collection]中的第一个元素.
设置[bound-variable-expression]后,执行[statement-block]>.如果存在[nested-for-statement],则执行该语句.
一旦[statement-block]和[nested-for-statement]>已经完成执行,[bound-variable-expression]就被设置为>实现定义中的[collection]中的下一个元素方式.如果> [collection]中没有更多元素,则[for-each->语句]的执行立即完成.否则,再次执行[statement-block],然后执行[nested-for-statement],并重复此>步骤.
当[for-each-statement]完成执行时,> [bound-variable-expression]的值是> [collection]中最后一个元素的数据值.
使用它作为基础,我认为很明显,分配给变量然后变为bound-variable-expression的Variant会在此示例中生成"Array is locked"错误:
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
Run Code Online (Sandbox Code Playgroud)
使用'v'作为[bound-variable-expression]创建一个回运给V的Let-assignment,它被运行时阻止,因为它是正在进行的枚举的目标,用于支持ForEach循环本身; 也就是说,运行时锁定变量,从而阻止循环为变量分配不同的值,这必然会发生.
这也适用于'Redim Preserve' - 调整大小或更改数组,从而更改变量的赋值,将违反循环初始化时放置在枚举目标上的锁定.
关于基于范围的赋值/迭代,请注意非对象元素的单独语义启动; "外部对象"提供特定于实现的枚举行为.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
Run Code Online (Sandbox Code Playgroud)
(_Default
可以通过检查VBA对象浏览器中的Excel对象库,通过突出显示Range对象,右键单击并选择"显示隐藏的成员")来识别该属性.
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
Run Code Online (Sandbox Code Playgroud)
这就是必须考虑真正的错误.当我第一次在VBA调试器中运行这两个样本时,它们就像初始问题中提供的OP一样运行.然后,在几次测试之后重新启动例程,然后将代码恢复到其原始形式(如此处所示),后者的行为随意地开始匹配其上面的基于对象的前任的行为!只有在我停止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
Run Code Online (Sandbox Code Playgroud)
这基本上是OP的原始情况,ForEach循环终止而没有一次迭代.现在,取消注释'Set vv = v'行,然后重新运行:现在For Each将迭代一次.我认为毫无疑问,我们在VB运行时的Variant评估机制中发现了一些非常(非常!)的微妙错误; 另一个'Variant'的任意设置等于循环变量强制进行For Each评估中不会发生的评估 - 我怀疑这与Collection在Variant中表示为Variant/Object/Collection的事实有关.添加这个虚假的'set'似乎会强制解决问题并使循环像基于对象的版本那样运行.
EDIT5:关于迭代和集合的最终想法
这可能是我对这个答案的最后一次编辑,但有一点我必须强迫自己确保在观察奇数循环行为时识别出变量被用作'bound-variable-expression'并且限制表达式是特别是当涉及到"变体"时,有时行为是通过迭代改变"束缚变量 - 表达"的内容而引起的.也就是说,如果你有:
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
Run Code Online (Sandbox Code Playgroud)
至关重要的是要记住(至少对我而言)要记住,在For Each中,'v'中的'bound-variable-expression'会因迭代而改变.也就是说,当我们开始循环时,v拥有一个Collection,枚举开始.但是当枚举开始时,v的内容现在是枚举的产物- 在这种情况下,是一个Range对象(来自Cell).在调试器中可以看到这种行为,因为你可以观察到'v'从Collection到Range; 这意味着迭代中的下一步将返回Range对象提供的枚举上下文,而不是"Collection".
这是一项伟大的研究,我很感激反馈.它帮助我理解比我想象的更好的事情.除非有更多的评论或问题,我怀疑这将是我对答案的最后编辑.