лекции (2003) (Глазкова) (1160821), страница 15
Текст из файла (страница 15)
В языке Паскаль, Си – S1, S2 – один оператор; если нам нужно несколько операторов, то нужно использовать блок, т.е. составной оператор. При подходе «терминатора» необходимость в составном операторе пропадает,. т.к. при этом любая последовательность операторов всегда имеет явное начало и явный конец.
В языке Ada после end требуется соответствующее ключевое слово:
if B then S1; . . . else S2 end if
Часть else S2 может отсутствовать.
Явная расстановка «терминаторов» избавляет от проблемы какую интерпретацию выбрать ( к какому if относится else), но возникает другая проблема: кроме ветвления на два направления, может быть ветвление на несколько направлений, т.е. когда некоторая совокупность условий просто «опрашивается».
Пример на языке Си:
if (B1)
S1
else if (B2)
S2
else if (B3)
S3
…
else if (BN)
SN
Такую структуру можно записать в виде лесенки, но практика показывает, что человеку очень трудно воспринимать вложенные структуры. Между тем, ступенчатые структуры человек воспринимает легко. Поэтому принято записывать не в лесенку, а в столбик. В языке же Ада или Модула-2 при записи такой конструкции появится много явных терминаторов.
Поэтому в Модула-2 (Ада ) появилось ключевое слово elseif (elif):
if B2 then
S1
{elseif (Bi) then
Si}
[else
S]
end
Т.о., и одновариантное, и многовариантное ветвление обслуживается одной и той же составной логической конструкцией.
Существует частный случай многовариантного ветвления, а именно, дискретное ветвление, когда каждое из условий Bi фактически приобретает вид:
выражение e = const.
Когда выражение одно и то же, а меняется только константа, то такой выбор называется дискретным. Впервые в явном виде оператор дискретного выбора появился в языке Паскаль. В принципе, никакой необходимости в этом операторе нет.
Зачем оператор дискретного выбора появился в ЯП?
(В Паскаль- case, в Си- switch, в Ада – case и т.п.)
-
Оператор выбора синтаксически похож на описание вариантной записи.
В частности, в силу принцип согласованности языковых конструкций ввели оператор выбора.
-
Иногда в машинных архитектурах существуют средства, которые позволяют эффективно запрограммировать множественные варианты перехода (4 оператора перехода в Фортран именно для эффективности).
Пример.
Case i of
0 : S1
1: S2
…
255 : S255
end
Способы реализации оператора дискретного выбора:
-
Это частный случай многовариантного выбора, если реализовывать switch через многовариантное ветвление (но получается неэффективно и несимметрично).
-
Через таблицу переходов (эффективный способ). В таблице указывается адрес перехода: goto T[i]. При этом любой вариант выполняется за фиксированное время. Это вторая причина появления оператора дискретного выбора.
Синтаксис языка Оберон:
case e of
<список констант1>: <список операторов> | - «терминатор», завершающий
этот список
………………………………………..
<список константN>: <список операторов> |
[else
S]
end
Ada:
case e of
when <список1> => . . . .
when <список2> => . . . .
. . . . .
else
S
end case
Явного терминатора здесь вводить не нужно (им служит следующее when).
В языке Си общий вид переключателя:
Switch (e) S
Конструкция переключателя явно противоречит принципам структурного программирования.
switch (e)
{ case i : [break;]
…..
default: ….
}
Смысл оператора переключателя:
если (e= = i) goto case i; иначе goto default;
Если нет default, то выход из оператора.
Т.о., схема многовариантной развилки заменена просто оператором условного перехода. Это противоречит принципам структурного программирования.
Для выхода из переключателя используется break; Если мы забыли break; отловить такие ошибки сложно.
Язык Java сознательно не содержит никаких форм оператора перехода. Тем не менее он вынужден следовать стандартной семантики break; (для того, чтобы обеспечить совместимость с привычками программиста на языке Си). Это неявно вводит в язык неконтролируемый оператор перехода.
В языке C# если break; нет, то конструкция ошибочна.
Case 0: i++; break;
Case 1 : ….
Вместо break; можно писать goto case i ( переход на соответствующую метку).
Циклы
Pascal: есть три вида циклов:
-
цикл «пока» - while B do S1;…SN;
-
цикл «до» - repeat S1;…SN; until B; где В – условие перехода.
-
цикл «for (итератор)» - впервые в Алголе
for i := е1 to еn do S
Модула-2:
for i := е1 to еn [step k] do
S1; . . . . . . . SN; end
Использовать step k давало возможность идти с произвольным шагом.
Оберон – минимальный ЯП:
В первом варианте языка (1988) оператора цикла for не существовало.
Впервые цикл for появился в Алгол 60. Вообще говоря, цикл for избыточен (его можно моделировать с помощью цикла while («пока»))
В языке Оберон-2, который является расширением языка Oberon, было добавлено несколько языковых конструкций и появился цикл for в том виде, в каком он был в Модула2.
Несмотря на избыточность, оператор for полезен.
В некоторых случаях циклы «пока» и «до» неадекватны. Поэтому иногда приходится употреблять goto, который делает структуру алгоритма более ясной.
Создатель языков Модула-2 и Оберон поступил проще:
оставил все циклы в стиле языка Паскаль и появилась 4-я форма цикла (наиболее общая):
LOOP
S1;…….Sn;
end
Это бесконечный цикл.
На языке Си это записывается так:
for ( ; ; ){
. . . . . . .
}
Зачем нужны такие циклы ?
-
При параллельном программировании процессы чаще всего описываются в форме процедуры. Тела этих процедур и представляют бесконечный цикл. Когда же этот цикл завершится? Когда кто-то убьет этот процесс.
-
В языке Модула 2 и Оберон существует оператор exit. Этот оператор аналогичен оператору break в Си и С++. Он производит безусловный выход из цикла.
В чем отличие exit от break?
Оператор exit может появиться только внутри цикла loop. Т.о., конструкция exit нужна, чтобы моделировать цикл с произвольным выходом.
И проблема соответствия структуре алгоритма исчезает. Оператор break можно использовать во всех циклах и еще в переключателе case. При этом возникают проблемы, если тело цикла представляет из себя переключатель, и нам нужно выйти не из переключателя, а из тела цикла. (В этом случае нужен goto).
Еще проблема, которая никак не решается в Модула 2 и Оберон, но может быть решена в Си и С++ с помощью оператора перехода. Есть трехмерная матрица, надо выдать индексы первого вхождения 0. Чтобы выйти из всего тройного цикла, надо использовать goto.
В Модула 2 и Оберон эта проблема не решается, т.к. там нет оператора goto.
Как же создатели языка Java решали эту проблему? В языке Java появляется конструкция
break метка, где метка помечает произвольный оператор цикла. По умолчанию break без метки - выход на один уровень.
Аналогичный подход в языке Ада.
Общая форма цикла:
loop
... exit; // выход из произвольного цикла
end loop
Эта форма обобщается до произвольных форм
while B loop
...
end loop
loop
...
while B // аналог цикла "до"
for i in range loop
...
end loop
Проблема : где должна быть описана переменная i? В языке С++ допустимо
for (int i=0; i<size; i++){…}.
Вопрос: что является областью действия переменной i?
В языке Ада область действия - только тело цикла.
В С++ область видимости i - объемлющий блок (это плохо, т.к., если несколько циклов подряд, - повторное объявление переменных).
В новом стандарте переменная i локализована внутри цикла for.
Вспомним, как должна программироваться работа с контейнерами в библиотеке STL языка С++. В любом контейнере описывается вложенный в него класс - итератор (для просмотра контейнера).
container c;
for (container::iterator i=c.begin(); i != c.end(); i++)
{
...
}
В теле цикла теперь можно работать с соответствующим объектом.
Чем удобна такая форма?
Это обрабатывающие алгоритмы: один раз описываем структуру данных и много раз ее обрабатываем.
И если нам надо заменить представление структуры данных, мы меняем в одном месте описание, а все обрабатывающие алгоритмы не меняются. Поэтому всю работу с контейнерами требуют программировать на уровне итераторов.
В результате алгоритм отделен от структуры данных, и это самое большое достоинство STL. Но это все на уровне соглашений. Так почему же соответствующие соглашения не внедрить в язык?
Во многих современных языках начинает появляться конструкция foreach
В языке Perl есть ассоциативный массив.
@names =- {"line1","line2","line3"};
"line1","line2","line3 - это ключи массива.
К элементам этого массива можно обращаться ассоциативно:
@names["line2"];
Как просмотреть все элементы ассоциативного массива? Есть специальная конструкция перебора.
foreach($ names) {println (@names); }
В языке С# пошли дальше: появляется специальная конструкция foreach (для коллекций):
foreach (object о in Сoll) {...}
Сoll - коллекция.
foreach - перебирает все элементы.
Чем foreach в Perl и C# идейно отличаются?
В Perl foreach работает с ассоциативными массивами, в C# - с произвольными коллекциями.
Кроме того, можно создавать пользовательские классы коллекций, к которым будет применим метод foreach.
Возникает явление рефлексия (отображение).
Если коллекция реализует стандартные интерфейсы, к ней применим метод foreach.
Т.о., то, что в С++ делается на уровне соглашений, здесь делается на уровне соглашений с компилятором.
Есть понятие интерфейса, который определяет "контракт". Если коллекция поддерживает этот контракт, к ней применим foreach.
В чем новшество C#?
Введено понятие интерфейса "контракта" между программой и компилятором. Это дает дополнительную гибкость и мощность языку.
Переходы
-
break
-
continue
-
exit
-
return (это тоже возврат из произвольной точки).
return соответствует концепции структурного программирования: процедура - черный ящик; выходить из нее можно разными способами, главное чтобы в одну точку.
Специализированные операторы:
throw – специализированный оператор возбуждение исключения.
Лекция 12
Глава 4. Процедурные абстракции.
В первых трёх главах мы рассматривали только базисные средства ЯП и средства защиты. Эта глава посвящена рассмотрению не базиса ЯП, а средств развития. Самым простым средством развития является процедурная абстракция.
Средство развития - это то, что позволяет создавать что-то новое в языке, не заложенное создателем языка в его базисе. Например, в Фортране были понятия формулы, выражения, оператора присваивания, оператора ввода/вывода и математических функций (например, экспонента). Однако в базисе языка Фортран не было функции вычисления интеграла, но ее можно было добавить средствами развития языка Фортран. Минимально, что нужно для этого:
-
модуль;
-
общие данные
-
процедуры - самый простой способ абстрагирования.
Все эти понятия в зачаточном виде были в стандартном Фортране 66, т.е. даже средства развития стандартного Фортрана 66 позволяли добавить функцию вычисления интеграла в базис языка.
Рассмотрим процедурные абстракции в современных языках программирования.
Процедурная абстракция - это абстрактная операция.
Например в Си или С++:
int f(int i, int j)
f: int * int -> int
Функции в ЯП могут служить некоторой реализацией абстрактного понятия отображение. Но в общем случае, понятие процедуры шире процедурной абстракции, потому что, во-первых, она может никакого значения не возвращать. С математической точки зрения такие отображения не имеют смысла. Самое главное понятие в машине фон Неймана понятие действия – перевод машины из одного состояния в другое (программа и данные при этом каким-то образом изменились).
С этой точки зрения процедура - это абстракция действия, произвольное действие, которое оформлено в виде законченного куска программы. Этим действием может явиться вычисление какого-то значения, побочный эффект (т.е. модификация параметров и/или глобальных переменных) и вообще любое изменение состояния машины.
Для вычисления произвольной процедуры (операции) нужны параметры – некоторые данные.
Эти данные делятся на два класса:
-
локальные данные, нужные только в пределах данной процедуры и явно описанные самим программистом;
-
временные (вспомогательные) данные
Пример локальных данных:
void f() {
int i; //локальные данные, явно описанные программистом.
...
for (i=0;i<N; i++)
{...}
}
Чем же отличаются локальные данные от временных данных?
Локальные данные явно описаны самим программистом. Временные же данные отводятся по усмотрению компилятора. Программист явно не управляет временными данными, тогда как локальными данными он управляет явно.
Пример временных данных: int f(int , int)
При вычислении выражения, возможно, компилятор отводит какие-то временные данные.
Как правило, для простых ЯП (Паскаль, Си) вопрос о размещении временных данных – это псевдо-вопрос.