учебники, программирование, основы, введение в,

 

Поддерживающие механизмы

Взаимодействие с не объектным ПО
До сих пор, элементы ПО выражались полностью в ОО-нотации. Но программы появились задолго до распространения ОО-технологии. Часто возникает необходимость соединить объектное ПО с элементами, написанными, например, на языках С, Fortran или Pascal. Нотация должна поддерживать этот процесс.
Сначала следует рассмотреть языковой механизм, а затем поразмышлять над его более широким значением как части процесса разработки ОО-продукта.
Внешние программы
ОО-системы состоят из классов, образованных компонентами (features), в частности, подпрограммами, содержащими инструкции. Что же является правильным уровнем модульности (granularity) для интегрирования внешнего программного продукта?
Конструкция должна быть общей - это исключает классы, существующие только в ОО-языках. Инструкции - слишком низкий уровень. Последовательность, в которой две ОО-инструкции окаймляют инструкцию на языке С:
-- только в целях иллюстрации
create x l make (clone (a))
(struct A) *x = &y; /* A piece of C */
x.display

трудно было бы понять, проверить, сопровождать.
Остается уровень компонентов. Он разумен и допустим, поскольку инкапсуляция компонентов совместима с ОО-принципами. Класс является реализацией типа данных, защищенных скрытием информации. Компоненты - единицы взаимодействия класса с остальной частью ПО. Поскольку клиенты полагаются на официальную спецификацию компонентов (краткую форму) независящую от их реализации, внешнему миру неважно, как написан компонент - в ОО-нотации или нет.
Отсюда вытекает понятие внешней программы. Внешняя программа имеет большинство признаков нормальной программы: имя, список аргументов, тип результата, если это функция, предусловие и постусловие, если они уместны. Вместо предложения do она имеет предложение external, определяющее язык реализации. Следующий пример взят из класса, описывающего символьные файлы:
put (c: CHARACTER) is
-- Добавить c в конец файла.
require
write_open: open_for_write
external
"C" alias "_char_write";
ensure
one_more: count = old count + 1
end

Предложение alias факультативно и используется, только если оригинальное имя внешней программы отличается от имени, данного в классе. Это случается, когда внешнее имя недопустимо в ОО-нотации, например, имя, начинающееся с символа подчеркивания (используемое в языке С).
Улучшенные варианты
Описанный механизм включает большинство случаев и достаточен для целей описания нашей книги. На практике полезны некоторые уточнения:

  • Некоторые внешние программные элементы могут быть макросами. Они имеют вид подпрограмм в ОО-мире, но любой их вызов предполагает вставку тела макроса в точке вызова. Этого можно достичь вариацией имени языка (как, например, "C:[macro]...").
  • Необходимо также разрешить вызовы программ из "динамически присоединяемых библиотек" (DLL), доступных в Windows и других платформах. Программа DLL загружается динамически во время первого вызова. Имя программы и библиотеки разрешается также задавать динамически в период выполнения. Поддержка DLL должна включать как способ статической спецификации имени, так и полностью динамический подход с использованием библиотечных классов DYNAMIC_LIBRARY и DYNAMIC_ROUTINE. Эти классы можно инициализировать во время выполнения, создавая объекты, представляющие динамически определенные библиотеки и подпрограммы.
  • Необходима и связь в обратном направлении, позволяющая не объектному ПО создавать объекты и вызывать компоненты. Например, графической системе может понадобиться механизм обратного вызова (callback mechanism), вызывающий определенные компоненты класса.

