变量迭代本身 - 不同类型的行为 [英] Variable iterating on itself - different behavior with different types

查看:237
本文介绍了变量迭代本身 - 不同类型的行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述


$ b

具体来说,请参阅更新4:变异比较诅咒






我已经看到了队友撞墙的理由,变种的作品,但从来没有想过我会有我自己的坏时刻。

我已经成功地使用了下面的VBA构造:
$

对于i = 1到i



是一个 Integer 或任何数字类型,从1迭代到 i的原始值 C>。我在 i 是一个 ByVal 参数的情况下做了这样的事情 - 你可能会说懒 - 让我自己声明一个新变量。

然后当这个构造停止按预期工作时,我有一个错误。经过一番艰苦的调试之后,我发现当 i 没有被声明为显式数值类型,而是 Variant 时, code>。这个问题是双重的:

$ For 的确切语义是什么, For Each 循环?我的意思是编译器进行的顺序是什么,顺序是什么?例如,在计数器的初始化之前是否对限制进行评估?在循环开始之前,这个限制被复制并固定在某个地方吗?等等同样的问题适用于 For Each


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

我准备了一个 For For Each 语句,结合整数,变体和对象。令人惊讶的结果要求明确地定义语义,或者至少检查这些结果是否符合定义的语义。



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

谢谢。



<$ p $ ()
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&对于i = 1 to v'(如果使用变量计数器,则相同:对于vv = 1到v)
v = i - 1'< - 不影响循环的结果
Debug.Print i,'1,2,3,4
Next

Debug.Print vbCrLf&对于v = v到v-3步骤-1
Debug.Print v,'4,3,2,1
下一个

Debug.Print vbCrLf& Case4 v - > v-0,
v = 4
For v = 1 To v - 0
Debug.Print v,'1,2,3,4
下一个

'到目前为止这么好?现在的业务严重

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

Debug.Print vbCrLf& 测试每个

Debug.Print vbCrLf& v $ [v $]中的Case6 v,
v = Array(1,1,1,1)
i = 1
'下面的任何注释行都会产生相同的RT错误:
'For Each v In v'这个数组是固定的或暂时锁定的
对于每个vv在v
'v = 4
'ReDim保存v(LBound(v)到UBound v))
如果i< UBound(v)然后v(i + 1)= i + 1',所以我们可以改变数组中的条目,而不是数组本身
i = i + 1
Debug.Print vv,'1 ,2,3,4
下一个

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。打印obj.Column,'1只?
下一个

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。打印v.column,什么都没有!
下一个

'Excel范围
Debug.Print vbCrLf& Case9范围为var,
'与集合一样?让我们看看
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

令人惊讶的Excel,不需要声明两个变量来遍历范围
Debug.Print vbCrLf& Case10 range in range,
Set rng =范围(A1:D1).Cells.Cells添加任意数量
对于每个rng in rng'(另一个隐式的.Cells在这里? )
Debug.Print rng.Column'1,2,3,4
Next
End Sub

更新1

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

pre $ Debug $打印vbCrLf &安培; Case7 modified,
Set obj = New Collection:For i = 1 To 4:obj.Add Cells(i,i):Next
Dim obj2:set obj2 = obj'< - This改变整个事情!
对于每个obj在obj
Debug.Print obj.Column,'1,2,3,4现在!!!
下一个

这意味着在最初的情况下,被迭代的集合被垃圾回收由于引用计数)在变量 obj 之后被赋值给集合的第一个元素。但是,这仍然是不可思议的。编译器应该对被迭代的对象持有一些隐藏的引用!比较这个情况6被重复的数组被锁定...

UPDATE 2



MSDN定义的 For 语句的语义可以在本页。您可以看到,明确指出最终值应该仅在循环执行之前计算一次。我们应该考虑这个奇怪的行为作为编译器错误吗?
$ b $ p UPDATE 3 有趣的情况7再次。 case7的反直觉行为不局限于变量本身的(比如说不寻常的)迭代。它可能发生在一个看似无辜的代码中,错误地删除了被迭代的集合的唯一引用,导致其垃圾收集。 c $ c> 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布尔说一些商业规则,说改变col
对于每个成员在col
someCondition = True
如果someCondition然后设置col = Nothing或新集合
'现在GC已经在迭代的时候杀死了最初的集合
'如果你在其他地方维护了另一个引用,那么行为就是正常的
Debug.Print member.Column,'1 only
Next

