Iteração variável em si mesma - comportamento diferente com tipos diferentes
Dê uma olhada nas atualizações mais recentes no final do post.
Em particular, consulteAtualização 4: a maldição de comparação de variantes
Eu já vi companheiros batendo a cabeça na parede para entender como uma variante funciona, mas nunca imaginei que terei meu próprio momento ruim com ela.
Utilizei com êxito a seguinte construção do VBA:
For i = 1 to i
Isso funciona perfeitamente quandoi
é umInteiro ou qualquer tipo numérico, iterando de 1 aovalor original doi
. Eu faço isso em ocasiões em quei
é umByVal
parâmetro - você pode dizer preguiçoso - para me poupar da declaração de uma nova variável.
Então tive um bug quando essa construção "parou" de funcionar como esperado. Após uma depuração difícil, descobri que não funciona da mesma maneira quandoi
não é declarado como tipo numérico explícito, mas umVariant
. A questão é dupla:
1- Quais são as semânticas exatas dosFor
e aFor Each
rotações? Quero dizer, qual é a sequência de ações que o compilador realiza e em qual ordem? Por exemplo, a avaliação do limite precede a inicialização do contador? Esse limite é copiado e "corrigido" em algum lugar antes do início do loop? Etc. A mesma pergunta se aplica aFor Each
.
2- Como explicar os diferentes resultados em variantes e em tipos numéricos explícitos? Alguns dizem que uma variante é um tipo de referência (imutável). Essa definição pode explicar o comportamento observado?
Eu preparei umMCVE para diferentes cenários (independentes) envolvendo aFor
e aFor Each
instruções, combinadas com números inteiros, variantes e objetos. Os resultados surpreendentes exigem a definiçãoinequivocamente a semântica ou, pelo menos, verifique se esses resultados estão em conformidade com a semântica definida.
Todas as idéias são bem-vindas, incluindo as parciais que explicam alguns dos resultados surpreendentes ou suas contradições.
Obrigado.
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
ATUALIZAÇÃO 1
Uma observação interessante que pode ajudar a entender parte disso. Com relação aos casos 7 e 8: se mantivermos outra referência na coleção que está sendo iterada, o comportamento mudará 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
Isso significa que, no caso inicial7, a coleção sendo iterada foi coletada como lixo (devido à contagem de referência) logo após a variávelobj
foi atribuído ao primeiro elemento da coleção. Mas isso ainda é estranho. O compilador deve ter mantido alguma referência oculta no objeto que está sendo iterado !? Compare isso com o caso 6, em que a matriz que está sendo iterada estava "bloqueada" ...
ATUALIZAÇÃO 2
A semântica doFor
pode ser encontrada a instrução definida pelo MSDNnesta página. Você pode ver que está explicitamente declarado que oend-value
deve ser avaliado apenas uma vez e antes da execução do loop. Devemos considerar esse comportamento estranho como um bug do compilador?
ATUALIZAÇÃO 3
O caso intrigante 7 novamente. ocontra-intuitivo o comportamento de case7 não se restringe à (digamos incomum) iteração de uma variável em si mesma. Isso pode acontecer em um código aparentemente "inocente" que, por engano, remove a única referência na coleção que está sendo iterada, levando à sua coleta de lixo.
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 intuição, espera-se que alguma referência oculta seja mantida na coleção para permanecer viva durante a iteração. Não apenas isso não acontece, mas o programa é executado sem problemas em tempo de execução, levando provavelmente a erros graves. Enquanto a especificação não declara nenhuma regra sobre manipulação de objetos sob iteração, a implementação acontece para proteger etrava Matrizes iteradas (caso 6), mas negligencia - nem mesmo mantém uma referência fictícia - em uma coleção (nem em um dicionário, eu testei isso também).
É responsabilidade do programador se preocupar com a contagem de referência, que não é o "espírito" do VBA / VB6 e as motivações arquitetônicas por trás da contagem de referência.
ATUALIZAÇÃO 4: A maldição de comparação de variantes
Variant
s exibem comportamentos estranhos em muitas situações. Em particular,comparar duas variantes de diferentes subtipos gera resultados indefinidos. Considere estes exemplos 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 você pode ver, quando as duas variáveis, o número e a sequência, foram declaradas variantes, a comparação é indefinida. Quando pelo menos um deles é digitado explicitamente, a comparação é bem-sucedida.
O mesmo ocorre quando se compara pela igualdade! Por exemplo,?2="2"
retorna True, mas se você definir doisVariant
variáveis, atribua a eles esses valores e compare-os, a comparação falha!
Sub Test4()
Debug.Print 2 = "2" ' True
Dim x, y: x = 2: y = "2"
Debug.Print x = y ' False !
End Sub