【问题标题】:Variable iterating on itself - different behavior with different types变量自身迭代 - 不同类型的不同行为
【发布时间】:2017-07-10 07:16:11
【问题描述】:

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

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


我已经看到队友们用头撞墙以了解变体的工作原理,但从没想过我会遇到自己的糟糕时刻。

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

For i = 1 to i

i整数 或任何数字类型时,这非常有效,从1 迭代到i原始值。我在 iByVal 参数的情况下这样做 - 你可能会说懒惰 - 以免自己声明一个新变量。

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

1- ForFor Each 循环的确切语义是什么?我的意思是编译器执行的操作顺序是什么以及顺序是什么?例如,限制的评估是否先于计数器的初始化?在循环开始之前,这个限制是否被复制和“固定”?等等。同样的问题也适用于For Each

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

我为涉及ForFor 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


【解决方案1】:

请参阅下面的修改!

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

Edit3 上有关 ForEach 和 Collections 的更多编辑

在 Edit4 上关于 ForEach 和 Collections 的最后一次编辑

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

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

简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值。但是,value 类型(例如 Integer)会被推送到 directly,因此不会重新评估(并且其值不会改变)。如果控制变量是Integer,但终止值是Variant,则Variant 在第一次迭代时被强制转换为Integer,并以类似方式推送。当终止条件是涉及 VariantInteger 的表达式时,也会出现同样的情况 - 它被强制转换为 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 的 对象引用 被推送,从而在编译器意识到变量持有终止值时触发重新评估。当变量在循环中被重新分配,并且在循环完成时重新查询值时,返回新值,循环终止。

对不起,如果这有点泥泞,但有点晚了,但我看到了这一点,忍不住想回答一下。希望它有一些意义。啊,很好的 VBA :)

编辑:

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

表达式 [start-value]、[end-value] 和 [step-increment] 在以下任何计算之前按顺序计算一次。如果 [start-value]、[end-value] 和 [step-increment] 的值不是 Let-coercible 到 Double,则会立即引发错误 13(类型不匹配)。否则,请使用原始的非强制值继续执行以下算法。

[for-statement] 的执行按照以下方式进行 算法:

  1. 如果[step-increment]的数据值为零或正数, 并且 [bound-variable-expression] 的值大于 [end-value] 的值,然后执行 [forstatement] 立即完成;否则,进入第 2 步。

  2. 如果 [step-increment] 的数据值为负数,则 [bound-variable-expression] 的值小于 [end-value],[for-statement] 的执行立即完成; 否则,进入第 3 步。

  3. [语句块]被执行。如果 [nested-for-statement] 是 存在,然后执行。最后,价值 [bound-variable-expression] 被添加到 [step-increment] 的值上 并将 Let-assign 回 [bound-variable-expression]。然后执行 在第 1 步重复。

我从中收集到的是,intent 是为了评估终止条件值一次且仅一次。如果我们看到证据表明更改该值会改变循环从其初始条件的行为,那几乎可以肯定是由于可能被非正式地称为意外重新评估,因为它是一个变体。如果是无意的,我们可能只能使用轶事证据来预测它的行为。

如果在运行时评估循环的开始/结束/步骤值,并将这些表达式的“值”压入堆栈,则 Variant 值会将“byref 扳手”扔到进程中。如果运行时没有首先识别变体、评估它并将 那个 值作为终止条件,那么几乎肯定会出现奇怪的行为(如您所展示的)。正如其他人所建议的那样,VBA 在这种情况下如何处理变体对于 pcode 分析来说是一项伟大的任务。

EDIT2:FOREACH

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

表达式 [collection] 在任何 >以下计算之前被评估一次。

  1. 如果[collection]的数据值为数组:

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

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

    [bound-variable-expression] 被设置后,[statement-block] > 被执行。如果存在 [nested-for-statement],则执行它。

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

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

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

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

    [bound-variable-expression] 被设置后,[statement-block] > 被执行。如果存在 [nested-for-statement],则执行它。

    一旦 [statement-block] 和 [nested-for-statement] > 已完成执行,[bound-variable-expression] 将被 Set 分配给 > [collection] 中的下一个元素实现定义的方式。如果在 [collection] 中没有更多元素,则 [for-each->statement] 的执行立即完成。否则,[statement-block] > 再次执行,如果存在 [nested-for-statement],则 > 重复该步骤。

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

以此为基础,我认为很明显,分配给变量的 Variant 然后成为绑定变量表达式会在此示例中生成“数组已锁定”错误:

    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 赋值,运行时会阻止它,因为它是支持 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

_Default 属性可以通过在 VBA 对象浏览器中通过突出显示 Range 对象、右键单击并选择“显示隐藏的成员”来检查 Excel 对象库来识别)。

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

这里只需要考虑一个真正的错误。当我第一次在 VBA 调试器中运行这两个示例时,它们的运行与最初问题中提供的 OP 完全相同。然后,在经过几次测试后重新启动例程,但随后将代码恢复到其原始形式(如此处所示),后一种行为任意开始与上面基于 object 的前任相匹配!只有在我停止 Excel 并重新启动它之后,后一个循环的 original 行为(不打印任何内容)才返回。除了编译器错误之外,真的没有其他方法可以解释。

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

这本质上是 OP 的原始情况,并且 ForEach 循环在没有一次迭代的情况下终止。现在,取消注释 'Set vv=v' 行,然后重新运行:现在 For Each 将迭代一次。我认为毫无疑问,我们在 VB 运行时的 Variant 评估机制中发现了一些非常(非常!)微妙的错误;另一个等于循环变量的“变体”的任意设置会强制进行在 For Each 评估中不发生的评估 - 我怀疑这与 Collection 在 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
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 对象的枚举上下文将提供的任何内容,而不是“集合”。

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

【讨论】:

  • Dim v, vv: v = 5: For vv = 1 To v: v = 2: Debug.Print vv: Next ----> 1, 2, 3, 4, 5。看,即使使用变体控制变量,它也是如此。谜团是为什么当For v = 1 to v 时它没有做同样的事情???
  • 我必须再次感谢您的努力。我认为我必须在我的问题中添加a link to that page 以方便进一步调查。在我看来,编译器确实不符合For v = 1 to v 语句中的语义。
  • 我们应该将其视为编译器错误吗?
  • @A.S.H.不客气 - 享受研究!我认为它至少是运行时中的一个错误。就 pcode 而言,编译器可能正在做它应该做的事情,但循环中变体的 runtime 评估可能是问题所在。无论哪种方式,我都闻起来像虫子。
  • 是的,这显然是一个即使在 VB6 代码编译为可执行文件时仍然存在的错误。您可以在VBForums 中发布一个帖子,并且可能用户The trick 可以通过检查(错误地)生成的x86 程序集来解释问题所在。
猜你喜欢
  • 2016-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-28
  • 2011-05-20
  • 1970-01-01
  • 1970-01-01
  • 2011-05-19
相关资源
最近更新 更多