直觉上,我们期望在迭代过程中隐藏的引用保持在集合中。不仅如此,程序运行平稳,没有运行时错误,可能导致很难的错误。虽然规范并没有规定任何有关在迭代操作对象的规则,但是实现恰好在集合上保护和锁定迭代的数组(情况6),但忽略 - 甚至不保存虚拟引用在字典上,我也测试过)。

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

更新4:变体比较诅咒


$ b $在许多情况下,b

Variant 会表现出奇怪的行为。特别是比较不同子类型的两个变体会得到未定义的结果。考虑下面这些简单的例子:

pre $ $ $ c $ Sub $ TestB $ b $
Debug.Print x> Ÿ假!
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 变量,并比较它们,比较失败!

pre $ $ $ c $ Sub Test4()
Debug.Print 2 =2'True

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

End Sub


解决方案

请参阅下面的编辑!

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

关于编辑3的ForEach和Collections的更多编辑
$ b


$ b

关于Edit5中迭代行为的最后一点注意



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

简而言之,当一个变体是终止值或控制变量时,终止值自然会由运行时每次迭代重新计算。然而, 类型(例如 Integer )直接被 推送,因此不重新评估(其价值不会改变)。如果控制变量是 Integer ,但终止值是 Variant ,则 Variant 在第一次迭代中被强制为一个 Integer ,并被类似地推送。当终止条件是一个涉及 Variant 和一个 Integer 的表达式时,会出现同样的情况 - 它被强制为一个<$在这个例子中:

 

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

变量v被分配一个整数值1,循环终止条件被重新评估,因为终止变量是一个变体 - 运行时识别Variant引用的存在,并强制每次迭代重新评估。结果,由于循环重新分配,循环完成。因为这个变量现在的值是1,所以终止条件满足了。



考虑下面的例子:

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

当终止条件是一个表达式时,例如v - 0表达式被 评估 转换为常规整数,而不是变体,因此其硬值被推送到在运行时堆栈。因此,每次循环迭代都不会重新评估值。

另一个有趣的例子是:

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

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



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



对不起有点浑浊,但是有点晚了,但是我看到了这个,忍不住答应了一下。希望这是有道理的。嗯,好ol'VBA:)

编辑:

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


表达式[start-value],[end-value]和[step-increment] ,并且在以下任何计算之前。如果[start-value],[end-value]和[step-increment]的值不是Let-to-coercible Double,则会立即产生错误13(类型不匹配)。否则,请使用原始的未强制值继续执行以下算法。
$ b

根据以下
算法执行[for-statement]:
p>


  1. 如果[step-increment]的数据值为零或正数,
    和[bound -variable-expression]大于
    [end-value]的值,则立即执行[forstatement]
    ;如果[step-increment]的数据值是一个负数,并且[bound]的
    值-variable-expression]小于
    [end-value]的值时,[for-statement]的执行立即完成;
    ,否则进入步骤3.


  2. 执行[statement-block]。如果一个[嵌套语句]是
    ,那么它将被执行。最后,将
    [bound-variable-expression]的值添加到[step-increment]
    的值,并将Let-指定回[bound-variable-expression]。



    $ b

    我从这就是 intent 用于终止条件值一次和一次。如果我们看到证据表明,改变这个价值观会改变循环的初始条件,那么几乎可以肯定的是,由于它是一个变体,可能被称为非正式的意外重新评估。如果它是无意的,我们可能只能使用anecodtal证据来预测它的行为。

    如果运行时计算循环的开始/结束/步骤值,并将值将这些表达式添加到堆栈中,Variant值会在进程中引发byref wrench。如果运行时不首先识别变体,对其进行评估并将 值作为终止条件,那么奇怪的行为(如您所示)几乎肯定会随之而来。在这种情况下,VBA如何处理变体对于pcode分析来说是一个很好的任务,正如其他人所说的那样。


    $ b EDIT2:FOREACH



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


    在下面的任何一个计算之前,表达式[collection]被计算一次。
    $ b $ ol

  3. 如果[collection ]是一个数组:

    如果数组没有元素,那么立即执行[for-each-statement]



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



    在设置了[bound-variable-expression]之后, [statement-block]>被执行。如果存在[嵌套语句],则会执行它。



    一旦[语句块]和(如果存在)[nested-for-语句]>已经完成执行,[bound-variable-expression]被赋值给数组中的下一个元素(或者如果它是一个> Object的数组,则被赋值)。当且仅当数组中没有更多元素时,执行[for-each-statement]立即完成。否则,再次执行> [statement-block],如果>存在,则执行[nested-forstatement],并重复此步骤。

    当[for-each - 语句]已经完成执行,> [bound-variable-expression]的值是数组的最后一个元素的数据值。

  4. [collection]的数据值不是一个数组:

    [collection]的数据值必须是一个对外部对象的对象引用,定义枚举>接口。






















    $ b $在[bound-variable-expression]被设置之后,[statement-block]>被执行。如果存在[嵌套语句],则会执行它。



    一旦[语句块]和(如果存在)[nested-for-语句]>已经完成执行,[bound-variable-expression]以实现定义的方式被设置 - 分配给[collection]中的下一个元素。如果> [集合]中没有更多元素,则立即执行[for-each->语句]。否则,将重新执行[statement-block],然后是[嵌套for语句](如果存在),并重复此步骤。



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

