Можно объяснить, что не так, изучив (ошибочно) сгенерированную сборку x86.

уйста, ознакомьтесь с последними обновлениями в конце поста.

В частности, см.Обновление 4: Вариант сравнения Curse

Я уже видел, как товарищи бились головой об стену, чтобы понять, как работает вариант, но никогда не предполагал, что у меня будет свой плохой момент с ним.

Я успешно использовал следующую конструкцию VBA:

For i = 1 to i

Это прекрасно работает, когдаi являетсяцелое число или любой числовой тип, повторяющийся от 1 допервоначальная стоимость изi, Я делаю это в тех случаях, когдаi этоByVal Параметр - можно сказать, ленивый - избавить себя от объявления новой переменной.

Тогда у меня была ошибка, когда эта конструкция перестала работать, как ожидалось. После некоторой трудной отладки я обнаружил, что это не работаетi не объявлен как явный числовой тип, ноVariant, Вопрос двоякий:

1- Какова точная семантикаFor иFor Each петли? Я имею в виду, какую последовательность действий выполняет компилятор и в каком порядке? Например, оценка предела предшествует инициализации счетчика? Этот лимит копируется и «фиксируется» где-то перед началом цикла? И т. Д. Тот же вопрос относится кFor Each.

2- Как объяснить различные результаты для вариантов и явных числовых типов? Некоторые говорят, что вариант является (неизменным) ссылочным типом. Может ли это определение объяснить наблюдаемое поведение?

Я подготовилMCVE для различных (независимых) сценариев, включающихFor иFor Each операторы в сочетании с целыми числами, вариантами и объектами. Удивительные результаты побуждают к определениюоднозначно семантика или, по меньшей мере, проверка, соответствуют ли эти результаты определенной семантике.

Мы приветствуем любые идеи, в том числе частичные, объясняющие некоторые неожиданные результаты или их противоречия.

Благодарю.

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

ОБНОВЛЕНИЕ 1

Интересное наблюдение, которое может помочь понять некоторые из них. Что касается случаев 7 и 8: если мы удерживаем другую ссылку на итерируемую коллекцию, поведение полностью меняется:

    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

Это означает, что в исходном случае7 повторяющаяся коллекция была собрана сборщиком мусора (из-за подсчета ссылок) сразу после переменнойobj был назначен на первый элемент коллекции. Но это все еще странно, хотя. Компилятор должен был содержать некоторую скрытую ссылку на итерируемый объект !? Сравните это с примером 6, в котором итеративный массив был «заблокирован» ...

ОБНОВЛЕНИЕ 2

СемантикаFor заявление, как определено MSDN, можно найтина этой странице, Вы можете видеть, что прямо указано, чтоend-value должен оцениваться только один раз и до того, как выполнение цикла продолжается. Должны ли мы рассматривать это странное поведение как ошибку компилятора?

ОБНОВЛЕНИЕ 3

Интригующий случай 7 раз.нелогичный поведение case7 не ограничивается (скажем, необычной) итерацией переменной для самого себя. Это может произойти в, казалось бы, «невинном» коде, который по ошибке удаляет единственную ссылку на итерируемую коллекцию, что приводит к ее сборке мусора.

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

Интуиция предполагает, что некоторая скрытая ссылка хранится в коллекции, чтобы остаться в живых во время итерации. Мало того, что это не так, но программа работает без ошибок во время выполнения, что, вероятно, приводит к серьезным ошибкам. В то время как спецификация не устанавливает никаких правил манипулирования объектами при итерации, реализация защищает изамок Итерированные массивы (случай 6), но они пренебрегают - даже не содержат фиктивной ссылки - в коллекции (ни в словаре, я это тоже проверял).

Программист обязан заботиться о подсчете ссылок, который не является «духом» VBA / VB6 и архитектурными мотивами подсчета ссылок.

ОБНОВЛЕНИЕ 4: Проклятие сравнения вариантов

Variants демонстрируют странное поведение во многих ситуациях. Особенно,сравнение двух вариантов разных подтипов дает неопределенные результаты, Рассмотрим эти простые примеры:

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

Как вы можете видеть, когда обе переменные, число и строка, были объявлены вариантами, сравнение не определено. Когда хотя бы один из них явно введен, сравнение успешно выполняется.

То же самое происходит при сравнении на равенство! Например,?2="2" возвращает True, но если вы определите дваVariant переменных, назначьте им эти значения и сравните их, сравнение не удастся!

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

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

End Sub

Ответы на вопрос(1)

Ваш ответ на вопрос