Все эти возможности присутствуют в ОО-среде, описанной в последней лекции. Однако их подробное обсуждение - это отдельный разговор.
Использование внешних программ
Внешние программы являются частью ОО-метода, помогая сочетать старое ПО с новым. Любой метод проектирования ПО, допускающий возможность повторного использования, должен допускать программный код, написанный на других языках. Трудно было бы убедить потенциального пользователя, что надо отказаться от всего существующего ПО, поскольку с этой минуты начинается повторное использование.
Открытость остальному миру - требование большинства программных продуктов. Это можно назвать принципом скромности: авторы новых инструментов должны дать возможность пользователям иметь доступ к ранее имевшимся возможностям.
Внешние программы также необходимы для обеспечения доступа к аппаратуре и возможностям операционной системы. Типичный пример - класс файлов. Другой пример - класс ARRAY, чей интерфейс рассматривался в предыдущих лекциях, и чья реализация основана на внешних программах: процедура создания make использует программу распределения памяти, функция доступа item использует внешний механизм для быстрого доступа к элементам массива, и т.д.
Эта техника обеспечивает ясный интерфейс между ОО-миром и другими подходами. Для клиентов внешняя программа - это просто программа. В примере, программа на С _char_write обрела статус компонента (feature) класса, дополнена предусловием и постусловием и получила стандартное имя put. Возможности, внутренне опирающиеся на не ОО-механизмы, получают новую упаковку абстрактных данных, так что участники ОО-мира начинают рассматривать их как законных граждан сообщества, и их низкое происхождение никогда не упоминается в "изысканном обществе". ("Изысканное общество" не означает бесклассовое.)
ОО-изменение архитектуры (re-architecturing)
Понятие внешней программы хорошо соответствует остальной части подхода. Основной вклад метода - архитектурный: объектная технология говорит, как разработать структуру систем, чтобы обеспечить расширяемость, надежность и повторное использование. Она также говорит, как заполнить эту структуру. Но что по-настоящему определяет, является ли система объектной, - так это ее модульная организация. Для использования ОО-архитектуры часто разумно использовать прием, называемый обертыванием (wrap), одевая в одежды класса внутренние элементы.
Крайний, но не совсем абсурдный, способ использования нотации - построить систему полностью на внешних программах. Объектная технология тогда служит просто инструментом упаковки, использующим мощные механизмы инкапсуляции: классы, утверждения, скрытие информации, клиент, наследственность.
Но обычно нет причины заходить так далеко. ОО-нотация адекватна вычислениям любого рода и столь же эффективна, как и вычисления на языках Fortran или C. В каких случаях полезна ОО-инкапсуляция внешнего ПО? Один из них мы видели: обеспечение доступа к операциям, зависящим от платформы. Другой - проблема многих организаций - управление старым ПО, доставшимся в наследство и продолжающим широко использоваться. Объектная технология предлагает возможность обновления таких систем, изменяя их архитектуру, но не переписывая их полностью.
Эта техника, которую можно назвать ОО-перестройкой (object-oriented re-architecturing) дает интересное решение сохранения ценных свойств существующего ПО, готовя его к будущему расширению и эволюции.
Однако для этого необходимы определенные условия:

  • Необходимо суметь подобрать хорошие абстракции для старого ПО, которое, не будучи объектным, как правило, имеет дело с абстракциями функций, а не данных. Но в этом и состоит задача - обернуть старые функции в новые классы. Если с выделением абстракций не удастся справиться, то никакая ОО-перестройка не поможет.
  • Наследуемое ПО должно быть хорошего качества. Перестроенное старье остается старьем - возможно хуже первоначального, поскольку оно будет скрыто под слоями абстракции.