使用这个作为基础,我认为变得很清楚,分配给一个变量然后变成绑定变量表达式的Variant生成了Array在这个例子中:

  Dim v As Variant,vv As Variant 
v = Array(1,1 ,1,1)
i = 1
'下面的任何注释行都会产生相同的RT错误:
For Each v In v'这个数组是固定的或者暂时被锁定的
'For Each vv In v
'v = 4
'ReDim Preserve v(LBound(v)To UBound(v))
如果i < UBound(v)然后v(i + 1)= i + 1',所以我们可以改变数组中的条目,而不是数组本身
i = i + 1
Debug.Print vv,'1 ,2,3,4
下一个

使用'v'作为[bound-variable -expression]创建一个Let-assignment回到V,这是由运行时阻止的,因为它正在进行一个枚举的目标来支持ForEach循环本身;也就是说,运行时会锁定变体,从而阻止循环向必须发生的变体赋予不同的值。



这也适用于'Redim保留' - 调整大小或更改数组,从而更改变体的赋值,将违反循环初始化时枚举目标上的锁定。

关于基于范围的赋值/迭代,请注意非对象元素的独立语义。 外部对象提供了一个特定于实现的枚举行为。一个Excel的Range对象有一个 _Default 属性,被对象名称引用时被调用,因为在这种情况下,当用作ForEach的迭代目标时隐式锁定(因此它不会产生锁定错误,因为它具有与Variant类别不同的​​语义):

  Debug.Print vbCrLf& Case10 range in range,
Set rng =范围(A1:D1).Cells.Cells添加任意数量
对于每个rng in rng'(另一个隐式的.Cells在这里? )
Debug.Print rng.Column,'1,2,3,4
下一个

_Default 属性可以通过在VBA对象浏览器中检查Excel对象库,通过突出显示Range对象,右键单击并选择Show Hidden成员)。

EDIT3:Collections





  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。打印obj.Column,'1只?
下一个

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。打印v.column,什么都没有!
下一个

这是一个真正的bug,当我第一次在VBA调试器中运行这两个样本时,他们按照OP提供的初始问题精确运行。然后,在几次测试之后重新启动例程,然后将代码恢复到其原始形式(如此处所示),后一种行为任意地开始与基于 的前辈的代码相匹配!只有当我停止Excel,并重新启动后,做了后者循环的原始行为(不打印任何东西),返回。

EDIT4带有变体的可重复行为



在注意到我在调试器中完成了某件事情之后,强制基于变种的迭代至少循环一次(就像Object版本一样),我终于发现改变行为的代码可重复的方式考虑这个原始的代码:

$ pre $ code> Dim v As Variant,vv As Variant

Set v = New Collection:For x = 1 To 4:v.Add Cells(x,x):Next x
'设置vv = v
对于每个v v
Debug.Print v.Column
下一个

这实际上是OP的原始情况,ForEach循环终止而没有一次迭代。现在,取消注释Set vv = v行,然后重新运行:现在For Each将迭代一次。我认为毫无疑问,我们在VB运行时的Variant评估机制中发现了一些非常(非常)的细微错误;另一个'Variant'等于循环变量的任意设置会强制在For Each评估中没有进行评估 - 我怀疑这是与Variant中Variant / Object / Collection 。添加这个伪造的集合似乎强制这个问题,并使循环操作为基于对象的版本。



