Пожалуйста, ознакомьтесь с последними обновлениями в конце сообщения.
В частности, см. Обновление 4: Проклятие сравнения вариантов
Я уже видел товарищей, ударяющих головой о стену, чтобы понять, как работает вариант, но никогда не думал, что у меня будет свой собственный плохой момент.
Я успешно использовал следующую конструкцию VBA:
For i = 1 to i
Это отлично работает, когда i
является целым или любым числовым типом, итерируя от 1 до исходного значения i
. Я делаю это в тех случаях, когда i
является параметром ByVal
- вы можете сказать, что ленивый - освободить себя от объявления новой переменной.
Тогда у меня была ошибка, когда эта конструкция "остановилась", как и ожидалось. После некоторой жесткой отладки я обнаружил, что она не работает одинаково, когда i
не объявляется как явный числовой тип, а <<26 > . Вопрос двоякий:
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 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 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: Проклятие сравнения вариантов
Variant
проявляют странное поведение во многих ситуациях. В частности, сравнение двух вариантов разных подтипов дает undefined результаты. Рассмотрим эти простые примеры:
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
Как вы можете видеть, когда обе переменные, число и строка были объявлены вариантами, сравнение undefined. Когда хотя бы один из них явно введен, сравнение выполняется успешно.
То же самое происходит при сравнении для равенства! Например, ?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