Переменная итерирующая сама по себе - разное поведение с разными типами

34

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

В частности, см. Обновление 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
  • 2
    Описание в документации довольно близко к «формальному» определению Variant . Если вы хотите более формальный, чем это, перейдите по ссылкам на MSDN. Я не совсем ясно, что тип данных, тип параметра и условие выхода из цикла связаны друг с другом в вопросе, хотя.
  • 2
    Очень интересный вопрос ... хотя тесты кажутся "грязными", потому что они все находятся в одной области видимости и многократно используют / переназначают одни и те же переменные, что просто не кажется правильным - было бы неплохо иметь отдельный тестовый метод для тестового случая ... с одним вызовом Assert ... Rubberduck -style :-)
Показать ещё 24 комментария
Теги:
for-loop
vb6
language-lawyer
variant

1 ответ

17
Лучший ответ

Смотрите правки ниже!

Для каждого редактирования также добавлено ниже в разделе Edit2

Дополнительные изменения о ForEach и коллекциях в Edit3

Последнее редактирование о ForEach и коллекциях в Edit4

Окончательная заметка об итерационном поведении в Edit5

Часть тонкости этого нечетного поведения в семантике оценки варианта при использовании в качестве управляющей переменной цикла или условия завершения.

В двух словах, когда вариант является конечным значением или управляющей переменной, конечное значение естественно переоценивается средой выполнения с каждой итерацией. Однако тип значения, например Integer, толкается directly и, следовательно, не переоценивается (и его значение не изменяется). Если управляющая переменная имеет значение Integer, но конечное значение равно Variant, то на первой итерации Variant принудительно применяется к Integer и толкается аналогично. Такая же ситуация возникает, когда условием завершения является выражение, включающее Variant и Integer - оно принуждалось к Integer.

В этом примере:

Dim v as Variant
v=4
for v= 1 to v
  Debug.print v,
next

Варианту v присваивается целочисленное значение 1, и условие завершения цикла переоценивается, поскольку конечная переменная является вариантом - среда выполнения распознает наличие ссылки Variant и принудительно переоценивает каждую итерацию. В результате цикл завершается из-за переназначения цикла. Поскольку вариант теперь имеет значение 1, условие завершения цикла выполняется.

Рассмотрим следующий пример:

Dim v as variant
v=4
for v=1 to v-0
   Debug.Print v,
next 

Когда условием завершения является выражение, такое как "v - 0", выражение оценивается и принуждается к регулярному целому, а не варианту, и, следовательно, его жесткое значение переносится на стек во время выполнения. В результате значение не переоценивается на каждой итерации цикла.

Другой интересный пример:

Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
   v=i-1
   Debug.print i,
next

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

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

Извините, если это немного грязно, но это немного поздно, но я видел это и не мог не ответить на ответ. Надеюсь, это имеет смысл. Ах, добрый ol VBA:)

ИЗМЕНИТЬ:

Нашел некоторую актуальную информацию из спецификации языка VBA в MS:

Выражения [начальное значение], [конечное значение] и [шаг-инкремент] оцениваются один раз, в порядке и до любого из следующих вычислений. Если значение [начальное значение], [конечное значение] и [шаг-инкремент] не являются допустимыми для Double, ошибка 13 (несоответствие типов) возникает немедленно. В противном случае перейдите к следующему алгоритму, используя исходные, несогласованные значения.

Выполнение операции [for-statement] продолжается в соответствии со следующим Алгоритм:

  • Если значение данных [step-increment] равно нулю или положительному числу, и значение [bound-variable-expression] больше, чем значение [end-value], затем выполнение [forstatement] немедленно завершается; в противном случае переходите к этапу 2.

  • Если значение данных [step-increment] является отрицательным числом, а значение значение [bound-variable-expression] меньше значения [end-value], выполнение команды [for-statement] немедленно завершается; в противном случае переходите к этапу 3.

  • Выполняется [оператор-блок]. Если команда [inested-for-statement] присутствует, то он выполняется. Наконец, значение [bound-variable-expression] добавляется к значению [step-increment] и Let-assign обратно в [bound-variable-expression]. Исполнение тогда повторяется на шаге 1.

