Поддерживающие механизмы
Выражения
Выражение задает вычисление, вырабатывающее значение, - объект или ссылку на объект. Выражениями являются:
- неименованные (манифестные) константы;
- сущности (атрибуты, локальные сущности, формальные аргументы, Result );
- вызовы функций;
- выражения с операторами (технически - это специальный случай вызова функций);
- Current.
Манифестные константы
Неименованная или манифестная константа задается значением, синтаксис которого позволяет определить и тип этого значения, например, целое 0. Этим она отличается от символьной константы, чье имя не зависит от значения.
Булевых констант две, - True и False. Целые константы имеют обычную форму, например:
453 -678 +66623
В записи вещественных (real) констант присутствует десятичная точка. Целая, либо дробная часть может отсутствовать. Может присутствовать знак и экспонента, например:
52.5 -54.44 +45.01 .983 -897. 999.e12
Символьные константы состоят из одного символа в одинарных кавычках, например, 'A'. Для цепочек из нескольких символов используется библиотечный класс STRING, описанный ниже.
Вызовы функций
Вызовы функций имеют такой же синтаксис, как и вызовы процедур. Они могут быть квалифицированные и неквалифицированные: в первом случае используется нотация с многоточием. При соответствующих объявлениях класса и функций, они, например, таковы:
b.f b.g(x, y, ...) b.h(u, v).i.j(x, y, ...)
Правило квалифицированного вызова, приведенное для процедур, применимо также к вызовам функций.
Текущий объект
Зарезервированное слово Current означает текущий экземпляр класса и может использоваться в выражении. Само Current - тоже выражение, а не сущность, допускающая запись. Значит присваивание Current, например, Current := some_value будет синтаксически неверным.
При ссылке на компонент (атрибут или программу) текущего экземпляра нет необходимости писать Current.f, достаточно написать f. Поэтому Current используется реже, чем в ОО-языках, где каждая ссылка на компонент должна быть явно квалифицированной. (Например, в Smalltalk компонент всегда квалифицирован, даже когда он применим к текущему экземпляру.) Случаи, когда надо явно называть Current включают:
- Передачу текущего экземпляра в качестве аргумента в программу, как в a.f (Current). Обычное применение - создание копии (duplicate) текущего экземпляра, как в x: = clone (Current).
- Проверку,- присоединена ли ссылка к текущему экземпляру, как в проверке x = Current.
- Использование Current в качестве опорного элемента в "закрепленном объявлении" в форме like Current ( "Техника наследования" ).
Выражения с операторами
Выражения могут включать знаки операций или операторы.
Унарные операторы + и - применяются к целым и вещественным выражениям и не применяются к булевым выражениям.
Бинарные операторы, имеющие точно два операнда, включают операторы отношения:
= /= < > <= >=
где /= означает "не равно". Значение отношения имеет булев тип.
Выражения могут включать один или несколько операндов, соединенных операторами. Численные операнды могут соединяться следующими операторами:
+ - . / ^ // \\
где // целочисленное деление, \\ целый остаток, а ^ степень (возведение в степень).
Булевы операнды могут соединяться операторами: and, or, xor, and then, or else, implies. Последние три объясняются в следующем разделе; xor - исключающее или.
Предшествование операторов, основанное на соглашениях обычной математики, строится по "Принципу Наименьшей Неожиданности". Во избежание неопределенности и путаницы, в книге используются скобки, даже там, где они не очень нужны.
Нестрогие булевы операторы
Операторы and then и or else (названия заимствованы из языка Ada), а также implies не коммутативны и называются нестрогими (non-strict) булевыми операторами. Их семантика следующая:
Нестрогие булевы операторы
- a and then b ложно, если a ложно, иначе имеет значение b.
- a or else b истинно, если a истинно, иначе имеет значение b.
- a implies b имеет то же значение, что и: (not a) or else b.
Первые два определения, как может показаться, дают ту же семантику, что и and и or. Но разница выявляется, когда b не определено. В этом случае выражения, использующие стандартные булевы операторы, математически не определены, но данные выше определения дают результат: если a ложно, то a and then b ложно независимо от b ; а если a истинно, то a or else b истинно независимо от b. Аналогично, a implies b истинно, если a ложно, даже если b не определено.
Итак, нестрогие операторы могут давать результат, когда стандартные не дают его. Типичный пример:
(i /= 0) and then (j // i = k)
которое, согласно определению, ложно, если i равно 0. Если бы в выражении использовался and, а не and then, то из-за неопределенности второго операнда при i равном 0 статус выражения неясен. Эта неопределенность скажется во время выполнения:
- Если компилятор создает код, вычисляющий оба операнда, то во время выполнения произойдет деление на ноль, и возникнет исключительная ситуация.
- Если же генерируется код, вычисляющий второй операнд только тогда, когда первый истинен, то при i равном 0 возвратится значение ложь.
Для гарантии интерпретации (2), используйте and then. Аналогично,
(i = 0) or else (j // i /= k)
истинно, если i равно 0, а вариант or может дать ошибку во время выполнения.
Можно недоумевать, почему необходимы два новых оператора - не проще и не надежнее ли просто поддерживать стандарт операторов and и or и принимать, что они означают and then и or else? Это не изменило бы значение булева выражения, когда оба оператора определены, но расширило бы круг случаев, где выражения могут получить непротиворечивое значение. Именно так некоторые языки программирования, в частности, ALGOL, W и C, интерпретируют булевы операторы. Однако есть теоретические и практические причины сохранять два набора различных операторов.
- С точки зрения теории, стандартные математические булевы операторы коммутативны: a and b всегда имеет значение такое же, как b and a, в то время как a and then b может быть определенным, когда b and then a не определено. Когда порядок операндов не имеет значения, предпочтительно использовать коммутативный оператор.
- С точки зрения практики, некоторые оптимизации компилятора становятся невозможными, если требуется, чтобы компилятор вычислял операнды в заданном выражением порядке, как в случае с некоммутативными операторами. Поэтому лучше использовать стандартные операторы, если известно, что оба операнда определены.
Отметим, что можно смоделировать нестрогие операторы посредством условных команд на языке, не включающем такие операторы. Например, вместо
b := ((i /= 0) and then (j // i = k))
можно написать
if i = 0 then b := false else b := (j // i = k) end
Нестрогая форма, конечно, проще. Это особенно ясно, когда она используется как условие выхода из цикла:
from i := a.lower invariant -- Для всех элементов из интервала [a.lower .. i - 1], (a @ i) /= x variant a.upper - i until i > a.upper or else (a @ i = x) loop i := i + 1 end; Result := (i <= a.upper)
Цель - сделать Result верным, если и только если значение x находится в массиве a. Использование or здесь будет неверным. В этом случае всегда могут вычисляться два операнда, так что при истинности первого операнда (i > a.upper) произойдет попытка доступа к несуществующему элементу массива a @(aupper+1), что приведет к ошибке во время выполнения (нарушение предусловия при включенной проверке утверждений).
Решение без нестрогих операторов будет неэлегантным.
Другой пример - утверждение, например, инварианта класса, выражающее, что первое значение списка l целых неотрицательно, при условии, что список непустой:
l.empty or else l.first >= 0
При использовании or инвариант был бы некорректен. Здесь нет способа написать условие без нестрогих операторов (кроме написания специальной функции и вызова ее в утверждении). Базовые библиотеки алгоритмов и структур данных содержат много таких случаев.
Оператор implies, описывающий включения, также нестрогий. Форма implies менее привычна, но часто более ясна, например, последний пример выглядит лучше в записи:
(not l.empty) implies (l.first >= 0)