Московский физико-технический институт
Опубликован: 23.12.2005 | Доступ: свободный | Студентов: 2868 / 252 | Оценка: 4.61 / 4.44 | Длительность: 27:18:00
ISBN: 978-5-9556-0051-2
Лекция 2:

Базовые понятия Action Script

Выражения

Флэш МХ имеет набор операторов, слегка расширенный по сравнению с Java. Сначала мы опишем (а точнее, кратко перечислим) ту часть операторов, которая является общей для Флэш МХ, Java и С++. Затем подробнее рассмотрим операторы, которых в С++ нет но есть в Java (для тех читателей, которые не работали на Java). И, наконец, подробно расскажем про операторы, которые вы кроме ActionScriptJavaScript ) нигде не найдете.

Стандартные операторы

Во Флэше имеются практически все операторы из С++ и все операторы из 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) действительно свершилось. А описанный нами алгоритм дал тот же результат, что и прямое выполнение операции "побитовое или". (Для наглядности мы представили результаты как в десятичном, так и в двоичном виде.)