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:
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.
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.
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.
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.
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 for