Эти два требования частично сходны, поскольку качество любого ПО в значительной степени определяется качеством его структуры.
Когда они выполнены, можно использовать внешний механизм для построения интересного ОО-программного продукта, основанного на прежних разработках. Приведем два примера, являющихся частью среды, описанной в последней лекции.

  • Библиотека Vision (библиотеки описываются вкурса "Основы объектно-ориентированного проектирования") дает переносимую графику и механизмы пользовательского интерфейса, позволяющие разработчикам создавать графические приложения для многих различных платформ с ощущением обычной перекомпиляции. Внутренне, она основана на "родных" механизмах, используемых во внешних программах. Точнее, ее нижний уровень инкапсулирует механизмы соответствующих платформ.
  • Другая библиотека, Math, обеспечивает широкий набор возможностей численных вычислений в таких областях как теория вероятностей, статистика, численное интегрирование, линейные и нелинейные уравнения, дифференциальные уравнения, оптимизация, быстрое преобразование Фурье, анализ временных рядов. Внутренне она основана на коммерческой библиотеке подпрограмм, библиотеке NAG от Nag Ltd., Oxford, но обеспечивает пользователям ОО-интерфейс. Библиотека скрывает используемые ею программы и предлагает абстрактные объекты, понятные математику, физику или экономисту, представленные классами: INTEGRATOR, BASIC_MATRIX, DISCRETE_FUNCTION, EXPONENTIAL_DISTRIBUTION. Прекрасные результаты достигаются благодаря качеству внешних программ - NAG аккумулирует сотни человеко-лет разработки и реализации численных алгоритмов. К нему добавлены ОО-преимущества: классы, скрытие информации, множественное наследование, утверждения, систематическая обработка ошибок через исключительные ситуации, согласованное именование.

Эти примеры типичны для сочетания лучших традиционных программных продуктов и объектной технологии.
Вопрос совместимости: гибридный программный продукт или гибридные языки?
Теоретически, мало кто не согласится с принципом скромности или будет отрицать необходимость механизма интеграции между ОО-разработками и старым ПО. Противоречия возникают, когда выбирается уровень интеграции.
Многие языки - самыми известными являются Objective-C, C++, Java, Object Pascal и Ada 95 - пошли по пути добавления ОО-конструкций в существовавший не ОО-язык. Они известны как гибридные языки (hybrid languages) - см.курса "Основы объектно-ориентированного проектирования".
Техника интеграции, описанная выше, основывалась на внешних программах и ОО-перестройке. Это другой принцип: необходимость в совместимости ПО не означает перегрузку языка механизмами, могущими расходиться с принципами объектной технологии.

  • Гибрид добавляет новый языковой уровень к существующему языку, например С. В результате сложность может ограничить привлекательность объектной технологии - простоту идей.
  • Начинающие часто с трудом осваивают гибридный язык, поскольку для них неясно, что именно является ОО, а что досталось из прошлого.
  • Старые механизмы могут быть несовместимыми, по крайней мере, с некоторыми аспектами ОО-идей. Есть много примеров несоответствий между системой типов языков С или Pascal и ОО-подходом.
  • Не объектные механизмы часто конкурируют со своими аналогами. Например, C++ предлагает, наряду с динамическим связыванием, возможность динамического выбора, используя аппарат указателей функций. Это смущает неспециалиста, не понимающего, какой подход выбрать в данном случае. В результате, программный продукт, хотя и создан ОО-средой, по сути является реализацией на языке С, и не дает ожидаемого качества и производительности, дискредитируя объектную технологию.

Если целью является получение наилучших программных продуктов и процесса их разработки, то компромисс на уровне языка кажется неправильным подходом. Взаимодействие (Interfacing) ОО-инструментария и приемов с достижениями прошлого и смешивание (mixing) различных уровней технологии - не одно и то же.


Можно привести пример из электроники. Конечно, полезно сочетать различные уровни технологии в одной системе, например, звуковой усилитель включает несколько диодов наряду с транзисторами и интегральными схемами. Но мало проку от компонента, который является полудиодом, полутранзистором.

ОО-разработка должна обеспечивать совместимость с ПО, построенным на других подходах, но не за счет преимуществ и целостности метода. Этого и достигает внешний механизм: отдельные миры, каждый из которых состоятелен и имеет свои достоинства, и четкий интерфейс, обеспечивающий взаимодействие между ними.

Передача аргументов

Один из аспектов нотации требует разъяснений: что происходит со значениями, переданными в качестве аргументов подпрограмме?
Рассмотрим вызов в форме
r (a1, a2, ..., an)

