Циклы

Циклы — это важнейшие структуры любого языка программирования. В большинстве программ они являются основным механизмом, обеспечивающим повторение вычислений. Остальные предназначенные для этой цели механизмы ActionScript — рекурсия, циклы временной шкалы, событие onEnterFrame и функция sctlntervalO — ввиду специфичности используются куда реже. Мы уже имеем достаточный опыт работы с циклами. В этом разделе систематизируем имеющиеся знания, дополнив их новыми сведениями и фактами.

Циклы с предусловием (while и do—while)
Простейшим среди циклов ActionScript является цикл while. Его синтаксис:

while (expression) statements,

где:
• expression — выражение, являющееся чаше всего некоторым условием. До тех пор, пока оно равно true, код в теле цикла выполняется. Как только оно принимает значение false, дальнейшая прокрутка цикла прекращается;

• statements — предложения, которые должны быть проделаны, если условие в expression истинно.

Как видите, по особенностям своего синтаксиса предложение while практически идентично условному предложению if. Их различие состоит в том, что код в теле if проделывается (в случае истинности условия в заголовке) только один раз, а код в блоке цикла while может выполняться несколько (или даже тысячи и миллионы) раз.


Пример:

var per:Boolean=true;
while (per){
trace (per);
}

Данный код является абсолютно верным с точки зрения синтаксиса цикла while. Однако в нем имеется серьезная логическая ошибка, не позволяющая считать его Правильным. Дело в том, что в теле приведенного цикла не происходит модификации переменной per, использующейся в скобках условия. А это означает, что ее значение всегда будет равно true, следовательно, цикл будет прокручиваться бесконечно. Бесконечные же циклы недопустимы как в ActionScript, так и в любом другом языке сценариев (какой смысл от такого цикла, если его результата все равно невозможно дождаться).

Любой корректный цикл должен каким-то образом модифицировать переменные или свойства, использующиеся в выражении условия в его заголовке. Например:

var i:Number =0; // Переменная-счетчик
while (i<5) { // Заголовок с условием прокрутки цикла
trace!!); // Действие
i++; // Модификация
) // В Output появляется: 0 1 2 3 4

Выражение в заголовке цикла while совсем необязательно должно возвращать true или false. Подобно условному предложению if, в алгоритме while проводится приведение типа expression к типу boolean, Эту особенность нужно использовать, уменьшая при возможности количество требуемых для выполнения сценария операций:

var i:Number = 10;
while (i) { // Аналогично: while(i>0) — 0 преобразуется в false,
// остальные числа — в true
trace (i);
i--;
} // Выводит: 10 9 8 7....1

Разновидностью цикла while является цикл do—while.


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

Цикл do—while имеет следующий, достаточно схожий с while, синтаксис:

do statements while (expression)

Как работает цикл do—while? Вначале выполняется код в теле (statements). Затем проверяется выражение условия (expression). Если его значение может быть преобразовано в true, то код в теле проделываете я снова — и так до тех пор, пока условие не станет ложным. Например:

var i:Number=9;
do {
i++;
} while (i<10);
trace(i); // Выводит: 10

Обычно цикл do—while применяется, если повторные действия должны быть осуществлены по результатам предварительного. Подобного рода необходимость встречается на практике весьма редко, поэтому предложение do—while является мало используемым. Каждая отдельная прокрутка цикла называется итерацией (от английского iteration — "повторение»). Число итераций, выполняемых циклом, может быть весьма значительным — в зависимости от сложности выражений, прописанных в его теле, это могут быть сотни тысяч и даже (на хороших машинах) миллионы итераций.


В AclionScript не имеется ограничений на количество витков, выполняемых циклом. Однако жестко лимитируется общее время выполнения циклов (равно как и функций), что является огромной преградой для создания на ActionScript многих сложных алгоритмов. Время, выделяемое на прокрутку одного цикла, приблизительно равняется 15 секундам.

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

van i:Number=0;
while (getTimer()<15000) { // Функция getTimer() возвращает время
//в миллисекундах с начала проигрывания фильма
i++; // Подсчитываем итерации
}
trace(i); // Выводит: 1965236 (!)

Цикл в приведенном примере успел проделать за 15 секунд почти два миллиона итераций! Однако даже такой высокой производительности порой не хватает. Подробнее о проблемах, вызываемых ограничением на время выполнения кода, мы поговорим ниже.

Цикл с параметром (for)
В любом корректном цикле с предусловием обязательно должны быть два элемента: переменная (или свойство), входящая в выражение условия, и операция ее модификации. В противном случае цикл будет бесконечным. Возникает вопрос: если в цикле всегда должны присутствовать описанные элементы, то почему бы их, для большей компактности и читабельности кода, просто не вынести в заголовок? «Действительно, почемубыинет», — ответили на этот вопрос разработчики языка ALGOL более 30 лет назад и создали цикл for, который теперь можно найти в большинстве языков программирования.

Цикл for имеет следующий синтаксис:

for(initValue; condition; next) statements,
где:

• initvalue — модифицируемая переменная, прямо или косвенно входящая в условие. Чаще всего это целочисленный счетчик (итератор), предназначенный, например,' для просмотра массива или создания группы клипов с однотипными именами, Обычно переменная initValue объявляется прямо в заголовке цикла for при помощи ключевого слова var, хотя вполне может быть использована и существующая переменная;

• condition —некоторое условие, в которое входит initValue. В принципе, это может быть любое выражение, а не только возвращающее булевы значения (так как for проводит автоматическое преобразование типа его значения к типу boolean). Подобно while, цикл for прокручивается до тех пор, пока выполняется данное условие;

• next — выражение, модифицирующее initValue. В девяти случаях из десяти это инкрементирование (++) или декрементирование (--).

Пример:
for (var i = 0; i<10; i++) {
trace(i); // Выводит: 0 1 2 3...10
}

Наиболее распространенная ошибка у новичков при работе с циклом for связана с тем, что вместо разделителя «;» в заголовке используется «,». Чтобы не допускать эту ошибку, нужно помнить, что определение счетчика, условие и модификация — это отдельные предложения, и они сами могут содержать запятую.

Создавая цикл, нужно следить, чтобы его переменная не совпадала по имени ни с одной из существующих переменных. Это связано с тем, что счетчик цикла for не обладает локальной видимостью, ограниченной только телом цикла. Переменная цикла — это точно такая же переменная, как и любая другая (вспомните синтаксис цикла while, аналогом которого является for).

Важный вопрос синтаксиса цикла for связан с тем. нужно ли использовать var в выражении инициализации счетчика. В общем, никакой заметной разницы от того, станете ли выэтоделатьилиженет, не будет. Однако ввиду того, что при инициализации итератора происходит создание вполне полноценной переменной, применение var является хорошим тоном, так как при этом соблюдается принцип единообразия. Кроме того, если цикл присутствует в коде функции, то при использовании var счетчик будет локальной переменной, что может помочь избежать конфликта между ним и переменными основного кода:

// Функция func запускается только один раз, так как после ее выполнения
// итератор цикла становится равным сразу 10
for (i=0; i<10; i++) {
func();
}
function func():Void {
func (i=0; i<10; i++) {
trace(i);
}
}

Модифицируемая переменная цикла for обычно хранит численные (а чаще всего целочисленные) значения. Однако это совсем не правило. Как и не правило то, что модификация должна быть обязательно инкрементированием или декрементированием. В качестве примера, иллюстрирующего гибкость цикла for, приведем код, составляющий строку из символов кодировки ASCII с индексами от 40 до 60:

for (var i = "", n = 0; i.length<20; i += String.fromCharCode(40+n++)) {
trace(i);
}

Изданного примера следует еще один чрезвычайно важный для практики вывод: используя оператор «,», можно задавать циклы сразу с несколькими модифицируемыми переменными. Особенностью переменных цикла for является то, что, если они объявляются в его заголовке, их нельзя строго типизировать. Вернее, объявить их тип можно — но транслятор данное объявление попросту проигнорирует. Это отличает итераторы от обычных переменных:

// Переменная объявляется как булева, но ей присваивается число.
// Ошибки это не вызывает
for (var i:Boolean = 1; i<10; i++) {
trace (i);
}
var per:Boolean=1; // Строка вызывает ошибку несоответствия типов

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

Если переменную цикла все же необходимо строго типизировать, ее можно просто объявить отдельно (или использовать цикл while). Например:

var i:Number = 1;
for (; i<10; i++) {
i = true; // Попытка присвоить итератору булеву величину
// вызывает ошибку
}

Один цикл может быть вложен в другой, причем при каждой итерации внешнего цикла внутренний цикл прокручивается полностью. Вложенные циклы используются, как правило, при создании многомерных массивов. В ActionScript имеются только массивы-векторы, однако многомерные массивы можно легко эмулировать, сохраняя в качестве элемента массива другие массивы:

// Кол создает массив 3x3, заполненный числами от 1 до 9
var arr:Array = []; // Создаем пустой массив
for (var i = 0; i<3; i++) { // Цикл, создающий строки матрицы
arr[i] = []; // Массив, хранящий элементы строки
for (var j = 0; j<3; j++) { // Цикл, заполняющий строки элементами
arr[i][j] = i*3+j+l;
}
}

Если циклы for и while являются идентичными, то какой из них предпочтительнее использовать? В общем, это дело только ваших вкусов и привычки. Хотя можно попробовать определить более быстрый цикл (что, учитывая жесткие ограничения на время выполнения кода, совсем немаловажно);

// Испытание эффективности цикла while
var i:Number = 0;
while (i<500000) {
var per:Number = Math.sin{Math.PI)*Math.cos(Math.PI);
i++;
}
trace(getTimer()); // Выводит: 3365
// Испытание эффективности цикла
for (var i = 0; i<500000; i++) {
var per:Number = Math.sin(Math.PI)*Math.cos(Math.PI);
}
trace(get Timer()); // Выводит: 3381

Как показывает тестирование, скорость работы циклов while и for практически идентична. Таким образом, нет никакой разницы, какой цикл использовать, и ваш выбор должен опираться только на специфику решаемой задачи.

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

constants 'i', 'per' // Прописываются используемые константы — имена переменных
push 'i', 0.00 // В стек добавляется строка с именем переменной
// и положенное для нее значение
varequals // Объявляется переменная
push 'i' // В стек добавляется имя переменной — "i"
getVariable // Читается значение переменной push 5000OO
// В стек добавляется число 500000
lessThan // Проверяется, меньше ли 500000, чем i
not // Так как требуется определить не то, меньше ли 500000, чем i,
// а то, меньше ли i, чем 500000, то над результатом, возвращенным операцией
// lessThan, проводится операция логического отрицания
branchIfTrue @17 // До строки 17 расположен код тела цикла, который
// выполняется, если в вершине стека хранится истина
push 'per1, -1.22460635382238Е-16 // В стек добавляется имя переменной
// per и положенное для нее значение.
// Реально ) cos( ) sin( π π × равняется 0. Присваиваемая же
// величина обусловлена ошибкой округления,
varequals // Объявляется переменная per
push 'i', 'i' // В стек помещаются две строки с именем переменной i
getVariable // Читается значение i
increment // Проводится операция инкрементирования
setVariable // Переменной i присваивается новое значение
branch @4 // Выполнение кода переводится на 4 строку — цикл
// переходит на новую итерацию
end // Конец программы

Из-за того, что циклы while и for компилируются в одну и ту же последовательность байт-кодов, невозможно на базе swf-файла однозначно восстановить исходный ActionScript-код. Поэтому swf-декомпиляторы обычно используют только один вид цикла. Аналогичная ситуация наблюдается и с некоторыми другими структурами языка.

Цикл for—in
Цикл for—in является, пожалуй, самым необычным и специфичным циклом ActionScript. Используя его, можно просмотреть все свойства объекта.

Цикл for—in имеет следующий синтаксис:

for (variable in expression) statements,
где:

• variable — переменная, которой в процессе работы цикла будут присваиваться имена перечисляемых свойств. Правила и ограничения, связанные с ее заданием, полностью идентичны описанным нами ранее для счетчика цикла for;

• expression — выражение, результат вычисления которого может быть преобразован в идентификатор объекта, свойства которого должны быть перечислены. Чаще всего это просто имя объекта или строка, его храпящая;

• statements — действия, которые должны быть проделаны при получении имени очередного
свойства объекта.

Пример:
// Создаем объект с тремя свойствами
var obj:Object = {property1:1, property2:2, property3:3};
for (var i in obj) { // Перечисляем свойства объекта obj
trace(); // Имена свойств выводим в utput
} // Выводит: property1 property2 property3

Как работает цикл for—in? Чтобы понять это, вспомним, что такое объект. Объект — это неупорядоченное множество свойств, которые могут хранить элементарные величины, объекты, клипы или функции. Что такое свойство? Практически то же самое, что и переменная — именованный контейнер с данными. Отличие между переменными и свойствами состоит в том, что свойства имеют набор атрибутов, определяющих их отношение к удалению, переопределению и перечислению (см. главу 2) (это полностью справедливо для ECMAScript и лишь частично — для ActionScript, так как в нем переменные всегда являются свойствами). Во внутреннем представлении объекта имена свойств и их значения разделены. Имена сохраняются в виде строк в специатьном списке в том порядке, в котором соответствующие свойства были созданы. При перечислении циклом for—in последовательно выводятся все элементы этого списка:

var obj:Object = {property1:1, property2:2, property3:3};
for (var i in obj) {
trace(typeof i); // Проверяем тип величин, присваиваемых i
} // Выводит: string string string

Так как присваиваемое переменной цикла for—in значение не является абсолютной ссылкой на компонент объекта, а представляет собой просто строку с именем свойства, то прочитать напрямую его значение невозможно. Чтобы сделать это, следует использовать имя объекта с оператором доступа к свойствам по строковым ключам «[]» или функции eval или get, в скобках параметров которых должна быть прописана соответствующая операция конкатенации:

var obj:Object= {property1:1, property2:2, property3;3};
for (var i in obj) {
trace(eval(i)); // Выводит: undefined undefined undefined (свойства
// находятся вне текущей цепочки oбластей видимости)
trace(obj[i]]; // Выводит: 1 2 3 (оптимальный вариант доступа
// к значениям свойств)
trace(eval("obj."+i)); // Выводит: 1 2 3
trace(get{"obj."+i)); // Выводит: 1 2 3
}

Если в процессе работы цикла for—in в исследуемом объекте создаются новые свойства, они перечислены не будут. Это связано с тем, что их имена добавляются в начало списка, а смешение для выведения следующего элемента циклом for—in определяется не абсолютно, а относительно:

var obj:Object = {prl:l, pr2:2, pr3:3};
var n:Number= 4;
for (var i in obj) {
trace();
obj["pr"+n]=n; // Добавляем новые свойства pr4=4, рr5=5, рr6=6
n++;
} // Выводит: prl, рr2, рг3 (новые свойства не перечисляются)
// Проверяем, были ли добавлены новые свойства в принципе
for (var i in obj) {
trace(i);
} // Выводит: рr6, pr5, pr4, pr1, pr2, рr3 (обратите внимание
// на последовательность, в которой были выведены свойства)

Если во время работы цикла for—in удалить при помощи оператора delete свойство, которое должно быть еще перечислено, то его значение будет заменено на undefined. Однако имя свойства из соответствующего списка удалено не будет, поэтому его присвоение переменной цикла все же произойдет:

var obj:Object = {prl:1, рr2:2, рr3:3};
for (var i in obj) {
trace(i+"="+obj[i]); // Выводим имя свойства вместе с его значением
delete obj.рr3; // Удаляем свойство рr3
} // Выводит: pr1=1 pr2=2 pr3=undefined

Если вы попытаетесь трассировать свойства любого встроенного объекта ActionScript, то в Output ничего не отобразится. Это связано с тем, что свойства встроенных объектов защищены от перечисления циклом for—in. Можно ли как-то эту защиту снять? Официально — пока нет. А вот используя недокументированные возможности — с легкостью.

У обычных свойств имеются 3 атрибута: Readonly (определяет, возможно ли переопределение свойства), DontDelete (задает, можно ли удалить свойство), DontEnum (определяет, доступно ли свойство для перечисления циклом for—in). Все свойства встроенных объектов защищены от удаления (DontDelete=l) и перечисления (DontEnum=l), большинство — и от переопределения (ReadOnly=l). Чтобы изменить значения атрибутов свойств, нужно воспользоваться недокументированной функцией ASSetPropFlags(). Так как этой функции мы ранее посвятили достаточно внимания, то сейчас описывать особенности ее синтаксиса не имеет смысла. Сразу приведем пример:

for (var i in Boolean.prototype) { // Пробуем просмотреть свойства
// при настройках по умолчанию
trace (i);
} // В окне Output ничего не отображается
// Снимаем защиту от перечисления со всех свойств прототипа конструктора
// класса Boolean
ASSetPropFlags(Boolean.prototype,null, null, 1);
for (var i in Boolean.prototype) {
trace (i);
} // Выводит: toString valueOf __proto__ (свойство
// указывает на следующий прототип в цепочке наследования)
// constructor (свойство указывает на конструктор класса)

Особенностью цикла for—in является то, что он перечисляет не только свойства, непосредственно принадлежащие объекту', но и наследуемые им. Для этого, просмотрев сам объект, он переходит к прототипу конструктора его класса, далее — к прототипу прототипа и так до тех пор, пока не будет пройдена вся цепочка наследования. Если один из прототипов содержит свойство с таким же именем, как и у одного из уже перечисленных ранее свойств, то повторно оно перечислено не будет (не имеет смысла, так как при вызове свойства по этому имени в любом случае произойдет обращение к компоненту, расположенному в цепочке наследования раньше). Например, и прототип класса Boolean, и прототип его надкласса Object хранят метод toString, однако в Output эта строка появляется лишь однажды:

var bul:Boolean=new Boolean(1); // Создаем объект класса Boolean
bul["isTrue"]=true; // Создаем для объекта bul свойство isTrue
ASSetPropFlags(Object.prototype,null,null,1); // Разрешаем перечислять
// свойства прототипа класса Object
ASSetPropFlags(Boolean.prototype,null,null,1); // Разрешаем перечислять
// свойства прототипа класса Boolean
for (var i in bul) {
trace(i);
} /* Выводит: toLocaleString isPropertyEnumerable isPrototypeOf hasOwnProperty
addProperty unwatch watch (свойства прототипа класса Object) toString valueOf
__proto__ constructor (свойства прототипа класса Boolean) isTrue (собственное свойство объекта)*/

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

На практике цикл for—in может быть полезен, если необходимо произвести модификацию всех свойств объекта. Например, чтобы сделать все клипы, находящиеся на основной временной диаграмме, невидимыми, достаточно набрать:

for (var i in _root) {
_root [i]._visible = false; }

Иногда цикл for—in используется, если нужно просмотреть или модифицировать все элементы массива. Впрочем, нужно проверить, является ли такой ход оправданным — вполне вероятно, что цикл for—in работает гораздо медленнее, чем for:

var arr:Array = []; // Создаем массив из 100000 элементов
for (var i = 0; i<100000; i++) {
arr[i] = i;
}
var time:Number = getTimer(); // Записываем момент времени, с которого
// началась работа цикла for
for (var i = 0; i<100000; i++) { // Переопределяем все элементы массива arr
arr[i] = Math.sqrt(i);
}
trace(getTimer()-time); // Выводит: 2190
time = getTimer(); // Засекаем время начала работы цикла for-in
for (var i in arr) {
arr[i] = Math.sqrt(i);
}
trace(getTimer()-time); // Выводит: 3926

Проведенное испытание показало, что цикл for—in значительно медленнее решает задачу перебора элементов массива по сравнению с циклом for. Следовательно, предпочтение стоит отдавать именно for.

Предложения break и continue
С предложением break мы уже встречались, когда разбирали условное предложение switch. Тогда оно было необходимо, чтобы прервать выполнение кода в блоке — иначе проделывались даже те действия, которые не относились к выполнившемуся условию, Однако полезные функции предложения break не ограничиваются только работой с предложением switch. Используя его, можно прервать выполнение любого из описанных выше циклов. Например:

var i:Number = 0;
while (true) { // Бесконечный цикл
if (i>=100) {
break; // Если переменная i превыснт по значению 99,
// цикл прервется
}
i++;
}
trace(i); // Выводит: 100

Предложение break, в принципе, весьма схоже с предложением return. Одно прерывает работу функций, другое — циклов. Предложение break нельзя заменить return, так как при этом остановится выполнение не только цикла, но и всего кода на временной шкале.

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

Предложение continue, также как и break, прерывает выполнение кода в блоке цикла. Однако между ними имеется серьезное различие. Предложение break полностью останавливает работу цикла, continue же просто переходит к выполнению следующей итерации. Например:

var i:Number = 0;
while (++i) { // Бесконечный цикл (модификация i проводится прямо
// в заголовке)
if (i<100) {
continue; // Если i меньше 100 — выполняем выражение в заголовке
// и переходим к новой итерации
}
// Нижележащий код не будет достигнут до тех пор, пока условие для i будет
// истинным
trace("i достигла 100!"); // Если i достигает 100, прерываем цикл
// и выводим сообщение
break;
} // Выводит: i достигла 100!

Предложение continue необходимо использовать в тех случаях, когда основной код в теле цикла должен быть выполнен лишь при соблюдении некоторого условия (в приведенном выше примере — достижения переменной значения 100). Если это условие не соблюдается, то continue осуществит быстрый переход к новой итерации. Применение continue зачастую является более техничным ходом, чем обращение к предложению if—else.

Оцените статью: (0 голосов)
0 5 0

Статьи из раздела Action Script на эту тему:
Блок предложений
Обработка исключительных ситуаций. Предложения try-catch-finally, throw и класс Error
Предложение return
Предложение with
Предложения var и function

Вернуться в раздел: Action Script / 6. Предложения