EDIT5:对迭代和集合的最后一个想法



我最后一次编辑这个答案,但有一件事我不得不强迫自己,以确保我在观察奇怪的循环行为,当一个变量被用作'绑定变量表达式',特别是当它涉及到变体,有时这种行为是通过迭代改变绑定变量表达的内容来引发的。也就是说,如果你有:

  Dim v as Variant 
Dim vv as Variant
Set v = new Collection():for x = 1 to 4:v.Add Cells(x,x ):next
设置vv = v'占位符使循环kinda为v
中的每个v工作
做某事
下一个


$ b

至关重要的是记住(至少对于我来说)要牢记在For Each中,bound-variable-表达式'举行'V'获得因为这个迭代,也就是说,当我们开始循环时,v拥有一个Collection,并且枚举开始。但是,当枚举开始时,v的内容现在是枚举的产物,在这种情况下是一个Range对象(来自Cell)。这个行为可以在调试器中看到,你可以观察到v从Collection到Range;这意味着迭代中的下一个脚本将返回Range对象的枚举上下文将提供的任何内容,而不是集合。

这是一个很好的研究,我很欣赏反馈。这有助于我理解比我想象的更好的东西。除非有更多意见或疑问,否则我怀疑这是我最后一次编辑的答案。

Please take a look at the latest updates at the end of the post.

In Particular, see Update 4: the Variant comparison Curse


I’ve already seen mates banging their head against the wall to understand how a variant works, but never imagined that I will have my own bad moment with it.

I have successfully used the following VBA construction:

For i = 1 to i

This works perfectly when i is an Integer or any numeric type, iterating from 1 to the original value of i. I do this on occasions where i is a ByVal parameter - you might say lazy - to spare myself the declaration of a new variable.

Then I had a bug when this construct "stopped" working as expected. After some hard debugging, I found that it doesn’t work the same way when i is not declared as explicit numeric type, but a Variant. The question is twofold:

1- What are the exact semantics of the For and the For Each loops? I mean what is the sequence of actions that the compiler undertakes and in which order? For example, does the evaluation of the limit precede the initialization of the counter? Is this limit copied and "fixed" somewhere before the loop starts? Etc. The same question applies to For Each.

2- How to explain the different outcomes on variants and on explicit numeric types? Some say a variant is an (immutable) reference type, can this definition explain the observed behavior?

I have prepared an MCVE for different (independent) scenarios involving the For and the For Each statements, combined with integers, variants and objects. The surprising results urge for defining unambiguously the semantics or, for the least, check if those results do conform to the defined semantics.

All insights are welcome, including partial ones that explain some of the surprising results or their contradictions.

Thanks.

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

UPDATE 1

An interesting observation that can help understanding some of this. Concerning cases 7 and 8: if we hold another reference on the collection being iterated, the behavior changes completely:

    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

This means that in the initial case7 the collection being iterated was garbage-collected (due to reference counting) just after the variable obj was assigned to the first element of the collection. But this is still weird though. The compiler should have held some hidden reference on the object being iterated!? Compare this to case 6 where the array being iterated was "locked"...

UPDATE 2

The semantics of the For statement as defined by MSDN can be found on this page. You can see that it is explicitly stated that the end-value should be evaluated only once and before the execution of the loop proceeds. Should we consider this odd behavior as a compiler bug?

UPDATE 3

The intriguing case 7 again. The counter-intuitive behavior of case7 is not restricted to the (say unusual) iteration of a variable on itself. It may happen in a seemingly "innocent" code that, by mistake removes the only reference on the collection being iterated, leading to its garbage collection.

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