соответствующий программе
r (x1: T1, x2: T2, ..., xn: Tn) is ...

где r может быть как функцией, так и процедурой, и вызов может быть квалифицированным, как в b.r (...). Выражения a1, a2, ..., an называются фактическими аргументами, а xi - формальными. (Помните, что для родовых параметров типа остается термин "параметр".)
Встают важные вопросы: каково соответствие между фактическими и формальными аргументами? Какие операции допустимы над формальными аргументами? Каково их влияние на соответствующие фактические аргументы?
Ответ на первый вопрос: эффект связывания фактических - формальных аргументов таков же как соответствующего присваивания. Обе операции называются присоединением (attachment). В предыдущем вызове можно считать, что запуск программы начинается с выполнения команд, неформально эквивалентных присваиваниям:
x1 := a1; x2 := a2;... xn := an

Ответ на второй вопрос: внутри тела программы любой формальный аргумент x защищен. Программа не может применять к нему прямых модификаций, таких как:

  • Присваивание x значения в форме x := ...
  • Процедуры создания, где x является целью: create x.make (...)

Читатели, знакомые с механизмом передачи, известным как вызов по значению, поймут, что здесь ограничения более строгое: при вызове по значению формальные аргументы инициализируются значениями фактических, но затем могут быть целью любых операций.

Ответ на третий вопрос - что может программа делать с фактическими аргументами? - вытекает из того, что присоединение используется для задания семантики связывания формальных и фактических аргументов. Присоединение означает копирование либо ссылки, либо объекта. Это зависит от того, являются ли соответствующие типы развернутыми:

  • Для ссылок (обычный случай) при передаче аргументов копируется ссылка, - Void, либо присоединенная к объекту.
  • Для развернутых типов (включающих основные типы INTEGER, REAL и т.п.), при передаче аргументов копируется объект.

В первом случае, запрет операций прямой модификации означает, что нельзя модифицировать ссылку (reference) через повторное присоединение или создание. Но если ссылка не пустая, то разрешается модифицировать присоединенный объект.
Если xi - один из формальных аргументов r, то тело программы может содержать вызов:
xi.p (...)

где p - процедура, применимая к xi, (объявлена в базовом классе типа Ti аргумента xi). Процедура может модифицировать поля объекта, присоединенного к xi во время выполнения, то есть объекта, присоединенного к соответствующему фактическому аргументу ai.
Вызов q (a) никогда не может изменить значение a, если a развернутого типа и является объектом. Если же a является ссылкой, то ссылка не меняется, но объект, присоединенный к ней, может измениться в результате вызова.
Существует много причин, по которым не следует позволять программам прямую модификацию их аргументов. Одна из самых убедительных - Конфликтующие присваивания. Предположим, что язык допускает присваивания аргументам, и процедура
dont_I_look_innocuous (a, b: INTEGER) is -- я выгляжу
-- безвредной, но не стоит мне доверять.
do
a := 0; b := 1
end

Теперь рассмотрим вызов dont_I_look_innocuous (x, x). Каково значение x после возвращения: 0 или 1? Ответ зависит от того, как компилятор реализует изменения формальных - фактических аргументов при выходе программы. Это ставит в тупик не только программистов, использующих язык Fortran.
Разрешение программе изменять аргументы приводит к ограничениям на фактические аргументы. В этом случае он должен быть элементом, способным изменять свое значение, что допустимо для переменных, но не постоянных атрибутов (см.). Недопустимым фактическим аргументом становится сущность Current, выражения, такие как a + b. Устранение модификации аргументов позволяет избежать подобных ограничений и использовать любые выражения в качестве фактических аргументов.
Следствием этих правил является признание того, что только три способа допускают модификацию значения ссылки x: процедура создания create x...; присваивание x := y; и попытка присваивания x ?= y, обсуждаемая ниже. Передача x как фактического аргумента никогда не модифицирует x.
Это также означает, что процедура не возвращает ни одного результата, функция - официальный результат, представленный сущностью Result. Для получения нескольких результатов необходимо одно из двух:

  • Использовать функцию, возвращающую объект с несколькими полями (обычно, возвращается ссылка на такой объект).
  • Использовать процедуру, изменяющую поля объектов соответствующих атрибутов. Затем клиент может выполнять запросы к этим полям.