То, что я собираю из этого, состоит в том, что цель заключается в том, чтобы оценивать значение завершающего условия один раз и один раз. Если мы увидим доказательства того, что изменение этого значения изменяет поведение цикла из его начального условия, это почти наверняка связано с тем, что можно было бы назвать неофициально случайной переоценкой, потому что это вариант. Если это непреднамеренно, мы, вероятно, можем использовать только анонимные доказательства, чтобы предсказать его поведение.

Если, когда среда выполнения оценивает значения начала цикла/конца/шага и выталкивает "значение" этих выражений в стек, значение Variant выдает ключ "byref" в этот процесс. Если во время выполнения не первый распознать вариант, оценить его и нажать это значение в качестве условия завершения, любопытное поведение (как вы показываете) почти наверняка произойдет. Точно так же, как VBA обрабатывает варианты в этом случае, это была бы большая задача для анализа pcode, как это предполагали другие.

EDIT2: FOREACH

Опция VBA снова дает представление об оценке циклов ForEach над коллекциями и массивами:

Выражение [коллекция] оценивается один раз до любого из следующих > вычислений.

  • Если значение данных [collection] является массивом:

    Если массив не имеет элементов, то выполнение команды [for-each-statement] немедленно завершается.

    Если объявленным типом массива является Object, тогда [bound-variable-expression] присвоено заданному первому элементу в массиве > . В противном случае выражение [bound-variable-expression] будет присвоено назначению > первому элементу массива.

    После того, как [bound-variable-expression] установлено, выполняется [оператор-блок] > . Если присутствует [inested-for-statement], он выполняется.

    После того, как [оператор-блок] и, если присутствует, [вложенный-для-оператора] > завершили выполнение, [bound-variable-expression] присвоено назначению > для следующего элемента в массиве (или Set - назначается, если это массив из > Object). Если и только в том случае, если в массиве больше элементов, тогда выполняется выполнение команды [for-each-statement]. В противном случае > [оператор-блок] выполняется снова, за которым следует [nested-forstatement] if > present, и этот шаг повторяется.

    Когда выполнение [for-each-statement] завершено, значение > [bound-variable-expression] - это значение данных последнего элемента массивa > .

  • Если значение данных [collection] не является массивом:

    Значение данных [collection] должно быть ссылкой на объект > внешний объект, поддерживающий интерфейс перечисления, определенный перечислением. [Bound-variable-expression] является либо присвоенным назначением, либо > Set-назначенным первому элементу в [коллекции] в > реализации- > определенным образом.

    После того, как [bound-variable-expression] установлено, выполняется [оператор-блок] > . Если присутствует [inested-for-statement], он выполняется.

    После того, как [оператор-блок] и, если присутствует, [вложенный-для-оператора] > завершили выполнение, [bound-variable-expression] присвоено назначению > для следующего элемента в [коллекции] в определенным образом. Если > в элементе [collection] больше нет элементов, то выполнение инструкции [for-each- > ] немедленно завершается. В противном случае, [оператор-блок] > выполняется снова, а затем [inested-for-statement], если он присутствует, и этот > шаг повторяется.

    Когда выполнение [for-each-statement] завершено, значение > [bound-variable-expression] - это значение данных последнего элемента в > [коллекции].

Используя это как основу, я думаю, становится ясно, что Variant, назначенный переменной, которая затем становится выражением bound-variable, генерирует ошибку "Array is locked" в этом примере:

    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

Использование 'v' в качестве [bound-variable-expression] создает привязку Let-assign к V, которая предотвращается средой выполнения, потому что она является целью перечислимого процесса для поддержки самого цикла ForEach; то есть время выполнения блокирует вариант, тем самым исключая цикл для назначения другому варианту варианта, который обязательно должен произойти.

Это также относится к "Redim Preserve" - изменению размера или изменению массива, тем самым изменяя назначение варианта, будет нарушать блокировку, помещенную в цель перечисления при инициализации цикла.

