Казахстан, Алматы, Гимназия им. Ахмета Байтурсынова №139, 2008 |
Базовые понятия Action Script
Выражения
Флэш МХ имеет набор операторов, слегка расширенный по сравнению с Java. Сначала мы опишем (а точнее, кратко перечислим) ту часть операторов, которая является общей для Флэш МХ, Java и С++. Затем подробнее рассмотрим операторы, которых в С++ нет но есть в Java (для тех читателей, которые не работали на Java). И, наконец, подробно расскажем про операторы, которые вы кроме ActionScript (и JavaScript ) нигде не найдете.
Стандартные операторы
Во Флэше имеются практически все операторы из С++ и все операторы из Java. (Из С++ не вошли только операторы для работы с указателями - за неимением в языке самих указателей). По поводу работы некоторых из этих операторов нужно сделать ряд комментариев.
В первую очередь рассмотрим ситуацию с логическими операторами && и ||, а также с операторами & и |. В языке Java последние два оператора применяются не только как побитовые операторы, но и (при работе с булевскими операндами) как операторы с обязательным вычислением обоих операндов. Напомним, что для стандартных логических операторов && и || характерно следующее поведение. Если первый операнд позволяет определить значение выражения без использования второго (то есть, если это false в случае && и true в случае || ), то значение второго операнда не вычисляется вовсе. Как правило, такое поведение весьма удобно (и позволяет, например, в первом операнде сделать проверку на null, а во втором - обратиться к методу только что проверенного объекта). Если же в процессе проверки вы производите какие-то дополнительные действия и хотите, чтобы они производились каждый раз, независимо от значения первого операнда, вы можете воспользоваться операторами & или |. Так вот, несмотря на то, что подобное использование не является официально рекомендованным во Флэш МХ, вы и во Флэше можете применять эти операторы с такой целью. Главное - привести выражения к типу Boolean перед вычислением. Вычисления, конечно, все равно происходят побитовые, но после преобразования к Boolean возможные значения операндов - это только 0 и 1, так что разницы никакой нет. Тот же самый прием - приведения к boolean или bool соответственно - с успехом может применяться в Java и в C++. Только во Флэше (и в С++) перед вычислением значения выражения происходит неявное преобразование из булевского типа обратно в целый. И результат тоже получается целочисленный. Итак, посмотрите на примеры применения всех упомянутых операторов:
a = function(){ trace("function a called"); return true; }; b = function(){ trace("function b called"); return false; }; trace("a() && b() = " + (a() && b())); trace("----------"); trace("b() && a() = " + (b() && a())); trace("----------"); trace("a() || b() = " + (a() || b())); trace("----------"); trace("b() || a() = " + (b() || a())); trace("\n========================\n"); trace("a() & b() = " + (a() & b())); trace("----------"); trace("b() & a() = " + (b() & a())); trace("----------"); trace("a() | b() = " + (a() | b())); trace("----------"); trace("b() | a() = " + (b() | a()));
Результат выполнения сего кода таков:
function a called function b called a() && b() = false ---------- function b called b() && a() = false ---------- function a called a() || b() = true ---------- function b called function a called b() || a() = true ======================== function a called function b called a() & b() = 0 ---------- function b called function a called b() & a() = 0 ---------- function a called function b called a() | b() = 1 ---------- function b called function a called b() | a() = 1
Видим, что замена логических операторов побитовыми действительно помогает добиться вычисления обеих частей каждого выражения.
Теперь посмотрим, что произойдет, если наши функции будут вести себя не так хорошо и станут возвращать целочисленные значения вместо булевских. Заменив в предыдущем примере функции а и b на следующие две
a = function(){ trace("function a called"); return 1; }; b = function(){ trace("function b called"); return -2; };
мы получим:
function a called function b called a() && b() = -2 ---------- function b called function a called b() && a() = 1 ---------- function a called a() || b() = 1 ---------- function b called b() || a() = -2 ======================== function a called function b called a() & b() = 0 ---------- function b called function a called b() & a() = 0 ---------- function a called function b called a() | b() = -1 ---------- function b called function a called b() | a() = -1
Обратите внимание, что в серии тестов с операторами & мы получили неправильные ответы - это из-за того, что мы пренебрегли приведением к Boolean. А вот операнды операторов | очень редко нуждаются в таком приведении: если один или оба - ненулевые, то и результат будет ненулевым. Неожиданности будут подстерегать нас лишь когда мы столкнемся с объектом, который не приводится к числу "естественным" образом и не является при этом строкой. То есть с объектом, имеющим тип Object, Function или другой подобный. При неявном приведении к типу Number для осуществления побитовых операций, из такого объекта получится Number.NaN. А это значение как для булевских, так и для побитовых операций равносильно 0. При непосредственном же приведении к булевскому типу - в случае использования оператора ||, а не | - объекты типа Object, Function, Array и т.д. преобразуются в true.
Еще одно интересное наблюдение мы можем сделать, если посмотрим на результаты работы логических операторов. В отличие от прошлого примера, когда мы получали true или false (потому что булевские значения возвращали функции a и b ), в этот раз мы имеем целые числа. То есть "на выходе" логического оператора никакого преобразования к Boolean не происходит. Таким образом, можно представить себе работу оператора && в выражении a() && b() как (temp = a()) ? b() : temp. В самом деле, в случае, когда a() дает истину, результат будет истинным, лишь если истинно b() - его и возвращаем. Если же а() есть ложь, то результат заведомо ложен, так что в качестве него можно а() и вернуть. Аналогичным образом работу оператора || в выражении a() || b() можно эмулировать вот так: (temp = a()) ? temp : b() . То есть если а() - истина, то и результат всего выражения - истина, так что смело возвращаем а(). А если же а() - ложь, тогда возвращаем b(), поскольку в этом случае истину мы получим только если истинно b(). Еще раз отметим, что хотя сейчас мы рассуждения проводили в булевских терминах, поведение реальных операторов и эмуляции совпадает во всех случаях. Что мы сейчас продемонстрируем на следующем примере.
a = function(){ trace("function a called"); return "Some string"; }; b = function(){ trace("function b called"); return -2; }; trace("((temp = a()) ? b() : temp) = " + ((temp = a()) ? b() : temp)); trace("----------"); trace("((temp = b()) ? a() : temp) = " + ((temp = b()) ? a() : temp)); trace("----------"); trace("((temp = a()) ? temp : b()) = " + ((temp = a()) ? temp : b())); trace("----------"); trace("( (temp = b()) ? temp : a() ) = " + ((temp = b()) ? temp : a())); trace("\n========================\n"); trace("a() && b() = " + (a() && b())); trace("----------"); trace("b() && a() = " + (b() && a())); trace("----------"); trace("a() || b() = " + (a() || b())); trace("----------"); trace("b() || a() = " + (b() || a()));
И получаем:
function a called ( (temp = a()) ? b() : temp ) = Some string ---------- function b called function a called ( (temp = b()) ? a() : temp ) = Some string ---------- function a called function b called ( (temp = a()) ? temp : b() ) = -2 ---------- function b called ( (temp = b()) ? temp : a() ) = -2 ======================== function a called a() && b() = Some string ---------- function b called function a called b() && a() = Some string ---------- function a called function b called a() || b() = -2 ---------- function b called b() || a() = -2
То есть наше представление операторов && и || с помощью оператора ?: оказалось правильным. А это значит, что в некоторых случаях удобнее применять операторы && и || вместо ?: - сокращается запись и не нужно сохранять результат вычисления выражения, используемого дважды, во временную переменную. Например, мы запрашиваем нужный нам набор параметров функцией getParamArray(), но если этот массив не задан ( undefined или null ), используем набор параметров по умолчанию. Стандартный код выглядит так:
getParamArray() ? getParamArray() : getDefaultParamArray()
что длинно и заставляет вызывать getParamArray() дважды (или надо будет использовать временную переменную). Альтернативный вариант такой: getParamArray() || getDefaultParamArray(). Согласитесь, что писать так гораздо удобнее. Только, если вы действительно соберетесь использовать этот прием, не забудьте снабдить такой код комментариями. Все-таки подобная запись совершенно не является общепринятой.
Скажем еще несколько слов по поводу побитовых операторов. Они фактически работают с двоичным представлением числа, а мы уже знаем, что работа с недесятичными системами счисления проводится в 32 битах. Если вы захотите проделать побитовую операцию с числом, которое больше или равно 232, то будут взяты только 32 младших бита этого числа. Последовательность действий Флэш МХ в этом случае можно описать примерно так. Сначала операнд представляется в форме действительного числа с фиксированной точкой. Затем отбрасывается дробная часть. Затем вычисляется остаток от деления этого числа на 232. С этим остатком и производятся все побитовые операции. Вот пример, иллюстрирующий все это:
trace((1e12 + 0.6) + " = 1e12 + 0.6"); trace((1e12 + 0.6).toString(2) + " = (1e12 + 0.6).toString(2)"); trace(1e12.toString(2) + " = 1e12.toString(2)" + "\n"); trace(1e12.toString(2) + " в десятичном виде = " + parseInt(1e12.toString(2), 2)); trace("\n" + ((1e12 + 0.6) | 7) + " = (1e12 + 0.6) | 7"); trace(((1e12 % Math.pow(2,32)) | 7) + " = (1e12 % Math.pow(2,32)) | 7"); trace("\nВ двоичном виде: "); trace(((1e12 + 0.6) | 7).toString(2) + " = ((1e12 + 0.6) | 7).toString(2)"); trace(((1e12 % Math.pow(2,32)) | 7).toString(2) + " = ((1e12 % Math.pow(2,32)) | 7).toString(2)");
что дает в результате
1000000000000.6 = 1e12 + 0.6 -101011010110101111000000000000 = (1e12 + 0.6).toString(2) -101011010110101111000000000000 = 1e12.toString(2) -101011010110101111000000000000 в десятичном виде = -727379968 -727379961 = (1e12 + 0.6) | 7 -727379961 = (1e12 % Math.pow(2,32)) | 7 В двоичном виде: -101011010110101110111111111001 = ((1e12 + 0.6) | 7).toString(2) -101011010110101110111111111001 = ((1e12 % Math.pow(2,32)) | 7).toString(2)
Мы нарочно напечатали результаты вычислений слева, чтобы числа оказались одно под другим и равенство полученных разным способом чисел было очевидно. Легко заметить, что результаты побитовой операции существенно меньше, чем 1012, так что отбрасывание старших битов (равносильное делению по модулю на 232) действительно свершилось. А описанный нами алгоритм дал тот же результат, что и прямое выполнение операции "побитовое или". (Для наглядности мы представили результаты как в десятичном, так и в двоичном виде.)