Первый прием уместен, когда речь идет о составном результате. Например, функция не может возвращать два значения, соответствующих заглавию и году публикации книги, но может возвращать одно значение типа BOOK, с атрибутами title и publication_year. В более общих ситуациях применяются процедуры. Эта техника будет обсуждаться вместе с вопросом побочных эффектов в разделе принципов модульного проектирования.

Инструкции

ОО-нотация, разработанная в этой книге, императивна: вычисления специфицируются через команды (commands), также называемые инструкциями (instructions). (Мы избегаем обычно применимого термина оператор (предложение) (statement), поскольку в слове есть оттенок выражения, описывающего факты, а хотелось подчеркнуть императивный характер команды.)
Для имеющих опыт работы с современными языками инструкции выглядят как хорошие знакомые. Исключение составляют некоторые специальные свойства циклов, облегчающие их верификацию. Вот список инструкций: Вызов процедуры, Присваивание, Условие, Множественный выбор, Цикл, Проверка, Отладка, Повторное выполнение, Попытка присваивания.
Вызов процедуры
При вызове указывается имя подпрограммы, возможно, с фактическими аргументами. В инструкции вызова подпрограмма должна быть процедурой. Вызов функции является выражением. Хотя сейчас нас интересуют инструкции, следующие правила применимы в обоих случаях.
Вызов может быть квалифицированным или неквалифицированным. Для неквалифицированного вызова подпрограммы из включающего класса в качестве цели используется текущий экземпляр класса. Этот вызов имеет вид:
r (без аргументов), или
r (x, y, ...) (с аргументами)
Квалифицированный вызов явно называет свою цель, заданную некоторым выражением. Если a - выражение некоторого типа, C - базовый класс этого типа, а - q одна из программ C, то квалифицированный вызов имеет форму a.q. Опять же, за q может следовать список фактических аргументов; a может быть неквалифицированным вызовом функции с аргументами, как в p (m).q (n), где p(m) - это цель. В качестве цели можно также использовать более сложное выражение при условии заключения его в скобки, как в (vector1 + vector2).count.
Также разрешаются квалифицированные вызовы с многоточием в форме: a.q1q2 ...qn, где a, так же, как и qi , может включать список фактических аргументов.
Экспорт управляет применением квалифицированных вызовов. Напомним, что компонент f, объявленный в классе B, доступен в классе A (экспортирован классу), если предложение feature, объявляющее f, начинается с feature (без дальнейшего уточнения) или feature {X, Y,... }, где один из элементов списка {X, Y,...} является A или предком A. Имеет место:
Правило Квалифицированного Вызова
Квалифицированный вызов вида b.q1. q2.... qn, появляющийся в классе C корректен, только если он удовлетворяет следующим условиям:

  1. Компонент, стоящий после первой точки, q1, должно быть доступен в классе C.
  2. В вызове с многоточием, каждый компонент после второй точки, то есть, каждое qi для i > 1, должен быть доступен в классе C.

Чтобы понять причину существования второго правила, отметим, что a.q.r.s - краткая запись для
b:= a.q; c:=b.r; c.s

которая верна только, если q, r и s доступны классу C, в котором появляется этот фрагмент. Не имеет значения, доступно ли r базовому классу типа q, и доступно ли s базовому классу типа r.


Вызовы могут иметь инфиксную или префиксную форму. Выражение a + b, записанное в инфиксной форме, может быть переписано в префиксной форме: a.plus (b). Для обеих форм действуют одинаковые правила применимости.

Присваивание (Assignment)
Инструкция присваивания записывается в виде:
x := e

