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&nbsp;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&nbsp;pode ser encontrada a instrução definida pelo MSDNnesta página. Você pode ver que está explicitamente declarado que oend-value&nbsp;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&nbsp;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&nbsp;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

Variants 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"&nbsp;retorna True, mas se você definir doisVariant&nbsp;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