By intuition one expects that some hidden reference is held on the collection to stay alive during iteration. Not only it doesn't, but the program runs smoothly with no run-time error, leading probably to hard bugs. While the spec does not state any rule about manipulating objects under iteration, the implementation happens to protect and lock iterated Arrays (case 6) but neglects - doesn't even hold a dummy reference - on a collection (neither on a Dictionary, I've tested that also).

It's the responsibility of the programmer to care about the reference counting, which is not the "spirit" of VBA/VB6 and the architectural motivations behind reference counting.

UPDATE 4: The Variant Comparison Curse

Variants exhibit weird behaviors in many situations. In particular, comparing two Variants of different sub-types yields undefined results. Consider these simple examples:

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

As you can see, when both variables, the number and the string, were declared variants, the comparison is undefined. When at least one of them is explicitly typed, the comparison succeeds.

The same occurs when comparing for equality! For instance, ?2="2" returns True, but if you define two Variant variables, assign them those values and compare them, the comparison fails!

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

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

End Sub

解决方案

Please see edits below!

For Each edits also added below under Edit2

More edits about ForEach and Collections at Edit3

One last edit about ForEach and Collections at Edit4

A final note about iteration behavior at Edit5

Part of the subtlety of this odd behavior in the semantics of variant evaluation when used as a loop control variable or terminating condition.

In a nutshell, when a variant is the terminating value, or the control variable, the terminating value is naturally re-evaluated by the runtime with each iteration. A value type, however, such as an Integer, is pushed directly, and thus not re-evaluated (and its value doesn't change). If the control variable is an Integer, but the terminating value is a Variant, the Variant is coerced to an Integer on the first iteration, and pushed similarly. The same situation arises when the terminating condition is an expression involving a Variant and an Integer - it's coerced to an Integer.

In this example:

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

The variant v is assigned an integer value of 1, and the loop termination condition is re-evaluated because terminating variable is a variant - the runtime recognizes the presence of the Variant reference and forces re-evaluation with each iteration. As a result, the loop completes because of the in-loop reassignment. Because the variant now has a value of 1, the loop termination condition is satisfied.

Consider this next example:

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

When the terminating condition is an expression, such as "v - 0", the expression is evaluated and coerced to a regular integer, not a variant, and thus its hard value is pushed to the stack at runtime. As a result, the value is not re-evaluated upon each loop iteration.

The other interesting example:

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

behaves as it does because the control variable is an Integer, and thus the terminating variable is coerced to an integer as well, then pushed to the stack for iteration.

I cannot swear these are the semantics, but I believe the terminating condition or value is simply pushed onto a stack, thus the integer value is pushed, or the Variant's object reference is pushed, thus triggering the re-evaluation when the compiler realizes a variant holds the terminating value. When the variant gets reassigned within the loop, and the value is re-queried as the loop completes, the new value is returned, and the loop terminates.

Sorry if that's a little muddy, but it's kinda late, but I saw this and couldn't help but take a shot at an answer. Hope it makes some sense. Ah, good ol' VBA :)

EDIT:

Found some actual info from the VBA language spec at MS:

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.

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

  1. 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.

  2. 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.

  3. 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.

What I gather from this is that the intent is for the terminating condition value to be evaluated once and once only. If we see evidence that changing that value changes the behavior of the loop from its initial condition, it is almost certainly due to what might be termed informally as accidental re-evaluation because it's a variant. If it's unintentional, we can probably only use anecodtal evidence to predict its behavior.

If as the runtime evaluates a loop's start/end/step values, and pushes the "value" of those expressions onto the stack, a Variant value throws a "byref wrench" into the process. If the runtime does not first recognize the variant, evaluate it, and push that value as the terminating condition, curious behavior (as you are showing) would almost certainly ensue. Exactly how VBA handles variants in this case would be a great task for pcode analysis, as others have suggested.

EDIT2: FOREACH

The VBA spec again provides insight into the evaluation of ForEach loops over collections and arrays:

The expression [collection] is evaluated once prior to any of the >following computations.

  1. If the data value of [collection] is an array:

    If the array has no elements, then execution of the [for-each-statement] immediately completes.

    If the declared type of the array is Object, then the [bound-variable-expression] is Set-assigned to the first element in the >array. Otherwise, the [bound-variable-expression] is Let-assigned to the >first element in the array.

    After [bound-variable-expression] has been set, the [statement-block] >is executed. If a [nested-for-statement] is present, it is then executed.

    Once the [statement-block] and, if present, the [nested-for-statement] >have completed execution, [bound-variable-expression] is Let-assigned to >the next element in the array (or Set-assigned if it is an array of >Object). If and only if there are no more elements in the array, then >execution of the [for-each-statement] immediately completes. Otherwise, >[statement-block] is executed again, followed by [nested-forstatement] if >present, and this step is repeated.

    When the [for-each-statement] has finished executing, the value of >[bound-variable-expression] is the data value of the last element of the >array.

  2. If the data value of [collection] is not an array:

    The data value of [collection] must be an object-reference to an >external object that supports an implementation-defined enumeration >interface. The [bound-variable-expression] is either Let-assigned or >Set-assigned to the first element in [collection] in an >implementation->defined manner.

    After [bound-variable-expression] has been set, the [statement-block] >is executed. If a [nested-for-statement] is present, it is then executed.

    Once the [statement-block] and, if present, the [nested-for-statement] >have completed execution, [bound-variable-expression] is Set-assigned to >the next element in [collection] in an implementation-defined manner. If >there are no more elements in [collection], then execution of the [for-each->statement] immediately completes. Otherwise, [statement-block] is >executed again, followed by [nested-for-statement] if present, and this >step is repeated.

    When the [for-each-statement] has finished executing, the value of >[bound-variable-expression] is the data value of the last element in >[collection].

Using this as a base, I think it becomes clear that a Variant assigned to a variable that then becomes the bound-variable-expression generates the "Array is locked" error in this example:

    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

Using 'v' as the [bound-variable-expression] creates a Let-assignment back to V that is prevented by the runtime because it is the target of an enumeration underway to support the ForEach loop itself; that is, the runtime locks the variant, thus precluding the loop from assigning a different value to the variant as would necessarily have to occur.

This also applies to the 'Redim Preserve' - resizing or changing the array, thus changing the variant's assignment, is going to violate the lock placed on the enumeration target at the loop's initialization.

With regard to Range-based assignments/iteration, note the separate semantics for non-object elements kicks in; the "external objects" provide an implementation-specific enumeration behavior. An excel Range object has a _Default property that is being called when referenced by the object name only, as in this case, which does not take an implicit lock when used as the iteration target of the ForEach (and thus does not generate the locking error, as it has different semantics than the Variant variety):

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

(The _Default property can be identified by examining the Excel object library within the VBA Object Browser via highlighting the Range object ,right-clicking, and selecting "Show Hidden Members").

EDIT3: Collections

The code involving collections gets interesting and a little hairy :)

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