где x - сущность, допускающая запись (writable), а e - выражение совместимого типа. Такая сущность может быть:

  • неконстантным атрибутом включающего класса;
  • локальной сущностью включающей подпрограммы. Для функции допустима сущность Result.

Сущности, не допускающие запись, включают константные атрибуты и формальные аргументы программы - которым, как мы видели, подпрограмма не может присваивать новое значение.
Создание (Creation)
Инструкция создания изучалась в предыдущих лекцияхв двух ее формах: без процедуры создания, как в create x, и с процедурой создания, как в create x.p (...). В обоих случаях x должна быть сущностью, допускающей запись.
Условная Инструкция (Conditional)
Эта инструкция задает различные формы обработки в зависимости от выполнения определенных условий. Основная форма:
if boolean_expression then
instruction; instruction; ...
else
instruction; instruction; ...
end

где каждая ветвь может иметь произвольное число инструкций (а возможно и не иметь их).
Будут выполняться инструкции первой ветви, если boolean_expression верно, а иначе - второй ветви. Можно опустить часть else, если второй список инструкций пуст, что дает:
if boolean_expression then
instruction; instruction; ...
end

Когда есть более двух возможных случаев, можно избежать вложения (nesting) условных команд в частях else, используя одну или более ветвей elseif, как в:
if c1 then
instruction; instruction; ...
elseif c2 then
instruction; instruction; ...
elseif c3 then
instruction; instruction; ...
...
else
instruction; instruction; ...
end

где часть else остается факультативной. Это дает возможность избежать вложения
if c1 then
instruction; instruction; ...
else
if c2 then
instruction; instruction; ...
else
if c3 then
instruction; instruction; ...
...
else
instruction; instruction; ...
end
end
end

Когда необходим множественный разбор случаев, более удобна инструкция множественного выбора inspect, обсуждаемая ниже.
ОО-метод, благодаря полиморфизму и динамическому связыванию, уменьшает необходимость явных условных инструкций и множественного выбора, поддерживая неявную форму выбора. Когда объект применяет некоторый компонент, имеющий несколько вариантов, то во время выполнения нужный вариант выбирается автоматически в соответствии с типом объекта. Этот неявный стиль выбора обычно предпочтительнее, но, конечно, инструкции явного выбора остаются необходимыми.
Множественный выбор
Инструкция множественного выбора (также известная, как инструкция Case) производит разбор вариантов, имеющих форму: e = vi , где e - выражение, а vi - константы то же типа. Хотя условная инструкция (if e = v1 then ...elseif e = v2 then...) работает, есть две причины, оправдывающие применение специальной инструкции, что является исключением из обычного правила: "если нотация дает хороший способ сделать что-то, нет необходимости вводить другой способ". Вот эти причины:

  • Разбор случаев настолько распространен, что заслуживает особого синтаксиса, увеличивающего ясность, позволяя избежать бесполезного повторения "e =".
  • Компиляторы могут использовать особенно эффективную технику реализации, - таблицу переходов (jump table), - неприменимую к общим условным инструкциям и избегающую явных проверок.

Что касается типа анализируемых величин (тип e и vi), то инструкции множественного выбора достаточно поддерживать только целые и булевы значения. Согласно правилу, они фактически должны объявляться либо все как INTEGER, либо как CHARACTER. Общая форма инструкции такова:
inspect
e
when v1 then
instruction; instruction; ...
when v2 then
instruction; instruction; ...
...
else
instruction; instruction; ...
end

