gpt4 book ai didi

vba - 自身进行变量迭代-不同类型的不同行为

转载 作者:行者123 更新时间:2023-12-03 08:49:52 24 4
gpt4 key购买 nike

请查看文章末尾的最新更新。

特别是,请参见更新4:变体比较Curse

我已经见过同伴撞墙撞头,以了解变体的工作原理,但从未想象过我会为此感到难过。

我已成功使用以下VBA构造:
For i = 1 to i
i是Integer或任何数字类型(从1迭代到i的原始值)时,此方法非常有效。我在iByVal参数(您可能会说是懒惰的)的情况下这样做,以免自己声明新变量。

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

1- ForFor Each循环的确切语义是什么?我的意思是编译器按照什么顺序执行 Action 顺序?例如,限制的评估是否在计数器初始化之前进行?循环开始之前,此限制是否已复制并“固定”在某个地方?等等,同样的问题也适用于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

凭直觉,人们希望在集合上保留一些隐藏的引用,以便在迭代过程中保持 Activity 状态。不仅如此,而且程序运行平稳,没有运行时错误,可能会导致硬错误。虽然规范没有说明有关在迭代过程中操作对象的任何规则,但是该实现恰好是保护和 锁定迭代数组(案例6),但忽略了-甚至没有保存伪引用-在集合上(既不在Dictionary上,我也测试过)。

程序员有责任关心引用计数,这不是VBA / VB6的“精神”和引用计数背后的体系结构动机。

更新4:变体比较诅咒
Variant在许多情况下都表现出怪异的行为。特别是, 比较不同子类型的两个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

如您所见,当两个变量(数字和字符串)都声明为变量时,比较是不确定的。当至少其中之一被显式键入时,比较成功。

比较相等时也会发生同样的情况!例如, ?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

最佳答案

请查看下面的修改!

对于每个编辑,还在Edit2 下添加了以下内容

在Edit3上对ForEach和集合的更多编辑

在Edit4上关于ForEach和集合的最后一个编辑

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

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

简而言之,当变量是终止值或控制变量时,运行时自然会在每次迭代时重新评估终止值。但是,值类型(例如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,并且因为终止变量是变量,所以重新评估了循环终止条件-运行时会识别变量引用的存在,并在每次迭代时强制重新评估。结果,由于回路内重新分配,回路完成。因为变量现在的值为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

行为之所以如此,是因为控制变量是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.

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.



我从中得到的是,目的是要一次评估一次终止条件值。如果我们看到改变该值的证据从其初始状态改变了回路的行为,则几乎可以肯定是由于它是一个变体,可能被非正式地称为意外重新评估。如果不是故意的,我们可能只能使用轶事证据来预测其行为。

如果在运行时评估循环的开始/结束/步骤值,并将这些表达式的“值”压入堆栈,则Variant值会将“byref Spanner ”扔到进程中。如果运行时没有首先 识别变体,对其进行评估,然后将该值作为终止条件,则几乎可以肯定会发生奇怪的行为(如您所示)。正如其他人所建议的那样,在这种情况下,VBA究竟如何处理变体将是pcode分析的一项艰巨任务。

编辑2:FOREACH

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

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



以此为基础,我认为很明显,在此示例中,分配给变量的Variant然后变为bound-variable-expression会生成“数组已锁定”错误:
    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的语义):
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属性)。

编辑3:集合

涉及集合的代码变得有趣并且有点毛茸茸:)
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版本一样),我终于找到了一种可代码重现的更改行为的方式

考虑以下原始代码:
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”且极限表达式为特别是涉及“变量”时,有时是由于迭代更改了“绑定(bind)变量表达式”的内容而引起了这种行为。也就是说,如果您具有:
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”。

这是一项很棒的研究,我感谢您的反馈。它帮助我比我想象的更好地理解了事情。除非对此有更多评论或疑问,否则我怀疑这将是我对答案的最后修改。

关于vba - 自身进行变量迭代-不同类型的不同行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42354228/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com