This is where nothing more than a genuine bug has to be considered at play. When I first ran these two samples in the VBA debugger, they ran precisely as the OP offered in the initial question. Then, after a restart of the routine following a few tests, but then restoring the code to its original form (as shown here), the latter behavior arbitrarily started matching that of the object-based predecessor above it! Only after I stopped Excel, and restarted it, did the original behavior of the latter loop (printing nothing), return. There's really no way to explain that other than a compiler bug.

EDIT4 Reproducible behavior with Variants

After noting that I'd done something within the debugger to force the variant-based iteration through a Collection to loop at least once (as it had with the Object version), I finally found a code-reproducible way of changing the behavior

Consider this original code:

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

This is essentially the OP's original case, and the ForEach loop terminates without a single iteration. Now, uncomment the 'Set vv=v' line, and re-run: now the For Each will iterate one time. I think there's no question that we've found some very (very!) subtle bug in Variant evaluation mechanism in the VB runtime; the arbitrary setting of another 'Variant' equal to the loop variable forces an evaluation that does not take place in the For Each evaluation - and I suspect that's tied to the fact that the Collection is represented within the Variant as a Variant/Object/Collection. Adding this bogus 'set' seems to force the issue and make the loop operate as the Object-based version does.

EDIT5: A final thought about iterations and collections

This will probably be my last edit to this answer, but one thing I had to force myself to be sure I recognized during the observation of odd loop behavior when a variables was used as the 'bound-variable-expression' and the limit expression was that, particularly when it comes to 'Variants', sometimes the behavior is induced by virtue of the iteration changing the contents of the 'bound-variable-expresssion.' That is, if you have:

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

it is vital to remember (at least it was for me) to keep in mind that within the For Each, the 'bound-variable-expression' held in 'v' gets changed by virtue of the iteration. That is, when we start the loop, v holds a Collection, and the enumeration begins. But when that enumeration starts, the contents of v are now the product of the enumeration - in this case, a Range object (from the Cell). This behavior can be seen in the debugger, as you can observe 'v' go from Collection to Range; meaning that the next kick in the iteration returns whatever the enumeration context of the Range object would provide, not the 'Collection.'

This has been a great study and I appreciate the feedback. It's helped me understand things even better than I thought. Unless there are more comments or questions on this, I suspect this will be my last edit to the answer.

这篇关于变量迭代本身 - 不同类型的行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