М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 16
Текст из файла (страница 16)
Ada |
when '0'.. '9' => statement_2;
when '+' | '-' |' *' | '/' =>statement_3;
when others => statement_4;
end case;
В Ada альтернативы представляются зарезервированным ключевым словом when, а альтернатива по умолчанию называется others. Case-альтернативаможет содержать диапазон значений value_1 .. value_2 или набор значений, разделенных знаком «|».
Оператор break в языке С
В языке С нужно явно завершать каждую case-альтернативу оператором break, иначе после него вычисление «провалится» на следующую case-альтернативу. Можно воспользоваться такими «провалами» и построить конструкцию, напоминающую многоальтернативную конструкцию языка Ada:
char с;
switch (с) {
case 'A': case'B': ... case'Z':
statement_1 ;
C |
case'O': ... case '9':
statement_2;
break;
case '+'; case '-': case '*': case '/':
statement_3 :
break;
default:
statement_4;
break;
Поскольку каждое значение должно быть явно написано, switch-оператор в языке С далеко не так удобен, как case-оператор в Ada.
В обычном программировании «провалы» использовать не стоит:
switch (е) {
casevalue_1:
C |
case value_2:
statement_2; /* автоматический провал на statement_2. */
break;
}
Согласно рис. 6.1 switch -оператор должен использоваться для выбора одного из нескольких возможных путей. «Провал» вносит путаницу, потому что при достижении конца пути управление как бы возвращается обратно к началу дерева выбора. Кроме того, с точки зрения семантики не должна иметь никакого значения последовательность, в которой записаны варианты выбора (хотя в смысле эффективности порядок может быть важен). При сопровождении программы нужно иметь возможность свободно изменять существующие варианты выбора или вставлять новые варианты, не опасаясь внести ошибку. Такую программу, к тому же, трудно тестировать и отлаживать: если ошибка прослежена до оператора statement_2, трудно узнать, был оператор достигнут непосредственным выбором или в результате провала. Чем пользоваться «провалом», лучше общую часть (common_code) оформить как процедуру:
switch (e) {
case value_1 :
C |
common_code();
break;
case value_2:
common_code();
break;
}
Реализация
Самым простым способом является компиляция case-оператора как последовательности проверок:
compute R1 ,ехрг Вычислить выражение
jump_eq R1,#value_1,L1
jump_eq R1,#value_2 ,L2
… Другие значения
default_statement Команды, выполняемые по
умолчанию
jump End_Case
L1: statement_1 Команды для statement_1
jump End_Case
L2: statement_2 Команды для statement_2
jump End_Case
… Команды для других операторов
End_Case:
С точки зрения эффективности очевидно, что чем ближе к верхней части оператора располагается альтернатива, тем более эффективен ее выбор; вы можете переупорядочить альтернативы, чтобы извлечь пользу из этого факта (при условии, что вы не используете «провалы»!).
Некоторые case-операторы можно оптимизировать, используя таблицы переходов. Если набор значений выражения образует короткую непрерывную последовательность, то можно использовать следующий код (подразумевается, что выражение может принимать значения от 0 до 3):
compute R1,expr
mult R1,#len_of_addr expr* длина_адреса
add R1 ,&table + адрес_начала_таблицы
jump (R1) Перейти по адресу в регистре R1
table: Таблица переходов
addr(L1)
addr(L2)
addr(L3)
addr(L4)
L1: statement_1
jump End_Case
L2: statement_2
jump End_Case
L3: statement_3
jump End_Case
L4: statement_4
End_Case:
Значение выражения используется как индекс для таблицы адресов операторов, а команда jump осуществляет переход по адресу, содержащемуся в регистре. Затраты на реализацию варианта с таблицей переходов фиксированы и невелики для всех альтернатив.
Значение выражения обязательно должно лежать внутри ожидаемого диапазона (здесь от 0 до 3), иначе будет вычислен недопустимый адрес, и произойдет переход в такое место памяти, где может даже не быть выполнимой команды! В языке Ada выражение часто может быть проверено во время компиляции:
Ada |
S: Status;
case S is ... -- Имеется в точности четыре значения
В других случаях будет необходима динамическая проверка, чтобы гарантировать, что значение лежит внутри диапазона. Таблицы переходов совместимы даже с альтернативой по умолчанию при условии, что явно заданные варианты выбора расположены непрерывно друг за другом. Компилятор просто вставляет динамическую проверку допустимости использования таблицы переходов; при отрицательном результате проверки вычисляется альтернатива по умолчанию.
Выбор реализации обычно оставляется компилятору, и нет никакой возможности узнать, какая именно реализация используется, без изучения машинного кода. Из документации оптимизирующего компилятора вы, возможно, и узнаете, при каких условиях будет компилироваться таблица переходов. Но даже если вы учтете их при программировании, ваша программа не перестанет быть переносимой, потому что сам case-оператор — переносимый; однако разные компиляторы могут реализовывать его по-разному, поэтому увеличение эффективности не является переносимым.
6.2. Условные операторы
Условный оператор — это частный случай case- или switch-оператора, в котором выражение имеет булев тип. Так как булевы типы имеют только два допустимых значения, условный оператор делает выбор между двумя возможными путями. Условные операторы — это, вероятно, наиболее часто используемые управляющие структуры, поскольку часто применяемые операции отношения возвращают значения булева типа:
C |
statement_1;
else
statement_2;
Как мы обсуждали в разделе 4.4, в языке С нет булева типа. Вместо этого применяются целочисленные значения с условием, что ноль это «ложь» (False), a не ноль — «истина» (Тruе).
Распространенная ошибка состоит в использовании условного оператора для создания булева значения:
Ada |
Result = True;
else
Result = False;
end if;
вместо простого оператора присваивания:
Ada |
Result := X > Y;
Запомните, что значения и переменные булева типа являются «полноправными» объектами: в языке С они просто целые, а в Ada они имеют свой тип, но никак не отличаются от любого другого типа перечисления. Тот факт, что булевы типы имеют специальный статус в условных операторах, не накладывает на них никаких ограничений.
Вложенные if-операторы
Альтернативы в if-операторе сами являются операторами; в частности, они могут быть и if-операторами:
if(x1>y1)
if (x2 > у2)
C |
else
statement_2;
else
if (хЗ > y3)
statemen_3;
else
statement_4;
Желательно не делать слишком глубоких вложений управляющих структур (особенно if-операторов) — максимум три или четыре уровня. Причина в том, что иначе становится трудно проследить логику различных путей. Кроме того, структурирование исходного текста с помощью отступов — всего лишь ориентир: если вы пропустите else, синтаксически оператор может все еще оставаться правильным, хотя работать он будет неправильно.
Другая возможная проблема — «повисший» else:
if (x1 > у1)
C |
statement_1;
else
statement_2;
Как показывают отступы, определение языка связывает else с наиболее глубоко вложенным if-оператором. Если вы хотите связать его с внешним if-оператором, нужно использовать скобки:
if(x1>y1){
if (x2 > у2)
statement_1; }
else
statement_2;
Вложенные if-операторы могут определять полное двоичное дерево выборов (рис. 6.2а) или любое произвольное поддерево. Во многих случаях тем не менее необходимо выбрать одну из последовательностей выходов (рис. 6.26).
Если выбор делается на основе выражения, можно воспользоваться switch-оператором. Однако, если выбор делается на основе последовательности выражений отношения, понадобится последовательность вложенных if-onepa-торов. В этом случае принято отступов не делать:
C |
…
} else if (x > z) {
} else if(y < z) {
} else {
...
}
Явный end if
Синтаксис if-оператора в языке С (и Pascal) требует, чтобы каждый вариант выбора был одиночным оператором. Если вариант состоит из нескольких операторов, они должны быть объединены в отдельный составной (compound) оператор с помощью скобок ({,} в языке С и begin, end в Pascal). Проблема такого синтаксиса состоит в том, что если закрывающая скобка пропущена, то компиляция будет продолжена без извещения об ошибке в том месте, где она сделана. В лучшем случае отсутствие скобки будет отмечено в конце компиляции; а в худшем — количество скобок сбалансируется пропуском какой-либо открывающей скобки и ошибка станет скрытой ошибкой этапа выполнения.
Эту проблему можно облегчить, явно завершая if-оператор. Пропуск закрывающей скобки будет отмечен сразу же, как только другая конструкция (цикл или процедура) окажется завершенной другой скобкой. Синтаксис if-оператора языка Ada таков:
if expression then
statement_list_1;
Ada |
statement_list_2;
end if;
Недостаток этой конструкции в том, что в случае последовательности условий (рис. 6.26) получается запутанная последовательность из end if. Чтобы этого избежать, используется специальная конструкция elsif, которая представляет другое условие и оператор, но не другой if-оператор, так что не требуется никакого дополнительного завершения:
if x > у then
….
Ada |
….
elsif у > z then
…
else
…
end if;
Реализация
Реализация if-оператора проста:
Обратите внимание, что вариант False немного эффективнее, чем вариант True, так как последний выполняет лишнюю команду перехода. На первый взгляд может показаться, что условие вида:
C |
if (!expression)
потребует дополнительную команду для отрицания значения. Однако компиляторы достаточно интеллектуальны для того, чтобы заменить изначальную команду jump_false на jump_true.
Укороченное и полное вычисления
Предположим, что в условном операторе не простое выражение отношения, а составное:
Ada |
if (х > у) and (у > z) and (z < 57) then...
Есть два способа реализации этого оператора. Первый, называемый полным вычислением, вычисляет каждый из компонентов, затем берет булево произведение компонентов и делает переход согласно полученному результату. Вторая реализация, называемая укороченным вычислением (short-circuit)*, вычисляет компоненты один за другим: как только попадется компонент со значением False, делается переход к False-варианту, так как все выражение, очевидно, имеет значение False. Аналогичная ситуация происходит, если составное выражение является or-выражением: если какой-либо компонент имеет значение True, то, очевидно, значение всего выражения будет True.
Выбор между двумя реализациями обычно может быть предоставлен компилятору. В целом укороченное вычисление требует выполнения меньшего числа команд. Однако эти команды включают много переходов, и, возможно, на компьютере с большим кэшем команд (см. раздел 1.7) эффективнее вычислить все компоненты, а переход делать только после полного вычисления.
В языке Pascal оговорено полное вычисление, потому что первоначально он предназначался для компьютера с большим кэшем. Другие языки имеют два набора операций: один для полного вычисления булевых значений и другой — для укороченного. Например, в Ada and используется для полностью вычисляемых булевых операций на булевых и модульных типах, в то время как and then определяет укороченное вычисление:
Ada |
Точно так же or else — эквивалент укороченного вычисления для or.
Язык С содержит три логических оператора: «!» (не), « &&» (и), и «||» (или). Поскольку в С нет настоящего типа Boolean, эти операторы работают с целочисленными операндами и результат определяется в соответствии с интерпретацией, описанной в разделе 4.4. Например, а && b равно единице, если оба операнда не нулевые. Как «&&», так и «||» используют укороченное вычисление. Убедитесь, что вы не спутали эти операции с поразрядными операциями (раздел 5.8).
Относительно стиля программирования можно сказать, что в языке Ada программисты должны выбрать один стиль (либо полное вычисление, либо укороченное) для всей программы, используя другой стиль только в крайнем случае; в языке С вычисления всегда укороченные.
Укороченность вычисления существенна тогда, когда сама возможность вычислить отношение в составном выражении зависит от предыдущего отношения:
Ada |
if (а /= 0) and then (b/a > 25) then .. .