Variable iterando sobre sí mismo: comportamiento diferente con diferentes tipos

Por favor, eche un vistazo a las últimas actualizaciones al final de la publicación.

En particular, verActualización 4: la maldición de comparación de variantes

Ya he visto compañeros golpeándose la cabeza contra la pared para comprender cómo funciona una variante, pero nunca imaginé que tendría mi propio mal momento con ella.

He utilizado con éxito la siguiente construcción de VBA:

For i = 1 to i

Esto funciona perfectamente cuandoi es unEntero o cualquier tipo numérico, iterando del 1 alValor original dei. Hago esto en ocasiones dondei es unByVal parámetro, se podría decir vago, para ahorrarme la declaración de una nueva variable.

Luego tuve un error cuando esta construcción "dejó de funcionar" como se esperaba. Después de una fuerte depuración, descubrí que no funciona de la misma manera cuandoi no se declara como tipo numérico explícito, sino unVariant. La pregunta es doble:

1- ¿Cuáles son las semánticas exactas de laFor y elFor Each bucles? Quiero decir, ¿cuál es la secuencia de acciones que realiza el compilador y en qué orden? Por ejemplo, ¿la evaluación del límite precede a la inicialización del contador? ¿Este límite se copia y "arregla" en algún lugar antes de que comience el ciclo? Etc. La misma pregunta se aplica aFor Each.

2- ¿Cómo explicar los diferentes resultados en variantes y en tipos numéricos explícitos? Algunos dicen que una variante es un tipo de referencia (inmutable), ¿puede esta definición explicar el comportamiento observado?

He preparado unMCVE para diferentes escenarios (independientes) que involucranFor y elFor Each declaraciones, combinadas con enteros, variantes y objetos. Los sorprendentes resultados instan a definirinequívocamente la semántica o, por lo menos, verifique si esos resultados se ajustan a la semántica definida.

Todas las ideas son bienvenidas, incluidas las parciales que explican algunos de los resultados sorprendentes o sus contradicciones.

Gracias.

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

ACTUALIZACIÓN 1

Una observación interesante que puede ayudar a comprender algo de esto. Con respecto a los casos 7 y 8: si tenemos otra referencia sobre la colección que se está iterando, el comportamiento cambia completamente:

    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

Esto significa que en el caso inicial7, la recolección que se está iterando fue recolectada como basura (debido al recuento de referencias) justo después de la variableobj fue asignado al primer elemento de la colección. Pero esto sigue siendo extraño sin embargo. ¿El compilador debería haber tenido alguna referencia oculta sobre el objeto que se está iterando? Compare esto con el caso 6 donde la matriz que se está iterando estaba "bloqueada" ...

ACTUALIZACIÓN 2

La semántica de laFor declaración como se define por MSDN se puede encontraren esta página. Puede ver que se declara explícitamente que elend-value debe evaluarse solo una vez y antes de que continúe la ejecución del bucle. ¿Deberíamos considerar este comportamiento extraño como un error del compilador?

ACTUALIZACIÓN 3

El intrigante caso 7 nuevamente. loscontra-intuitivo El comportamiento de case7 no se limita a la iteración (digamos inusual) de una variable en sí misma. Puede suceder en un código aparentemente "inocente" que, por error, elimina la única referencia en la colección que se está iterando, lo que lleva a su recolección de basura.

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

Por intuición, se espera que alguna referencia oculta en la colección se mantenga viva durante la iteración. No solo no lo hace, sino que el programa se ejecuta sin problemas y sin errores de tiempo de ejecución, lo que probablemente provoque errores graves. Si bien la especificación no establece ninguna regla sobre la manipulación de objetos bajo iteración, la implementación pasa a proteger ybloquear iteraciones de matrices (caso 6) pero descuida, ni siquiera tiene una referencia ficticia, en una colección (ni en un diccionario, lo he probado también).

Es responsabilidad del programador preocuparse por el conteo de referencias, que no es el "espíritu" de VBA / VB6 y las motivaciones arquitectónicas detrás del conteo de referencias.

ACTUALIZACIÓN 4: La maldición de comparación de variantes

Variants exhiben comportamientos extraños en muchas situaciones. En particular,La comparación de dos variantes de diferentes subtipos arroja resultados indefinidos. Considere estos ejemplos simples:

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

Como puede ver, cuando ambas variables, el número y la cadena, se declararon variantes, la comparación no está definida. Cuando al menos uno de ellos está escrito explícitamente, la comparación tiene éxito.

¡Lo mismo ocurre cuando se compara por la igualdad! Por ejemplo,?2="2" devuelve True, pero si define dosVariant variables, asígneles esos valores y compárelos, ¡la comparación falla!

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

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

End Sub

Respuestas a la pregunta(1)

Su respuesta a la pregunta