Все значения vi должны быть различными; часть else факультативна; каждая из ветвей может иметь произвольное число инструкций или не иметь их.
Инструкция действует так: если значение e равно значению vi (это может быть только для одного из них), выполняются инструкции соответствующей ветви; иначе, выполняются инструкции в ветви else, если они есть.
Если отсутствует else, и значение e не соответствует ни одному vi, то возникает исключительная ситуация ("Некорректно проверяемое значение"). Это решение может вызвать удивление, поскольку соответствующая условная инструкция в этом случае ничего не делает. Но оно характеризует специфику инструкции множественного выбора. Когда вы пишете inspect с набором значений vi, нужно включить ветвь else, даже пустую, если вы понимаете, что во время выполнения значения e могут не соответствовать никаким vi. Если вы не включаете else, то это эквивалентно явному утверждению: "значение e всегда является одним из vi". Проверяя это утверждение и создавая исключительную ситуацию при его нарушении, реализация оказывает нам услугу. Бездействие в данной ситуации - означает ошибку - в любом случае, ее необходимо устранить как можно раньше.
Одно из частых приложений инструкции множественного выбора - анализ символа, введенного пользователем:
inspect
first_input_letter
when 'D' then
"Удалить строку"
when 'I' then
"Вставить строку"
...
else
message ("Неопознанная команда; введите H для получения справки")
end

Когда значения vi целые, то они могут быть определены как уникальные (unique values), концепция которых рассмотрена в следующей лекции. Это делает возможным в объявлении определить несколько абстрактных констант, например, Do, Re, Mi, Fa, Sol, La, Si: INTEGER is unique, и затем анализировать их в инструкции: inspect note when Do then...when Re then...end.
Как и условные инструкции, инструкции множественного выбора не должны использоваться для замены неявного выбора, основанного на динамическом связывании.
Циклы
Синтаксис циклов описан при обсуждении Проектирования по Контракту :
from
initialization_instructions
invariant
invariant
variant
variant
until
exit_condition
loop
loop_instructions
end

Предложения invariant и variant факультативны. Предложение from требуется, хотя и может быть пустым. Оно задает инициализацию параметров цикла. Не рассматривая сейчас факультативные предложения, выполнение цикла можно описать следующим образом. Вначале происходит инициализация, и выполняются initialization_instructions. Затем следует "циклический процесс", определяемый так: если exit_condition верно, то циклический процесс - пустая инструкция (null instruction); если условие неверно, то циклический процесс - это выполнение loop_instructions, затем следует (рекурсивно) повторение циклического процесса.
Проверка
Инструкция проверки рассматривалась при обсуждении утверждений . Она говорит, что определенные утверждения должны удовлетворяться в определенных точках:
check
assertion -- Одно или больше предложений
end

Отладка
Инструкция отладки является средством условной компиляции. Она записывается так:
debug instruction; instruction; ... end

В файле управления (Ace-файле) для каждого класса можно включить или отключить параметр debug. При его включении все инструкция отладки данного класса выполняются, при отключении - они не влияют на выполнение.
Эту инструкцию можно использовать для включения специальных действий, выполняющихся только в режиме отладки, например, печати некоторых величин.
Повторение вычислений
Инструкция повторного выполнения рассматривалась при обсуждении исключительных ситуаций . Она появляется только в предложении rescue, повторно запуская тело подпрограммы, работа которой была прервана.

Выражения

Выражение задает вычисление, вырабатывающее значение, - объект или ссылку на объект. Выражениями являются:

  • неименованные (манифестные) константы;
  • сущности (атрибуты, локальные сущности, формальные аргументы, 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 and then b истинно независимо от b. Аналогично, a implies b истинно, если a ложно, даже если b не определено.
Итак, нестрогие операторы могут давать результат, когда стандартные не дают его. Типичный пример:
(i /= 0) and then (j // i = k)

которое, согласно определению, ложно, если i равно 0. Если бы в выражении использовался and, а не and then, то из-за неопределенности второго операнда при i равном 0 статус выражения неясен. Эта неопределенность скажется во время выполнения:

  1. Если компилятор создает код, вычисляющий оба операнда, то во время выполнения произойдет деление на ноль, и возникнет исключительная ситуация.
  2. Если же генерируется код, вычисляющий второй операнд только тогда, когда первый истинен, то при 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)

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика