vba - variable - macros en excel
Variable que se itera sobre sí misma: comportamiento diferente con diferentes tipos (1)
Por favor, eche un vistazo a las últimas actualizaciones al final de la publicación.
En particular, consulte la Actualización 4: la maldición de comparación de variantes
Ya he visto compañeros golpeándose la cabeza contra la pared para entender cómo funciona una variante, pero nunca imaginé que iba a tener mi propio mal momento.
He utilizado con éxito la siguiente construcción de VBA:
For i = 1 to i
Esto funciona perfectamente cuando i
soy un Entero o cualquier tipo numérico, iterando desde 1 hasta el valor original de i
. Hago esto en ocasiones en las que soy un parámetro de ByVal
, podría decirse flojo, para ahorrarme la declaración de una nueva variable.
Luego tuve un error cuando esta construcción "paró" funcionando como se esperaba. Después de una depuración exhaustiva, descubrí que no funciona de la misma manera cuando no se declara como un tipo numérico explícito, sino como una Variant
. La pregunta es doble:
1- ¿Cuál es la semántica exacta de los bucles For
y For Each
? Quiero decir, ¿cuál es la secuencia de acciones que emprende el compilador y en qué orden? Por ejemplo, ¿la evaluación del límite precede a la inicialización del contador? ¿Se copia este límite y se "arregla" en alguna parte antes de que comience el ciclo? Etc. La misma pregunta se aplica a For 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 un MCVE para diferentes escenarios (independientes) que implican las declaraciones For
y For Each
, combinadas con enteros, variantes y objetos. Los resultados sorprendentes instan a definir inequívocamente la semántica o, por lo menos, verificar 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 en la colección que se itera, 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 colección que se iteraba se recogió como basura (debido al recuento de referencias) justo después de que se asignara la variable obj
al primer elemento de la colección. Pero esto aún es extraño. El compilador debería haber mantenido alguna referencia oculta en el objeto que se itera !? Compare esto con el caso 6 donde la matriz que se iteraba estaba "bloqueada" ...
ACTUALIZACIÓN 2
La semántica de la instrucción For
tal como se define en MSDN se puede encontrar en esta página . Puede ver que se establece explícitamente que el end-value
debe evaluarse solo una vez y antes de que la ejecución del bucle continúe. ¿Deberíamos considerar este extraño comportamiento como un error del compilador?
ACTUALIZACIÓN 3
El intrigante caso 7 otra vez. El comportamiento contraintuitivo de case7 no está restringido 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 itera, 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 uno espera que alguna referencia oculta se mantenga en la colección para mantenerse vivo durante la iteración. No solo no funciona, sino que el programa se ejecuta sin problemas, sin errores en el tiempo de ejecución, lo que probablemente conduce a errores graves. Si bien la especificación no establece ninguna regla sobre la manipulación de objetos bajo iteración, la implementación protege y bloquea matrices iteradas (caso 6) pero descuida (ni siquiera contiene una referencia ficticia) en una colección (ni en un diccionario, I '' he probado eso también).
Es responsabilidad del programador preocuparse por el recuento de referencias, que no es el "espíritu" de VBA / VB6 y las motivaciones arquitectónicas detrás del recuento de referencias.
ACTUALIZACIÓN 4: La maldición de comparación de variantes
Variant
exhiben comportamientos extraños en muchas situaciones. En particular, la comparación de dos variantes de diferentes subtipos produce 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 se escribe explícitamente, la comparación tiene éxito.
¡Lo mismo ocurre cuando se compara por igualdad! Por ejemplo ?2="2"
devuelve True, pero si define dos variables de Variant
, 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
Por favor, mira las ediciones a continuación!
Para cada ediciones también se agrega debajo en Editar2
Más ediciones sobre ForEach y colecciones en Edit3
Una última edición sobre ForEach y colecciones en Edit4
Una nota final sobre el comportamiento de iteración en Edit5
Parte de la sutileza de este extraño comportamiento en la semántica de la evaluación de variante cuando se usa como una variable de control de bucle o condición de terminación.
En pocas palabras, cuando una variante es el valor de terminación, o la variable de control, el valor de terminación es naturalmente reevaluado por el tiempo de ejecución con cada iteración. Sin embargo, un tipo de valor , como un Integer
, se envía directly
y, por lo tanto, no se vuelve a evaluar (y su valor no cambia). Si la variable de control es un Integer
, pero el valor de terminación es una Variant
, la Variant
se fuerza a un Integer
en la primera iteración y se empuja de manera similar. La misma situación se presenta cuando la condición de terminación es una expresión que implica una Variant
y un Integer
: se fuerza a un Integer
.
En este ejemplo:
Dim v as Variant
v=4
for v= 1 to v
Debug.print v,
next
La variante v tiene asignado un valor entero de 1, y la condición de terminación del bucle se vuelve a evaluar porque la variable de terminación es una variante: el tiempo de ejecución reconoce la presencia de la referencia de la variante y fuerza la reevaluación con cada iteración. Como resultado, el bucle se completa debido a la reasignación en bucle. Como la variante ahora tiene un valor de 1, se cumple la condición de terminación del ciclo.
Considera este próximo ejemplo:
Dim v as variant
v=4
for v=1 to v-0
Debug.Print v,
next
Cuando la condición de terminación es una expresión , como "v - 0", la expresión se evalúa y coacciona a un entero regular , no a una variante, y por lo tanto su valor fijo se envía a la pila en tiempo de ejecución. Como resultado, el valor no se vuelve a evaluar en cada iteración de bucle.
El otro ejemplo interesante:
Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
v=i-1
Debug.print i,
next
se comporta como lo hace porque la variable de control es un Entero y, por lo tanto, la variable de terminación también se fuerza a un entero, luego se empuja a la pila para la iteración.
No puedo jurar que esta es la semántica, pero creo que la condición o el valor de terminación simplemente se empuja hacia una pila, por lo tanto, se empuja el valor entero o se empuja la referencia de objeto de la variante, lo que provoca la reevaluación cuando el compilador realiza una variante contiene el valor de terminación. Cuando la variante se reasigna dentro del ciclo, y el valor se vuelve a consultar a medida que se completa el ciclo, se devuelve el nuevo valor y el ciclo finaliza.
Lo siento si está un poco embarrado, pero es un poco tarde, pero vi esto y no pude evitar intentar una respuesta. Espero que tenga algo de sentido. Ah, buen viejo ''VBA :)
EDITAR:
Se encontró información real de la especificación de idioma de VBA en MS:
Las expresiones [valor inicial], [valor final] e [incremento paso] se evalúan una vez, en orden y antes de cualquiera de los siguientes cálculos. Si el valor de [valor inicial], [valor final], e [incremento por pasos] no se vuelven coercibles para doblar, el error 13 (desajuste de tipo) se plantea inmediatamente. De lo contrario, proceda con el siguiente algoritmo utilizando los valores originales no cobrados.
La ejecución del [for-statement] procede de acuerdo con el siguiente algoritmo:
Si el valor de datos de [step-increment] es cero o un número positivo, y el valor de [bound-variable-expression] es mayor que el valor de [end-value], entonces la ejecución de [forstatement] se completa inmediatamente; de lo contrario, avance al Paso 2.
Si el valor de datos de [step-increment] es un número negativo, y el valor de [bound-variable-expression] es menor que el valor de [end-value], la ejecución de [for-statement] se completa inmediatamente; de lo contrario, avance al Paso 3.
El [bloque de instrucción] se ejecuta. Si está presente [anidado-para-enunciado], entonces se ejecuta. Finalmente, el valor de [bound-variable-expression] se agrega al valor de [step-increment] y Let-assigned regresa a [bound-variable-expression]. La ejecución se repite en el paso 1.
Lo que deduzco de esto es que la intención es que el valor de la condición de terminación sea evaluado una sola vez . Si vemos evidencia de que cambiar ese valor cambia el comportamiento del bucle de su condición inicial, es casi seguro debido a lo que podría llamarse informalmente una reevaluación accidental porque es una variante. Si no es intencional, probablemente solo podamos usar evidencia anecdótica para predecir su comportamiento.
Si como el tiempo de ejecución evalúa los valores de inicio / fin / paso de un bucle, y empuja el "valor" de esas expresiones en la pila, un valor de Variante arroja una "llave byref" en el proceso. Si el tiempo de ejecución no reconoce primero la variante, la evalúa y empuja ese valor como condición de terminación, es muy probable que se produzca un comportamiento curioso (como se muestra). Exactamente cómo VBA maneja las variantes en este caso sería una gran tarea para el análisis de pcode, como otros han sugerido.
EDIT2: FOREACH
La especificación de VBA nuevamente proporciona información sobre la evaluación de los bucles ForEach sobre colecciones y matrices:
La expresión [colección] se evalúa una vez antes de cualquiera de los siguientes cómputos.
Si el valor de datos de [colección] es una matriz:
Si el conjunto no tiene elementos, la ejecución del [for-each-statement] se completa inmediatamente.
Si el tipo declarado de la matriz es Object, entonces [bound-variable-expression] es Set-assigned al primer elemento en la> matriz. De lo contrario, [bound-variable-expression] es Let-assigned al> primer elemento de la matriz.
Después de que se ha establecido [bound-variable-expression], se ejecuta [statement-block]>. Si está presente [anidado-para-enunciado], entonces se ejecuta.
Una vez que [statement-block] y, si está presente, el [nested-for-statement]> ha completado la ejecución, [bound-variable-expression] se Let-assigned to> el siguiente elemento en el conjunto (o Set-assigned if es una matriz de> Objeto). Si y solo si no hay más elementos en la matriz, entonces> la ejecución de [para-cada-declaración] se completa inmediatamente. De lo contrario,> [instrucción-bloque] se ejecuta de nuevo, seguido por [declaración-anidada] si> presente, y este paso se repite.
Cuando el [for-each-statement] ha terminado de ejecutarse, el valor de> [bound-variable-expression] es el valor de datos del último elemento de la matriz.
Si el valor de datos de [colección] no es una matriz:
El valor de datos de [colección] debe ser una referencia de objeto a un> objeto externo que admita una enumeración> interfaz definida por la implementación. El [bound-variable-expression] es Let-assigned o> Set-assigned al primer elemento en [collection] de una manera> implementation-> defined.
Después de que se ha establecido [bound-variable-expression], se ejecuta [statement-block]>. Si está presente [anidado-para-enunciado], entonces se ejecuta.
Una vez que [statement-block] y, si está presente, [anidado-para-declaración]> han completado la ejecución, [bound-variable-expression] está Set-assigned a> el siguiente elemento en [collection] en una implementación definida manera. Si> no hay más elementos en [colección], la ejecución de la [instrucción for-each-> se completa inmediatamente. De lo contrario, [bloque de instrucción]> se ejecuta de nuevo, seguido de [anidado-para-enunciado] si está presente, y este> paso se repite.
Cuando el [for-each-statement] ha terminado de ejecutarse, el valor de> [bound-variable-expression] es el valor de datos del último elemento en> [collection].
Usando esto como base, creo que queda claro que una Variante asignada a una variable que luego se convierte en la expresión de variable enlazada genera el error "La matriz está bloqueada" en este ejemplo:
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
El uso de ''v'' como [expresión-variable-encuadernada] crea una asignación-De vuelta a V que impide el tiempo de ejecución porque es el objetivo de una enumeración en curso para admitir el bucle ForEach mismo; es decir, el tiempo de ejecución bloquea la variante, impidiendo así que el ciclo asigne un valor diferente a la variante, ya que necesariamente tendría que ocurrir.
Esto también se aplica a la ''Redim Preserve'' - cambiar el tamaño o cambiar la matriz, cambiando la asignación de la variante, va a violar el bloqueo colocado en el objetivo de enumeración en la inicialización del bucle.
Con respecto a las asignaciones / iteraciones basadas en rangos, observe la semántica separada para los elementos no-objetos que entran en juego; los "objetos externos" proporcionan un comportamiento de enumeración específico de la implementación . Un objeto Excel Range tiene una propiedad _Default
que se llama cuando se hace referencia solo por el nombre del objeto, como en este caso, que no toma un bloqueo implícito cuando se utiliza como el objetivo de iteración de ForEach (y por lo tanto no genera el error de bloqueo , ya que tiene una semántica diferente a la variedad 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
(La propiedad _Default
se puede identificar examinando la biblioteca de objetos de Excel dentro del navegador de objetos de VBA resaltando el objeto de rango, haciendo clic con el botón derecho y seleccionando "Mostrar miembros ocultos").
EDIT3: Colecciones
El código que involucra colecciones se vuelve interesante y un poco peludo :)
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
Aquí es donde nada más que un error genuino debe ser considerado en juego. La primera vez que ejecuté estas dos muestras en el depurador de VBA, se ejecutaron exactamente como el OP ofrecido en la pregunta inicial. Luego, después de reiniciar la rutina después de algunas pruebas, pero luego restaurar el código a su forma original (como se muestra aquí), el último comportamiento arbitrariamente comenzó a coincidir con el del predecesor basado en objetos sobre él. Solo después de detener Excel y reiniciarlo, el comportamiento original del último bucle (sin imprimir nada), regresa. Realmente no hay forma de explicar eso que no sea un error del compilador.
EDIT4 Comportamiento reproducible con variantes
Después de señalar que había hecho algo dentro del depurador para forzar a la iteración basada en variantes a través de una Colección a repetir al menos una vez (como lo hizo con la versión de Object), finalmente encontré una forma de cambiar el comportamiento reproducible por código
Considera este código original:
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
Este es esencialmente el caso original de OP, y el bucle ForEach termina sin una sola iteración. Ahora, elimine el comentario de la línea ''Establecer vv = v'' y vuelva a ejecutar: ahora For Each repetirá una vez. Creo que no hay dudas de que hemos encontrado algún error muy (¡muy!) Sutil en el mecanismo de evaluación de Variant en el tiempo de ejecución de VB; la configuración arbitraria de otra ''Variante'' igual a la variable de ciclo obliga a una evaluación que no tiene lugar en la evaluación For Each - y sospecho que está ligada al hecho de que la Colección está representada dentro de la Variante como una Variante / Objeto / Colección . Agregar este falso ''conjunto'' parece forzar el problema y hacer que el bucle opere como lo hace la versión basada en Objetos.
EDIT5: Un pensamiento final sobre iteraciones y colecciones
Esta será probablemente la última edición de esta respuesta, pero tuve que forzarme a asegurarme de reconocer durante la observación del comportamiento de bucle impar cuando se usaba una variable como ''expresión-variable-enlazada'' y la expresión límite era que, particularmente cuando se trata de ''Variantes'', a veces el comportamiento se induce en virtud de la iteración cambiando el contenido de la ''variable-variable-expresión''. Es decir, si tiene:
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
es vital recordar (al menos fue para mí) tener en cuenta que dentro de For Each, la ''expresión de variable enlazada'' contenida en ''v'' se cambia en virtud de la iteración. Es decir, cuando iniciamos el ciclo, v contiene una Colección y comienza la enumeración. Pero cuando comienza esa enumeración, el contenido de v ahora es el producto de la enumeración , en este caso, un objeto Range (desde la Celda). Este comportamiento se puede ver en el depurador, ya que puede observar que ''v'' pasa de Collection a Range; lo que significa que la próxima patada en la iteración devuelve todo lo que el contexto de enumeración del objeto Range proporcionaría, no la ''Colección''.
Este ha sido un gran estudio y agradezco los comentarios. Me ayudó a entender las cosas incluso mejor de lo que pensaba. A menos que haya más comentarios o preguntas sobre esto, sospecho que esta será mi última edición de la respuesta.