Что касается назначений/итераций на основе диапазона, обратите внимание на отдельную семантику для неэлементных элементов; "внешние объекты" обеспечивают специфическое перечисление для конкретной реализации. Объект Excel Range имеет свойство _Default, которое вызывается при упоминании только имени объекта, как в этом случае, которое не принимает неявный блокировку при использовании в качестве цели итерации для ForEach (и, следовательно, не генерирует ошибка блокировки, поскольку она имеет разную семантику, чем разновидность 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

(Свойство _Default можно идентифицировать, изучив библиотеку объектов Excel в обозревателе объектов VBA, выделив объект Range, щелкнув правой кнопкой мыши и выбрав "Показать скрытые элементы" ).

EDIT3: Коллекции

Код с коллекциями становится интересным и немного волосатым:)

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

Вот где не более, чем настоящая ошибка должна рассматриваться в игре. Когда я впервые запускал эти два образца в отладчике VBA, они выполнялись точно так же, как OP, предлагаемый в начальном вопросе. Затем, после перезапуска процедуры после нескольких тестов, но затем восстанавливая код до его первоначальной формы (как показано здесь), последнее поведение произвольно начиналось с сопоставления с предшественником на основе объекта над ним! Только после того, как я остановил Excel и перезапустил его, изменилось исходное поведение последнего цикла (ничего не печатающего), return. Невозможно объяснить это, кроме ошибки компилятора.

EDIT4 Воспроизводимое поведение с вариантами

Отметив, что я сделал что-то в отладчике, чтобы принудительно выполнить итерацию на основе варианта через цикл Collection, по крайней мере, один раз (как и в случае с версией Object), я, наконец, нашел воспроизводимый код способ изменения поведение

Рассмотрим этот оригинальный код:

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

Это по существу исходный случай OP, и цикл ForEach завершается без единой итерации. Теперь раскомментируйте строку "Установить vv = v" и запустите: теперь для каждого будет выполняться однократное повторение. Я думаю, нет никаких сомнений в том, что мы нашли очень (очень!) Тонкую ошибку в механизме оценки Variant в среде исполнения VB; произвольная установка другого "Варианта", равная переменной цикла, заставляет оценку, которая не имеет место в оценке "Для каждой оценки", и я подозреваю, что она связана с тем фактом, что коллекция представлена ​​в Варианте как вариант/объект/сборник, Добавление этого фиктивного набора, похоже, заставляет проблему и заставить цикл работать, как это делает объектная версия.

EDIT5: Последняя мысль об итерациях и коллекциях

Это, вероятно, будет моим последним изменением этого ответа, но я должен был заставить себя убедиться, что я узнал во время наблюдения поведения нечетного цикла, когда переменные использовались как выражение "bound-variable-expression" и предельное выражение заключалось в том, что, особенно когда дело доходит до "Вариантов", иногда поведение индуцируется в силу итерации, изменяющей содержимое "bound-variable-expresssion". То есть, если у вас есть:

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

важно помнить (по крайней мере, это было для меня) иметь в виду, что внутри "Каждого" выражение "bound-variable-expression", содержащееся в "v", изменяется в силу итерации. То есть, когда мы начинаем цикл, v содержит коллекцию, и начинается перечисление. Но когда это перечисление начинается, содержимое v теперь является произведением перечисления - в данном случае - объекта Range (из ячейки). Такое поведение можно увидеть в отладчике, так как вы можете наблюдать "v" от коллекции до диапазона; это означает, что следующий удар в итерации возвращает все контексты перечисления объекта Range, а не "Collection".

Это было отличное исследование, и я ценю отзывы. Это помогло мне понять вещи даже лучше, чем я думал. Если у вас больше комментариев или вопросов, я подозреваю, что это будет мое последнее изменение ответа.

  • 0
    Спасибо, я ценю ваши усилия :). Однако это предварительное объяснение, похоже, не работает в случае 2, не так ли? Пожалуйста, проверьте это еще раз.
  • 0
    Нет - я пытался решить эту ситуацию. Случай 2 представляет собой экземпляр управляющей переменной является явное целое число , которое, в свою очередь, заставляет переменную предел быть принуждены к целому числу, тем самым вызывая петлю вести себя «правильно» (освобожденный управлением переменной изменения внутри цикла).
Показать ещё 27 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню