Ada 95 и объектная технология: оценка
Если рассматривать язык Ada 95 с позиций объектной технологии, то сначала он может привести в замешательство. Со временем, освоив различные языковые механизмы, можно добиться эффекта единичного наследования, полиморфизма и динамического связывания.
Однако цена этого - сложность. К сложному языку Ada 83 добавился новый набор понятий со многими внутренними связями и связями со старыми конструкциями. При сравнении с ОО-методом, где введено достаточно простое понятие класса, обнаружится, что в Ada 95 нужно изучить, по крайней мере, пять сложных понятий:
пакеты, являющиеся модулями, но не типами, могут быть родовыми, предлагая нечто похожее на наследование: дочерние пакеты (как и ряд других возможностей, не описанных подробно выше, таких как возможность объявления дочернего пакета как private);дескрипторные типы записей, являющиеся типами, но не модулями и имеющие некоторую форму наследования, хотя в отличие от классов они не позволяют синтаксического включения подпрограмм в объявление типа;задачи, являющиеся модулями, но не типами и не имеющие наследования;типы задач, являющиеся модулями и типами, но без возможности быть родовыми (хотя они могут включаться в родовые пакеты) и не имеющие наследования;"защищенные типы" (понятие, до сих пор не встречавшееся), являющиеся типами и включающие подпрограммы, что делает их похожими на классы, но без наследования: protected type ANOTHER_ACCOUNT_TYPE is procedure deposit (amount: in MONEY); function balance return MONEY; private deposit_list: ...; ... end ANOTHER_ACCOUNT_TYPE;Комбинация возможностей взаимодействия поразительна. Например, пакеты имеют, в добавление к понятию дочернего пакета, механизмы Ada use и with. В одном из руководств дается следующее объяснение:
Закрытые потомки предназначены для "внутренних" пакетов, которые должны применять механизм with только к ограниченному числу пакетов. Закрытый потомок может применить механизм with только к телу своего родителя или к его потомкам. В обмен на такое ограничиние потомок получает новые полномочия: его спецификация автоматически видима в открытых и закрытых частях спецификаций всех его предков.Без сомнения, можно уловить смысл подобных объяснений. Но стоит ли результат усилий?
Интересно отметить, что Жан Ичбиа, создатель языка Ada, публично покинул аналитическую группу Ada 95 после тщетных попыток сохранить расширения простыми. В его пространном заявлении об уходе дается следующий комментарий: дополнительные возможности приведут в результате к огромному увеличению сложности в 9X [позже Ada 95]... В 9X количество рассматриваемых взаимодействий приближается к 60000. |
Базовые понятия объектной технологии, при всей их силе, удивительно просты. В языке Ada 95 предпринята, возможно, самая амбициозная попытка сделать их сложными.
Использование пакета
Приведем пример из клиентского пакета, использующего стек вещественных чисел:
s: REAL_STACKS.STACK (1000); REAL_STACKS.put (3.5, s); ...; if REAL_STACKS.empty (s) then ...;Среда языка Ada должна иметь возможность компилировать такой клиентский код, располагая только интерфейсом REAL_STACKS, не имея доступа к его телу.
Синтаксически каждое использование сущности (здесь "сущности" включают имена программ и типов) повторяет имя пакета REAL_STACKS. Это утомительно - необходима неявная форма квалификации. Если включена директива:
use REAL_STACKS;в начале клиентского пакета, то выражения записываются проще:
s: STACK (1000); put (3.5, s); ...; if empty (s) then ...;Конечно, используется и полная форма для сущности, чье имя вступает в конфликт с именем, указанным в другом доступном пакете (скажем, объявленное в самом пакете или в пакете из списка в директиве use).
В литературе по языку Ada иногда встречается совет программистам вообще не использовать директиву use, поскольку она мешает ясности: неквалифицированная ссылка, например вызов empty (s), сразу не говорит о поставщике empty (в нашем примере REAL_STACKS). Его аналог в ОО-подходе, s.empty, однозначно определяет поставщика через цель s.
В ОО-мире подобная проблема возникает из-за наследования: имя в классе может ссылаться на компонент, объявленный любым из предков. Техника, частично решающая проблему, - это плоская форма класса.
Немного контекста
Создание языка Ada было реакцией на кризис середины 70-х годов, ощутимый для политики в области разработки ПО в Департаменте Обороны США (DoD). В отчете, предшествовашем появлению языка Ada, отмечалось, что в военной отрасли в тот момент использовалось более 450 языков программирования, многие из которых технически устарели. Все это мешало управлению подрядными работами, обучению программистов, техническому прогрессу, разработке качественного ПО и контролю цен.
Помня об успехе языка COBOL, разработанного в 50-х годах по запросу DoD, был объявлен конкурс на разработку современного языка создания ПО. Одна из заявленных целей - возможность поддержки встроенных приложений в режиме реального времени. В результате были отобраны четыре, затем - два, и, наконец, в 1979 году, после действительно справедливого отбора, победителем оказался язык Green, созданный Жаном Ичбиа (Jean D. Ichbiah) и его группой CII-Honeywell Bull. На основе опыта нескольких лет и первых промышленных реализаций язык был пересмотрен и в 1983 году был принят как стандарт ANSI.
Язык Ada (так был назван язык Green) начал новый этап в разработке языков. Никогда раньше язык не подвергался такому интенсивному испытанию перед выпуском. Никогда раньше создание языка не трактовалось как крупномасштабный инженерный проект. Лучшие эксперты многих стран в составе рабочих групп проводили недели, рассматривая предложения и делая - в те доинтернетовские дни - большое количество комментариев. Подобно языку Algol 60 в предыдущем поколении языков, Ada определил не только языковую перспективу, но и само понятие разработки языка.
Дальнейший пересмотр языка Ada привел к новой версии языка, официально называемой Ada 95, описываемой в конце данной лекции. В других частях курса название Ada без дальнейшего уточнения относится к версии Ada 83, широко используемой и сегодня.
Был ли язык Ada успешным? И да, и нет. Департамент Обороны получил то, что заказывал: благодаря строгому выполнению "поручения" язык Ada стал через несколько лет доминирующим техническим языком различных отраслей Американской военной промышленности и военных организаций некоторых других стран.
Он используется в таких невоенных правительственных агентствах, как NASA и Европейское Космическое Агентство. Но, кроме некоторого проникновения в сферу обучения теории вычислительных систем - частично по инициативе Департамента Обороны, - этот язык имел лишь ограниченный успех в остальном мире ПО. Возможно, он бы распространился шире, если бы не конкуренция со стороны объектной технологии, внезапно появившейся на сцене, как раз тогда, когда язык Ada и промышленность созрели друг для друга.
По иронии судьбы разработчики языка Ada были хорошо знакомы с ОО-идеями. Хотя это не всем известно, Ичбиа создал один из первых компиляторов для Simula 67 - первого ОО-языка. Позже, когда его спрашивали, почему он не представил ОО-проект Департаменту Обороны, он объяснял, что в контексте конкуренции такой проект посчитали бы настолько далеким от основного направления, что у него было бы шансов на победу. И он, без сомнения, прав. Действительно, до сих пор можно удивляться смелости проекта, принятого DoD. Было разумно ожидать, что процесс приведет к чему-то вроде усовершенствованной версии языка JOVIAL (языка военных приложений 60-х гг.). Но все четыре отобранных языка были основаны на языке Pascal, с его явным академическим привкусом. А Ada являлся воплощением новых смелых идей во многих областях, например, в обработке исключений, универсальности и параллелизме. Ирония состоит и в том, что язык Ada, направленный на поддержание соответствия проектов DoD прогрессу в разработках ПО, вытесняя старые подходы, в последующие годы невольно привел к задержке принятия новой (post-Ada) технологии в военном и космическом сообществе.
Уроки языка Ada остаются незаменимыми, и жаль, что многие ОО-языки 80-х и 90-х гг. не обращали большего внимания на акцент качества программной инженерии, характерный для языка Ada. Хотя в этой книге мы неоднократно будем противопоставлять решения, принятые в языке Ada, методам, принятым в объектной технологии, но эти замечания следует воспринимать не как укор, а как дань уважения к предшественнику, в сравнении с которым должны оцениваться новые методы.
Обсуждение: наследование модулей и типов
При изучении языка Ada 95 попутно интересно отметить, что разработчики Ada 95 считали необходимым помимо механизма наследования для дескрипторных типов ввести понятие пакета потомка. Язык Ada, конечно, всегда разделял понятия модуля и типа, в то время как классы объединяют эти два понятия. Но методологи языка Ada 95 предлагают при введении типа наследника, такого как SAVINGS_ACCOUNТ, объявлять его в целях ясности и модульности не в первоначальном пакете (Accounts), а в пакете потомка. Если обобщить этот совет, то дойдет до создания, наряду с иерархией типов, иерархии модулей, строго ему следующей.
У классов в объектной технологии такие вопросы не возникают. Классы являются модулями, и существует только одна иерархия.
Выбор, сделанный в Ada 95, является еще одним примером популярного взгляда, что "следует отделять наследование типа от повторного использования кода". Понимание же объектной технологии, начиная с языка Simula, заключается в соединении понятий - модуля и типа, подтипов и модульного расширения. Как и любое другое смелое соединение понятий, считавшихся ранее совершенно различными, эта идея могла временами пугать, но без нее мы бы лишились замечательного упрощения архитектуры ПО.
ОО-механизмы языка Ada 95: пример
Текст ниже приведенного пакета иллюстрирует некоторые технические приемы Ada 95. Его смысл должен быть достаточно ясен для читателя. Для получения нового типа с дополнительными полями (форма наследования Ada 95), нужно объявить уже существующий тип, такой как ACCOUNT, как дескрипторный (tagged). Это, конечно, противоречит принципу Открыт-Закрыт, поскольку необходимо знать заранее, какие типы могут иметь потомков, а какие - нет. Множественное наследование отсутствует, так что тип new можно получить только из одного типа. Обратите внимание на синтаксис получения нового типа без добавления атрибутов (null record, к удивлению, без end).
package Accounts is type MONEY is digits 12 delta 0.01; type ACCOUNT is tagged private; procedure deposit (a: in out ACCOUNT; amount: in MONEY); procedure withdraw (a: in out ACCOUNT; amount: in MONEY); function balance (a: in ACCOUNT) return MONEY; type CHECKING_ACCOUNT is new ACCOUNT with private; function balance (a: in CHECKING_ACCOUNT) return MONEY; type SAVINGS_ACCOUNT is new ACCOUNT with private; procedure compound (a: in out SAVINGS_ACCOUNT; period: in Positive); private type ACCOUNT is tagged record initial_balance: MONEY := 0.0; owner: String (1..30); end record; type CHECKING_ACCOUNT is new ACCOUNT with null record; type SAVINGS_ACCOUNT is new ACCOUNT with record rate: Float; end record; end Accounts;Дескрипторные типы по-прежнему объявляются как записи. Основное свойство большинства ОО-языков - операции над типом являются частью типа и фактически определяют тип - здесь не работает. Подпрограммы задаются вне объявления типа и принимают в качестве аргумента значение типа. (В ОО-языках, deposit и т. д. будут частью объявления ACCOUNT, а compound - частью SAVINGS_ACCOUNT, им не требуются их первые аргументы.) Здесь же все, что требуется, - так это объявление подпрограмм и типа как части одного и того же пакета; им даже не нужно находиться рядом друг с другом. В приведенном примере, только расположение показывает читателю, что определенные программы концептуально связаны с определенными дескрипторными типами записей.
Это отличается от обычного взгляда на создание ОО ПО. Хотя дескрипторный тип и связанные с ним подпрограммы с теоретической точки зрения являются частью одного абстрактного типа данных, они не образуют синтаксической единицы. Это противоречит принципу Лингвистических Модульных Единиц, предполагающему тесную связь между модульной концепцией и синтаксической структурой.
Появление нового объявления для balance в SAVINGS_ ACCOUNT сигнализирует о переопределении. Процедуры withdraw и deposit не переопределяются. Как будет понятно, это означает, что Ada 95 использует механизм перегрузки для получения ОО-эффекта от переопределения подпрограмм. Не существует синтаксической метки (как redefine), сигнализирующей о переопределении. Чтобы увидеть, что функция balance в SAVINGS_ACCOUNT отличается от базовой версии в ACCOUNT, следует просмотреть весь текст пакета. В данном случае каждая версия подпрограммы находится рядом с соответствующим типом, с отступами для выделения этой связи, но это условность стиля, а не правило языка.
Дескрипторный тип может объявляться как abstract, соответствуя понятию отложенного класса. Подпрограмму также можно сделать abstract, не создавая для нее тело.
Функция, возвращающая результат абстрактного типа, должна сама быть абстрактной. Это правило сначала может показаться странным и помехой для написания эффективной функции, возвращающей, скажем, вершину стека фигур в предположении, что тип FIGURE абстрактный. В языке Ada, однако, результат такой функции обычно будет принадлежать не типу FIGURE, а "типу доступа", описывающему ссылки на экземпляры FIGURE. Так что можно будет написать эффективную функцию.
К сущностям дескрипторного типа можно применить динамическое связывание, как в следующем примере:
procedure print_balance (a: in ACCOUNT'Class) is -- Печать текущего баланса. begin Put (balance (a)); New_Line; end print_balance;Динамическое связывание следует задать явным образом. Подпрограмма объявляется как "выходящая за рамки класса" (classwide operation) заданием классификатора 'Class для типа аргумента.Это напоминает объявление в C++ любой динамически связываемой функции как "виртуальной". Только здесь клиент выбирает статическое или динамическое связывание.
Ada 95 позволяет определить "дочерний пакет" A1.B существующего пакета A. Это дает новому пакету возможность получить свойства из A и добавить свои собственные расширения и модификации. (Это понятие, конечно, близко к наследованию, но отличается от него.) Вместо объявления трех типов счетов в одном пакете, возможно, лучше было бы разделить пакет на три, где Accounts.Checking представляет CHECKING_ACCOUNT и его подпрограммы, а Accounts.Saving делает то же для SAVINGS_ACCOUNT.
Версия языка Ada 95 предусматривает добавление ОО-концепций. В ней нет понятия класса в нашем смысле слова (модуль плюс тип), но есть поддержка наследования и динамического связывания для типов записей.
Пакеты
Любой инкапсулирующий язык предлагает модульную конструкцию для группирования логически связанных программных элементов. В языке Ada она называется пакетом, модулем - в Modula-2 и Mesa, кластером - в CLU.
Класс определяется и как структурный системный компонент - модуль, и как тип. Напротив, пакет - это только модуль. Ранее отмечалось, что пакеты являются чисто синтаксическими понятиями, а классы имеют и семантическое значение. Пакеты дают способ распределения элементов системы (переменных, подпрограмм ...) в согласованные подсистемы, но они нужны только для управляемости и удобочитаемости ПО. Декомпозиция системы на пакеты не затрагивает ее семантики: можно трансформировать многопакетную систему Ada в однопакетную систему, дающую те же самые результаты, посредством чисто синтаксической операции - сняв все границы пакетов, расширяя родовые порождения (это объясняется ниже) и разрешая конфликт имен посредством переименования. Классы являются семантической конструкцией, представляя одновременно единицу модульной декомпозиции, они описывают поведение объектов во время выполнения. Благодаря наследованию семантика обогащается полиморфизмом и динамическим связыванием.
Пакет языка Ada - это свободное соединение элементов программы. Он используется для различных целей. Разумное использование этого понятия включает создание пакета, содержащего:
набор связанных констант (как в случае с наследованием возможностей);библиотеку подпрограмм, например, математическую библиотеку;набор переменных, констант и подпрограмм, описывающих реализацию одного абстрактного объекта, или фиксированное количество абстрактных объектов, доступных только через назначенные операции;реализацию абстрактного типа данных.Последнее использование наиболее интересно для данного обсуждения. Оно будет изучаться на примере пакета, описывающего стеки, взятого из руководства по языку Ada.
Простой интерфейс
Первую версию интерфейса пакета, задающего стек, можно выразить следующим образом. Заметим, что ключевое слово package (пакет) вводит интерфейс; тело, появляющееся позднее, вводится сочетанием package body (тело пакета).
package REAL_STACKS is type STACK_CONTENTS is array (POSITIVE range <>) of FLOAT; type STACK (capacity: POSITIVE) is record implementation: STACK_CONTENTS (1..capacity); count: NATURAL := 0; end record; procedure put (x: in FLOAT; s: in out STACK); procedure remove (s: in out STACK); function item (s: STACK) return FLOAT; function empty (s: STACK) return BOOLEAN; Overflow, Underflow: EXCEPTION; end REAL_STACKS;Этот интерфейс перечисляет экспортированные элементы: тип STACK - для объявления стеков, вспомогательный тип STACK_CONTENTS, используемый типом STACK, четыре открытые подпрограммы (процедуры и функции) и два исключения. Клиентские пакеты будут опираться только на интерфейс (предполагается, что создающие их программисты имеют представление о семантике, связанной с программами).
Этот пример наводит на несколько общих замечаний:
Удивительно видеть все детали представления стека в объявлениях типов STACK и STACK_CONTENTS, появившихся в том, что должно быть чистым интерфейсом. Кратко рассмотрим причину этой проблемы и способ ее устранения.В отличие от класса, пакет не определяет тип. Тип STACK следует определить отдельно. Одним из следствий этого отделения для программиста, создающего пакет вокруг реализации абстрактного типа данных, является необходимость изобретения двух различных имен - одно для пакета, другое - для типа. Другое следствие состоит в том, что подпрограммы имеют еще один аргумент по сравнению со своими ОО-аналогами: здесь все они имеют первым аргументом стек s, в то время как для класса он задается неявно (см. предыдущие лекции).Объявление может определять не только тип сущности, но и ее исходное значение. Здесь объявление count в типе STACK предписывает исходное значение 0. Оно устраняет необходимость явной операции инициализации, задаваемой процедурой создания (конструктором) класса.Однако этот способ не работает, если требуется более сложная инициализация.Для понимания объявления типа следует привести некоторые детали языка Ada: POSITIVE и NATURAL обозначают подтипы INTEGER, включающие, соответственно, положительные и неотрицательные целые, спецификация типа вида array (TYPE range <>), где <> известно как Box-символ, описывает шаблон для типов массивов. Для получения действительного типа из такого шаблона нужно выбрать конечный отрезок TYPE. Здесь это делается при определении типа STACK, использующем интервал [1..capacity] типа POSITIVE. STACK является примером параметризованного типа. Любое объявление сущности типа STACK должно задавать фактическое значение емкости стека capacity, как в: s: STACK (1000)В языке Ada каждый аргумент подпрограммы характеризуется статусом in, out или in out, определяющим права подпрограммы на использование фактических аргументов (только для чтения, только для записи, для обновления). В отсутствии явного ключевого слова состояние по умолчанию - in.Наконец, интерфейс определяет два имени исключений Overflow и Underflow. Исключение - это ситуация, когда из-за ошибок прерывается нормальный порядок вычислений. Интерфейс пакета должен перечислить любые исключения, которые могут возбуждаться в процессе работы подпрограмм пакета и передаваться для обработки клиентам. Подробно механизм исключений языка Ada описывается ниже.
Реализация
Тело пакета REAL_STACKS может объявляться следующим образом. Полностью показана только одна подпрограмма.
package body REAL_STACKS is procedure put (x: in FLOAT; s: in out REAL_STACK) is begin if s.count = s.capacity then raise Overflow end if; s.count := s.count + 1; s.implementation (count) := x; end put; procedure remove (s: in out STACK) is ... Реализация remove ... end remove; function item (s: STACK) return X is ... Реализация item ... end item; function empty (s: STACK) return BOOLEAN is ... Реализация empty ... end empty; end REAL_STACKS;Два свойства, показанные в этом примере, будут подробно обсуждаться ниже: использование исключений и необходимость повторения в теле большей части информации интерфейса (заголовков подпрограммы).
Реализация стеков
Скрытие информации поддерживается в языке Ada двухъярусным объявлением пакетов. Каждый пакет состоит из двух частей, официально известных как "спецификация" и "тело". Первый термин - слишком сильный для конструкции, не поддерживающей формального описания семантики пакета (в форме утверждений или похожих механизмов), поэтому лучше использовать скромное слово "интерфейс".
Интерфейс перечисляет общедоступные свойства пакета: экспортированные переменные, константы, типы и подпрограммы. Для подпрограмм он дает только заголовки, перечисляя формальные аргументы и их типы, и тип результата для функции, например:
function item (s: STACK) return X;Часть, содержащая тело пакета, обеспечивает реализацию подпрограмм и добавляет любые необходимые секретные элементы.
Скрытие представления: частная история
Пакет STACKS в том виде, как он задан, не реализует принцип скрытия информации. Объявления типов STACK и STACK_CONTENTS, находясь в интерфейсе, позволяют клиентам непосредственный доступ к представлению стеков. Например, клиент может включить код вида:
[1] use REAL_STACKS_1;... s: STACK; ... s.implementation (3) := 7.0; s.last := 51;грубо нарушая основную спецификацию абстрактных типов данных.
Концептуально объявления типа должны находиться в теле. Почему их туда не помещают с самого начала? Объяснение находится вне языка и требует рассмотрения проблем программного окружения.
Одно из уже упомянутых требований к языку Ada состояло в возможности независимой компиляции пакета при наличии доступа к его интерфейсу, но необязательно к его телу. Принятая технология предполагала построение сверху вниз: для продолжения работы над модулем достаточно знать спецификацию необходимых ему средств. Действительная реализация могла появиться значительно позже.
Если есть доступ к интерфейсу REAL_STACKS_1 (то есть к интерфейсу STACKS, REAL_STACKS_1 является просто его родовым порождением), можно компилировать любого из его клиентов. Такой клиент будет содержать объявления вида:
use REAL_STACKS_1;... s1, s2: STACK; ... s2 := s1;Компилятор не сможет их хорошо обрабатывать, не зная размера объекта типа STACK. Но это может определяться только из объявлений типа для STACK и вспомогательного типа STACK_CONTENTS.
Отсюда концептуальная дилемма, стоявшая перед проектировщиками языка Ada: вопросы реализации требуют помещения объявлений типа в рай - интерфейс, в то время как им место в аду - теле пакета.
Пришлось создать чистилище: специальный раздел пакета, физически видимый в интерфейсе и компилируемый с ним, но такой, что клиенты не могут обращаться к его элементам. Чистилище - это закрытая часть интерфейса, она вводится ключевым словом private. Любое объявление, появляющееся здесь, недоступно клиентам. Эта схема иллюстрируется нашей последней версией интерфейса пакета, задающего стек:
generic type G is private; package STACKS is type STACK (capacity: POSITIVE) is private; procedure put (x: in G; s: in out STACK); procedure remove (s: in out STACK); function item (s: STACK) return G; function empty (s: STACK) return BOOLEAN; Overflow, Underflow: EXCEPTION; private type STACK_VALUES is array (POSITIVE range <>) of G; type STACK (capacity: POSITIVE) is record implementation: STACK_VALUES (1..capacity); count: NATURAL := 0; end record end STACKS;Отметим, тип STACK теперь должен объявляться дважды: сначала в открытой части интерфейса, где он специфицируется как private, затем еще раз в закрытой части, где дается полное описание.
Без первого объявления строка вида s: REAL_STACK не будет разрешенной в клиенте, поскольку доступ есть только к сущностям, объявляемым в открытой части. Первое объявление, специфицируя тип как private, запрещает клиентам доступ к любым свойствам помимо универсальных операций: присваивания, проверки на равенство и использование в качестве фактических аргументов.
Заметьте, тип STACK_VALUES чисто внутренний и не нужен клиентам. Поэтому он не объявляется в открытой части интерфейса пакета.
Важно понять, что информация, помещаемая в закрытую часть интерфейса, должна была быть в теле пакета и появляется в спецификации пакета только по причинам реализации языка. С новой формой STACKS клиентский код, выше помеченный как [1], имевший прямой доступ к представлению в клиенте, становится неправильным.
Авторы клиентских модулей могут видеть внутреннюю структуру экземпляров STACK, но они не могут воспользоваться ею в своих модулях. Это могло бы приводить разработчиков к танталовым мукам. (Хорошая среда языка Ada могла бы скрывать эту часть от клиента, также как это делает инструмент short, описанный в предыдущих лекциях.) Удивительная для новичков, эта политика не противоречит правилу скрытия информации. Как отмечалось ранее, цель скрытия не в том, чтобы не дать авторам клиента возможности прочитать скрытые подробности, а чтобы не дать им использовать эти подробности.
Те, кому хотелось бы все усложнить, могли бы подвести итог в двух предложениях (произнесенных очень быстро, чтобы произвести впечатление и на друзей, и на врагов):. Закрытый раздел интерфейса пакета перечисляет реализацию тех концептуально закрытых типов, которые должны быть объявлены в интерфейсе, хотя их реализация недоступна для использования. В открытой части интерфейса эти типы объявлены закрытыми.
У15.1 Как выиграть, не используя скрытия
Проблема компиляции пакетов Ada, приведшая к появлению закрытого раздела в интерфейсе, в равной степени затрагивает и ОО-языки, если среда программирования поддерживает независимую компиляцию классов. В действительности, проблема кажется более серьезной из-за наследования: объявленная переменная типа C, может во время выполнения ссылаться на экземпляры не только типа C, но и любого класса-наследника. Поскольку любой наследник может добавить свои атрибуты, размер этих экземпляров различен. Если C - отложенный класс, невозможно даже присвоить его экземплярам размер по умолчанию. Объясните, почему, несмотря на эти замечания, ОО-нотация этой книги не нуждается в языковой конструкции, подобной механизму private языка Ada. (Подсказка: Ваши рассуждения должны рассматривать, в частности, следующие понятия: расширенные типы в сравнении со ссылочными типами, отложенные классы и технические приемы, используемые в нашем ОО-каркасе для создания спецификации абстрактных классов, не требующие от автора классов ш_ написания двух отдельных частей модуля.) Обсудите компромиссы того и другого решения. Можете ли Вы предложить другие подходы к решению проблемы каркаса языка Ada?
У15.2 Родовые параметры подпрограммы
Родовые параметры пакетов Ada могут быть не только типами, но и подпрограммами. Объясните релевантность этой возможности для реализации ОО-понятий и ее ограничения. (См. также приложение В.)
У15.3 Классы как задачи (для программистов Ada)
Перепишите класс COMPLEX как тип задачи Ada. Приведите примеры, использущие результирующий тип.
У15.4 Добавление классов к Ada
(Это упражнение предполагает хорошее знание языка Ada.) Придумайте адаптацию Ada 83, сохраняющую понятие пакета, но расширяющую записи до классов с полиморфизмом, динамическим связыванием и наследованием (единичным или множественным), в соответствии с общими принципами ОО.
У15.5 Пакеты-классы
(Это упражнение предполагает хорошее знание Ada 83.) Используя в качестве образца типы задач, придумайте адаптацию Ada 83, поддерживающую пакеты, создающие экземпляры во время выполнения, а, следовательно, играющие роль классов с полиморфизмом, динамическим связыванием и наследованием.
Универсальность
Пакет, в том виде как он появился, слишком специфичен. Он приложим к типу FLOAT, а хотелось бы задания произвольного типа. Чтобы сделать его универсальным, в языке Ada используется следующий синтаксис:
generic type G is private; package STACKS is ... Все, как и ранее, заменяя все вхождения FLOAT на G ... end STACKS;Предложение generic синтаксически более тяжелое, чем наша ОО-нотация для универсальных классов (class C [G]...), но зато в нем больше возможностей. В частности, параметры, объявляемые в generic, могут представлять не только типы, но и подпрограммы. В приложении B эти возможности обсуждаются при сравнении универсальности и наследования.
В теле пакета generic не повторяется, там достаточно конкретный тип FLOAT заменить родовым G.
Спецификация is private заставляет остальную часть пакета рассматривать G как закрытый тип. Это означает, что сущности этого типа могут использоваться только в операциях, применимых ко всем типам языка Ada: в качестве исходного или целевого объекта при присваивании, как операнд в проверке равенства, как фактический аргумент в подпрограмме, и в некоторых других специальных операциях. Это близко к соглашению для неограниченных формальных параметров универсальных классов нашей нотации. В языке Ada доступны и другие возможности. Можно ограничить операции, объявляя параметр как limited private, что запрещает все использования кроме фактических аргументов подпрограмм.
Называясь пакетом, универсально параметризованный модуль, такой как STACKS, в действительности является шаблоном пакета, поскольку клиенты не могут использовать его непосредственно; они должны получить из него действительный пакет, используя фактические родовые параметры. Новую версию нашего пакета стеков действительных величин можно определить через следующее родовое порождение:
package REAL_STACKS_1 is new STACKS (FLOAT);Родовое порождение - главный механизм языка Ada адаптации модулей. Из-за отсутствия наследования он менее гибок, поскольку можно выбирать только между универсальными модулями (параметризованными, но не используемыми непосредственно) или используемыми модулями (более не расширяемыми). Напротив, наследование допускает произвольные расширения существующих модулей, в соответствии с принципом Открыт-Закрыт. В приложении даются подробности сравнения.
Упрощение управляющей структуры
Исключения в языке Ada являются техникой исправления ошибок, не затрагивающей управляющей структуры процесса вычислений. Если рассматривать программу как выполнение ряда действий, каждое из которых может прерваться из-за сбоев, то ее структура могла бы выглядеть так:
action1; if error1 then error_handling1; else action2; if error2 then error_handling2; else action3; if error3 then error_handling3; else ...Механизм исключений в Ada предназначен для борьбы со сложностью подобной схемы - где элементы, выполняющие "полезные" задачи, выглядят как острова в океане кода, обрабатывающего ошибки программы. Ada отделяет обработку ошибок от их обнаружения. Конечно же, обработка ошибок должна включать тесты, определяющие тип возникшей ситуации. Единственным решением является в момент возникновения ошибки возбуждение определенного сигнала - исключения, обрабатывающегося далее где-то в другом месте.
Возбуждение и обработка исключений
Чтобы возбудить исключительную ситуацию, а не обрабатывать ошибки на месте, можно переписать текст следующим образом:
action1; if error1 then raise exc1; end; action2; if error2 then raise exc2; end; action3; if error3 then raise exc3; end; ...При выполнении команды raise exc нормальный порядок вычислений прерывается, и управление передается обработчику исключений (exception handler), представленному специальным блоком подпрограммы и имеющему вид:
exception when exc1, ...=> treatment1; when exc2 ...=> treatment2; ...При возбуждении исключения exc первым его обрабатывает захвативший его обработчик из динамической цепи вызовов - списка элементов, начинающегося подпрограммой, содержащей вызвавшее исключение предложение raise, и всеми вызывающими подпрограммами, как показано на рис. 15.1:
Рис. 15.1. Цепь вызовов (этот рисунок впервые появился в лекции 12 курса "Основы объектно-ориентированного программирования")
Говорят, что обработчик захватывает exc, если exc появляется в одном из его предложений when (или он содержит предложение вида when others). Такой обработчик выполняет соответствующие команды (после символа =>), после чего управление передается вызывающей программе или заканчивается в случае главной программы. (Ada имеет понятие главной программы.) Если никакой обработчик в динамической цепи не обрабатывает exc, выполнение приложения заканчивается, и управление возвращается к операционной системе, а она, вероятно, выведет системное сообщение об ошибке.
Вперед к ОО-языку Ada
Язык Ada 95 кажется сложным. Но это не значит, что сама идея создания ОО-языка Ada обречена. Просто следует ставить реальные цели и постоянно заботиться о простоте и состоятельности. Сообщество Ada может снова попытаться разработать ОО-расширение, сопровождающееся удалением некоторых возможностей. Возможны два общие направления:
Первая идея, близкая по духу к замыслу Ada 95, состоит в сохранении пакетной структуры и введении понятия класса, обобщающего типы записей Ada, с поддержкой наследования и динамического связывания. Но это должны быть действительные классы, включающие применимые подпрограммы. Такое расширение, в принципе, подобно расширению, ведущему от C к C++. Оно должно стремиться к минимализму, пытаясь применять как можно шире уже существующие механизмы (такие как with и use для пакетов), не вводя новых возможностей, приводящих потом к проблемам взаимодействия, упоминаемых Ичбиа.Другой подход может строиться на замечании, сделанном при представлении задач в данной лекции. Отмечалось, что типы задач близки по духу к классам, поскольку они могут иметь экземпляры, созданные во время выполнения. Структурно они обладают многими свойствами пакетов. Можно было бы ввести модуль, имеющий, грубо говоря, синтаксис пакетов и семантику классов. Можно думать о нем как о пакет-классе, или о типе задач, необязательно являющихся параллельными. Понятие "защищенного типа" может стать отправной точкой, будучи интегрировано в существующий механизм.Упражнения в конце данной лекции предлагают исследовать эти возможности.
Fortran
FORTRAN должен фактически устранить кодирование и отладку.
FORTRAN: Предварительный отчет, IBM, Ноябрь, 1954
Самый старый уцелевший язык программирования Fortran по-прежнему широко используется в сфере научных вычислений. Это может показаться удивительным для тех, кто использует такие "структурированные" языки как Pascal, но в Fortran легче достигнуть многих ОО-свойств, благодаря возможностям, которые считаются низкоуровневыми и предназначены для других целей.
Эмулирующие классы
Описанная техника будет работать в определенных пределах. Ее даже можно расширить для эмуляции наследования.
Но она неприменима к серьезным разработкам, что иллюстрируется на рис. 16.1. Каждый экземпляр любого класса должен физически содержать ссылки на все применимые к нему подпрограммы. Это приведет к существенным потерям памяти, особенно при наследовании.
Для снижения потерь заметим, что подпрограммы одинаковы для всех экземпляров класса. Поэтому для каждого класса можно ввести структуру данных периода выполнения, дескриптор класса, содержащий ссылки на подпрограммы. Его можно реализовать как связный список или массив. Требования к пространству значительно уменьшаются: вместо n*m указателей можно иметь их n+m, где n - число подпрограмм, а m - число объектов, как показано на рис. 16.2.
Рис. 16.2. Объекты C, разделяющие дескриптор класса
Это приводит к незначительным временным потерям, но экономия пространства и простота стоят этого.
В этой технике нет секрета. Именно она сделала С полезным в качестве средства реализации для компиляторов ОО-языков, начиная с Objective-C и C++ в начале 80-х. Способность использовать указатели функций, в сочетании с идеей группирования этих указателей в дескриптор класса, разделяемый произвольным числом экземпляров, служит первым шагом к реализации ОО-техники.
Конечно, это только первый шаг, и нужно еще найти способы реализации наследования (особенно непросто множественное наследование), универсальности, исключений, утверждений и динамического связывания. Объяснения потребовали бы отдельной книги. Отметим лишь одно важное свойство, выводимое из всего, что мы видели до сих пор. Реализация динамического связывания требует доступа к типу каждого объекта во время выполнения для нахождения нужного варианта компонента f в динамически связываемом вызове x.f (...) (написанном здесь в ОО-нотации). Другими словами: в дополнение к официальным полям объекту необходимо дополнительное внутреннее поле, порождаемое компилятором и указывающее на тип объекта. Описанный подход показывает возможную реализацию такого поля - как указателя на дескриптор класса. По этой причине на рис. 16.2 для такого поля используется ярлык type.
Эмуляция объектов
Помимо инкапсуляции, для эмуляции продвинутых свойств настоящего ОО-подхода можно использовать одно из наиболее специализированных свойств языка - возможность манипуляций с указателями на функции. Этот механизм заслуживает внимания, хотя он требует аккуратного обращения и его стоит рекомендовать в первую очередь разработчикам компиляторов, а не обычным программистам.
С внешней точки зрения "каждый объект имеет доступ к операциям, применимым к нему". Возможно, это немного наивно, но не является концептуально неверным. Язык С буквально поддерживает это понятие! Экземпляр "структуры" языка С (эквивалент записи в Pascal) может содержать среди своих полей указатели на функции.
Рис. 16.1. Объект С со ссылками на функцию
Например, структурный тип REAL_STACK можно объявить так:
typedef struct { /* Экспортируемые компоненты */ void (*remove) (); void (*put) (); float (*item) (); BOOL (*empty) (); /* Закрытые компоненты (реализация) */ int count; float representation [MAXSIZE]; } REAL_STACK;
Фигурные скобки {...} ограничивают компоненты структуры; float задает вещественный тип; процедуры объявляются как функции с типом результата void; комментарии берутся в скобки /* и *?/. Важный символ *? служит для разыменования указателей. В практике программирования на С, чтобы все работало, принято добавлять достаточное количеств указателей, если это не помогает, то всегда можно попробовать добавить один или парочку символов &. Если и это не дает результата, всегда найдется кто-нибудь, кто сможет помочь. |
В структурном типе REAL_STACK два последних компонента - переменная и массив, остальные - ссылки на функции. В данном тексте комментарии предупреждают об экспортируемых и закрытых компонентах эмулируемого класса, но на уровне языка клиентам доступно все.
Каждый экземпляр типа должен инициализироваться так, чтобы поля ссылок указывали на соответствующие функции. Например, если my_stack является переменной этого типа, а C_remove - функция, реализующая выталкивание из стека, то можно присвоить полю remove объекта my_stack ссылку на эту функцию таким образом:
my_stack.remove = C_removeВ эмулируемом классе remove не имеет необходимого для нее аргумента. Для доступа к соответствующему стеку следует объявить функцию C_remove так:
C_remove (s) REAL_STACK s; { ... Реализация операции remove ... }Тогда клиент сможет применить remove к стеку my_stack:
my_stack.remove (my_stack)В общем случае, подпрограмма rout, имеющая n аргументов в эмулируемом классе, порождает функцию C_rout с n+1 аргументами. Вызов ОО-подпрограммы:
x.rout (arg1, arg2, ..., argn)эмулируется как:
x.C_rout (x, arg1, arg2, ..., argn)Модульные расширения языка Pascal
За пределами стандарта Pascal многие коммерчески доступные версии снимают ограничения на порядок объявлений и включают поддержку модульности, включая независимую компиляцию. Такие модули могут содержать константы, типы и подпрограммы. Эти языки более гибкие и сильные, чем стандартный Pascal, сохраняют имя Pascal. Они не стандартизированы, и в действительности больше напоминают инкапсулирующие языки, такие как Modula-2 или Ada, обсуждаемые в предыдущей лекции.
OO C: оценка
Обсуждение показало, что в С есть технические способы введения ОО-идей. Но это еще не значит, что программисты должны их использовать. Как и в случае с языком Fortran, эмуляция - это некоторое насилие над языком. Сила языка С - в его доступности как "структурного языка ассемблера" (последователя BCPL и PL/360, созданного Виртом), переносимого, разумно простого и эффективно интерпретируемого. Его базисные понятия далеки от ОО-проектирования.
Опасность попыток навязывания языку С ОО-идей может привести к несостоятельной конструкции, ухудшающей процесс разработки ПО и качество получаемых продуктов. Лучше использовать С для того, что он может делать хорошо: создания интерфейсов для оборудования и операционных системы, и как машинно-генерируемый целевой код. Для применения объектной технологии лучше использовать инструмент, созданный для этой цели.
ОО-программирование и язык C
Созданный в тиши кабинета язык C быстро стал известным. Большинство людей, интересующиеся и С, и объектной технологией, перешли к ОО-расширениям С, обсуждаемым в следующей лекции (C++, Objective-C, Java). Но по-прежнему интересно, как можно заставить сам С эмулировать ОО-концепции.
ОО-программирование на языке Pascal?
Язык Pascal, введенный в 1970г. Никласом Виртом, много лет являлся доминирующим языком начального обучения программированию на факультетах информатики и оказал большое влияние на построение последующих языков. Pascal - это функциональный язык в только что определенном смысле.
ОО-расширения языка Pascal
Некоторые компании предложили ОО-расширения языка Pascal, широко известные как "Object Pascal". Две версии особенно значимы:
версия Apple, происходящая от языка, первоначально называвшегося Clascal и используемого для компьютера Macintosh и его предшественника, - Lisa;версия Borland Pascal, адаптированная в среде Borland Delphi.Наше обсуждение не затрагивает эти языки, так как реально их связь с Pascal проявляется только в имени, стиле синтаксиса и статически типизированном подходе. В частности, Borland Pascal - это ОО-язык с обработкой исключений. Он не поддерживает механизмов универсальности, утверждений, сборки мусора и множественного наследования.
Основные положения
Дисциплинарный подход применим к языку С, как и к любому другому языку. За его пределами для реализации модульности можно использовать понятие файла. Файл - это понятие языка С, балансирующее на границе между языком и операционной системой. Файл - единица компиляции, он может содержать функции и данные. Некоторые функции могут быть скрытыми от других файлов, другие - общедоступны. Это прямой путь к инкапсуляции: файл может содержать все элементы, относящиеся к реализации одного или более абстрактных объектов, или абстрактного типа данных. Благодаря понятию файла, С достигает уровня инкапсулирующего языка, как Ada или Modula-2. В сравнении с Ada здесь нет универсальности и отличия между интерфейсом и и реализацией.
Обычная техника программирования на С не расположена к ОО-принципам. Большинство программ С используют "файлы заголовков", описывающих разделяемые данные. Любой файл, нуждающийся в данных, получает доступ к ним через директиву "include" (управляемую встроенным препроцессором С):
#include <header.h>где header.h - это имя файла заголовка (h - обычный суффикс для таких имен файлов). Это эквивалентно копированию файла заголовка в точке появления директивы. В результате, традиция С, если не сам язык, дает возможность модулям клиента получить доступ к структурам данных через их физические представления, что явно противоречит принципам скрытия информации и абстракции данных. Однако возможно использовать файлы заголовка более дисциплинированным путем, скорее насаждая, а не нарушая абстракцию данных. Они могут даже помочь продвинуться к определению модулей интерфейса в стиле, обсуждаемом для языка Ada в предыдущей лекции.
Собственно Pascal
Многое ли из ОО-подхода можно реализовать в Pascal?
К сожалению, немногое. Структура программы в Pascal основана на совершенно другой парадигме. Программа на языке Pascal состоит из последовательности описательных разделов, следующих в неизменном порядке: метки, константы, типы, переменные, подпрограммы (процедуры и функции) и раздела выполняемых инструкции. Сами подпрограммы рекурсивно имеют ту же структуру.
Это простое правило облегчает однопроходную компиляцию. Но любая попытка использования ОО-техники обречена. Рассмотрим, что нужно для реализации ADT, например, стека, представленного массивом: несколько констант, задающих размер массива, тип, описывающий элементы стека, несколько переменных, таких как указатель на вершину стека и несколько подпрограмм, задающих операции АТД. В Pascal эти элементы будут разбросаны по разделам: все константы вместе, все типы вместе и т. д.
Результирующая программная структура противоположна ОО-проектированию. Использование Pascal противоречит принципу Лингвистических Модульных Единиц: любая политика модульности должна поддерживаться доступными конструкциями языка.
Итак, если рассматривать официальный стандарт Pascal, то сделать можно немногое, не считая использования дисциплинарного подхода.
Техника COMMON
Fortran система состоит из главной программы и ряда подпрограмм. Как обеспечить схожесть с абстракцией данных?
Возможная техника состоит в том, чтобы представить данные в так называемом общем блоке COMMON, а экспортируемые компоненты (например, put и т. д. для стеков) реализовать в виде независимых подпрограмм. Блок COMMON - это механизм Fortran, предоставляющий доступ к данным любой подпрограмме, желающей их получить. Вот набросок подпрограммы put для стека действительных чисел:
SUBROUTINE RPUT (X) REAL X C C ВТАЛКИВАНИЕ X НА ВЕРШИНУ СТЕКА C COMMON /STREP/ TOP, STACK (2000) INTEGER TOP REAL STACK C TOP = TOP + 1 STACK (TOP) = X RETURN ENDЭта версия не управляет переполнением (будет исправлено в следующей версии). Функция, возвращающая элемент вершины:
INTEGER FUNCTION RITEM C C ВОЗВРАЩЕНИЕ ВЕРШИНЫ СТЕКА C COMMON /STREP/ TOP, STACK (2000) INTEGER TOP REAL STACK RITEM = STACK (TOP) RETURN ENDЗдесь также необходимо было бы проверять стек на пустоту. Подпрограммы REMOVE и другие строятся по тому же образцу. Имя общего блока - STREP - объединяет различные подпрограммы, дающие доступ к одним и тем же данным.
Ограничения очевидны: данная реализация описывает один абстрактный объект (один отдельный стек), а не абстрактный тип данных, из которого во время выполнения можно создать множество экземпляров. Мир Fortran статичен: необходимо указывать размеры всех массивов (в примере 2000 - произвольно выбранное число). Поскольку отсутствует универсальность, то в принципе, придется объявлять новый набор подпрограмм для каждого типа элементов стека. Отсюда имена RPUT и RITEM, где R означает Real. Можно справиться с этими проблемами, но не без значительных усилий.
Техника подпрограммы с множественным входом
Техника, основанная на блоке COMMON, как это видно, нарушает Принцип Лингвистических Модульных Единиц. В модульной структуре системы подпрограммы, являясь концептуально связаннами, физически независимы.
Эту ситуацию можно улучшить (не убирая другие перечисленные ограничения) посредством особенности языка, легализованной в Fortran 77 - множественными точками входа в одной подпрограмме.
Это расширение, введенное, возможно, для других целей, можно использовать во благо ОО-подхода. Клиентские подпрограммы могут вызывать точки входа, как если бы они были автономными подпрограммами, и разные входы могут иметь разные аргументы. Вызов входа начинает выполнение подпрограммы с этой точки. Все входы разделяют хранимые данные подпрограммы, появляющиеся с директивой SAVE и сохраняемые от одной активизации подпрограммы к другой. Понятно, куда мы клоним: эту технику можно использовать для определения модуля, инкапсулирующего абстрактный объект, почти как в инкапсулирующем языке. Здесь модуль моделируется подпрограммой, структура данных - набором объявлений с директивой SAVE, и каждый компонент соответствующего класса в ОО-языке - входом, заканчивающимся инструкцией RETURN:
ENTRY (arguments) ... Инструкции ... RETURNВ отличие от предыдущего решения, основанного на COMMON, теперь все необходимое сосредоточено в единой синтаксической единице. В таблице 34.1 дан пример реализации стека действительных величин. Вызовы клиента выглядят следующим образом:
LOGICAL OK REAL X C OK = MAKE () OK = PUT (4.5) OK = PUT (-7.88) X = ITEM () OK = REMOVE () IF (EMPTY ()) A = BВзглянув на этот текст, можно почти поверить, что это использование класса, или, по крайней мере, объекта, через его абстрактный, официально определенный интерфейс!
Подпрограмма в Fortran и ее точки входа должны быть все либо подпрограммами, либо функциями. Здесь, поскольку EMPTY и ITEM должны быть функциями, все другие входы должны тоже объявляться как функции, включающие MAKE, чей результат бесполезен. |
C --РЕАЛИЗАЦИЯ C --АБСТРАКТНЫЙ СТЕК ЧИСЕЛ C INTEGER FUNCTION RSTACK () PARAMETER (SIZE=1000) C C --ПРЕДСТАВЛЕНИЕ C REAL IMPL (SIZE) INTEGER LAST SAVE IMPL, LAST C C --ВХОД С ОБЪЯВЛЕНИЯМИ C LOGICAL MAKE LOGICAL PUT LOGICAL REMOVE REAL ITEM LOGICAL EMPTY C REAL X C C -- СОЗДАНИЕ СТЕКА C ENTRY MAKE () MAKE = .TRUE. LAST = 0 RETURN C C -- ДОБАВЛЕНИЕ ЭЛЕМЕНТА C ENTRY PUT (X) IF (LAST .LT. SIZE) THEN PUT = .TRUE. LAST = LAST + 1 IMPL (LAST) = X ELSE PUT = .FALSE. END IF RETURN | C --УДАЛЕНИЕ ВЕРШИНЫ C ENTRY REMOVE (X) IF (LAST .NE. 0) THEN REMOVE = .TRUE. LAST = LAST - 1 ELSE REMOVE = .FALSE. END IF RETURN C C --ЭЛЕМЕНТ ВЕРШИНЫ C ENTRY ITEM () IF (LAST .NE. 0) THEN ITEM = IMPL (LAST) ELSE CALL ERROR * ('ITEM: EMPTY STACK') END IF RETURN C C -- ПУСТ ЛИ СТЕК? C ENTRY EMPTY () EMPTY = (LAST .EQ. 0) RETURN C END |
Этот стиль программирования может успешно применяться для эмуляции инкапсуляции Ada или Modula-2 в контекстах, где нет другого выбора, кроме использования Fortran. Конечно, он страдает от жестких ограничений:
Не разрешаются никакие внутренние вызовы: в то время как подпрограммы в ОО-классе обычно опираются друг на друга для реализации, вызов входа той же подпрограммы будет понят как рекурсия - проклятие для Fortran - и бедствие во время выполнения во многих реализациях.Как отмечалось, этот механизм строго статичен, поддерживая только один абстрактный объект. Он может быть обобщен преобразованием каждой переменной в одномерный массив. Но не существует переносимой поддержки для создания динамического объекта.На практике некоторые среды Fortran не слишком хорошо работают с множественными входами подпрограмм.Наконец, сама идея использования языкового механизма для целей, отличных от проектируемых, порождает опасность путаницы.У16.1 Графические объекты (для программистов на Fortran)
Напишите на Fortran набор подпрограмм с множественными входами, реализующих основные графические объекты (точки, окружности, многоугольники). Для спецификации имеющихся абстракций и соответствующих операций можно опираться на графический стандарт GKS.
У16.2 Универсальность (для программистов на C)
Как бы Вы преобразовали на С эмуляцию класса "real stack" в эмуляцию с родовыми параметрами, адаптируемую к стекам любого типа G, а не просто float?
У16.3 ОО-программирование на C (семестровый проект)
Постройте и реализуйте простое ОО-расширение С, используя идеи этой лекции. Вы можете написать либо препроцессор, переводя расширенную версию языка на С, либо функциональный пакет, не изменяющий самого языка.
Подойдите к задаче через три последовательные уточнения:
сначала реализуйте механизм, позволяя объектам содержать их собственные ссылки на имеющиеся подпрограммы;затем посмотрите, как факторизовать ссылки на уровне класса;наконец, изучите, как добавить механизм единичного наследования.Уровни языковой поддержки
Оценивая возможности поддержки ОО-концепций языками программирования, можно разделить их на три широкие категории (игнорируя самый низкий уровень, в основном, ассемблерных языков, не поддерживающих даже понятия подпрограммы):
К функциональному уровню отнесем языки, где единицей декомпозиции является подпрограмма, функциональная абстракция, описывающая шаг обработки. Абстракция данных, если она есть, обрабатывается через определения структур данных, либо локальных для подпрограммы, либо глобальных.Языки инкапсулирующего уровня позволяют группировать подпрограммы и данные в синтаксической единице, называемой модулем или пакетом. Обычно такие единицы допускают независимую компиляцию. Довольно подробно это обсуждалось при рассмотрении языка Ada.На третьем уровне идут ОО-языки. Здесь не место обсуждать, что дает право языку на такое звание. Это вопрос детально рассмотрен в лекции 2 курса "Основы объектно-ориентированного программирования", здесь же отметим необходимость поддержки классов, наследования, полиморфизма и динамического связывания.
Для категории инкапсулирующих языков, поддерживающих механизм абстракции данных, но не поддерживающих классы, наследование, полиморфизм и динамическое связывание, в литературе используется термин основанный на объекте, введенный в статье Питера Вегнера. Поскольку слова основанный и ориентированный близки и не отражают концептуальной разницы между языками, довольно трудно объяснить, особенно новичкам, суть термина "основанный на объекте". Поэтому я решил придерживаться выражений "инкапсулирующие языки" и "объектно-ориентированные языки". |
Еще немного о терминологии. Термин "функциональные языки" двусмысленен, поскольку в литературе он применяется к классу языков, основанных на математических принципах и часто прямо или косвенно происходящих от Lisp, а этот язык использует функции, свободные от побочных эффектов, вместо императивных конструкций, таких как процедуры и присваивания.
Во избежание путаницы в данной книге для обозначения этого стиля программирования всегда используется термин "аппликативный". В нашем толковании "функционального языка" слово функция противопоставляется не процедуре, а объекту. Путаница еще более усугубляется употреблением термина "процедурный язык" как синоним "не объектно-ориентированный"! Для такой терминологии нет оснований - для нас "процедурный" является синонимом "императивный", в противоположность термину "аппликативный". Все обычные ОО-языки, включающие нотацию этой книги, явно процедурные.
Общее замечание по ОО-эмуляции. В своей основе объектная технология - это "программирование с абстрактными типами данных". Даже на функциональном уровне можно применять рудиментарную форму этой идеи, определив набор строгих методологических правил, требующих вызова подпрограмм для доступа к данным. Предполагается, что начинать надо с ОО-построения, определяющего АТД и его компоненты. Затем пишется набор подпрограмм, представляющих эти компоненты - put, remove, item, empty, как в нашем стандартном примере стека. Далее требуется, чтобы все клиентские модули использовали только эти подпрограммы. При отсутствии языковой поддержки, но при условии, что все в команде подчиняются навязанным правилам, это можно рассматривать как начало объектного подхода. Назовем эту технику дисциплинарным подходом.
C++
Язык C++ создан примерно в 1986 г. Бьерном Страуструпом в AT&T Bell Laboratories (организации, известной помимо других достижений разработкой Unix и C). Он быстро развивался и занял лидирующую позицию в промышленных разработках, стремившихся получить преимущества объектной технологии при сохранении совместимости с языком С. Язык остался почти полностью снизу вверх совместимым (корректная программа на С является в нормальных обстоятельствах корректной программой на C++).
Первые реализации C++ были простыми препроцессорами, преобразующими ОО-конструкции в обычный С, основываясь на технике, описанной в предыдущей лекции. Современные компиляторы, однако, являются "родными" реализациями C++. Теперь трудно найти компилятор С, становящийся одновременно компилятором C++ при включении специального параметра компиляции "C++ конструкции". Это один из показателей успеха. Компиляторы C++ доступны практически для большинства платформ.
Первоначально C++ представлял улучшенную версию C благодаря конструкции класса и строгой формы типизации. Вот пример класса:
class POINT { float xx, yy; public: void translate (float, float); void rotate (float); float x (); float y (); friend void p_translate (POINT*, float, float); friend void p_rotate (POINT*, float); friend float p_x (POINT*); friend float p_y (POINT*); };Первые четыре подпрограммы задают привычный ОО-интерфейс класса. Как показывает этот пример, объявление класса содержит только заголовки подпрограмм, а не их реализации, определяемые отдельно. В связи с этим возникают вопросы области действия объявлений, важные и для компиляторов, и для читателей.
Другие четыре подпрограммы - это примеры "дружественных" подпрограмм. Это понятие характерно для C++ и дает возможность вызова подпрограмм C++ из нормального кода С. Дружественные подпрограммы нуждаются в дополнительном аргументе, задающем объект, к которому применяется операции. Здесь этот аргумент имеет тип POINT*, означающий указатель на POINT.
C++ предлагает широкий набор мощных механизмов:
Скрытие информации, включая способность скрывать компоненты от собственных наследников.Поддержка наследования. Первоначальные версии поддерживали только единичное наследование, но теперь язык включает множественное наследование. Дублируемое наследование не обладает покомпонентной гибкостью. (В лекции, посвященной множественному наследованию, отмечалась важность этого свойства.) Вместо этого, разделяется или дублируется весь набор методов дублируемых предков.По умолчанию предлагается статическое связывание, для динамического связывания функция должна быть определена как виртуальная. Подход C++ к этому вопросу подробно обсуждался.Понятие "чистой виртуальной функции" напоминает отложенные методы.Введена более строгая типизация, чем в языке С, но все же разрешающая преобразования типа (кастинг).Сборка мусора обычно отсутствует (из-за приведений типа и использования указателей для массивов и подобных структур), хотя доступны некоторые инструменты для надлежаще ограниченных программ.Из-за отсутствия автоматического управления памятью введено понятие деструктора для явного удаления объектов (понятие, дополняющее понятие конструктора класса - процедуры создания).Обработка исключений не входила в первоначальное определение, но теперь поддерживается большинством компиляторов.Введена некоторая форма попытки присваивания - downcasting.Введена универсальность - "шаблоны". У них два ограничения: отсутствует ограниченная универсальность, и при конкретизации шаблона велика нагрузка на работу во время компиляции (известная в С++ как проблема).Разрешена перегрузка операторов (знаков операций).Введена инструкция assert для отладки, но отсутствуют утверждения для поддержки Проектирования по Контракту (предусловия, постусловия, инварианты классов), соединенные с ОО-конструкциями.Библиотеки, доступны от различных поставщиков, например библиотека MFC (Microsoft Foundation Classes).
C++: оценка
Язык C++ мало кого оставляет безразличным. Известный автор Гради Буч называет его в интервью "Geek Chic", "языком моего предпочтения". Зато, по словам Дональда Кнута, Эдсгера Дейкстру "сама мысль о программировании на С++ сделала бы больным".
В данном случае для C++ подходит ответ Юнии, данный Нерону в "Британике" Ж. Расина:
За что такая честь? За что такой позор? (пер. Э. .Л. .Линецкой)Разочарование C++ является следствием преувеличенных надежд. Предыдущие обсуждения в данной книге тщательно анализировали некоторые наиболее противоречивые концепции языка - особенно в области типизации, управления памятью, соглашений по наследованию и динамического связывания - и показали возможность лучших решений. Но никто не может критиковать C++, полагая, что это первый и единственный ОО-язык. Язык C++ появился в нужное время и в нужном месте, ему удалось вне всяких сомнений поймать тот особенный момент в истории ПО, когда многие профессионалы и менеджеры были готовы использовать объектную технологию, но не были готовы отбросить существующую практику. C++ был почти магическим ответом: языка С достаточно, чтобы еще не напугать менеджеров, ОО уже достаточно, чтобы привлечь передовых специалистов. Уловив это обстоятельство, С++ просто следовал примеру С, который пятнадцать лет назад тоже был продуктом совпадающих возможностей - необходимости переносимого машинно-ориентированного языка, разра ботки Unix, появления персональных компьютеров, и наличия нескольких списанных машин в лаборатории Bell. Заслуга С++ в том, что он способствовал историческому подъему в развитии объектной технологии, представив ее всему сообществу, возможно не принявшему бы эти идеи в менее общепринятом облачении.
Тот факт, что C++ не является идеальным ОО-языком, о чем часто говорят авторы и лекторы и что ясно всем, изучавшим эти концепции, не должен умалять его заслугу. Не следует смотреть на C++, как будто бы ему суждено остаться основным инструментом программистского сообщества и в 21 веке, поскольку тогда он переживет себя. Тем временем С++ восхитительно играет свою роль - роль переходной технологии.
Доступность
Simula часто представляется как респектабельный, но более не существующий предок. В действительности он еще жив и используется небольшим, но восторженным сообществом. Определение языка поддерживается Группой Стандартов Simula (Simula Standards Group). Существуют компиляторы и ПО от нескольких компаний, в основном скандинавских.
Другие языки
Oberon: [Wirth 1992], [Oberon-Web]. Modula-3: [Harbison 1992], [Modula-3-Web]. Sather: [Sather-Web]. Beta: [Madsen 1993], [Beta-Web]. Self: [Chambers 1991], [Ungar 1992].
Другие ОО-языки
До сих пор описывались широко известные языки, но не только они привлекали внимание. Отметим еще несколько ОО-языков, каждый из которых заслуживает отдельной лекции. Ссылки на эти языки можно найти в разделе библиографии.
Oberon - это ОО-последователь Modula-2, созданный Виртом, является частью проекта, включающего среду программирования и поддержку оборудования.Modula-3, созданный в исследовательской лаборатории Digital Equipment (DEC Research), является модульным языком с типами, похожими на класс, также основанный на Modula-2.Trellis, тоже созданный в лаборатории DEC Research, был среди первых языков, предлагающих универсальность и множественное наследование.Sather, частично возникший из концепций первого издания этой книги, в частности, широко использует утверждения. Его версия pSather дает интересный механизм параллелизма.Beta - это прямой потомок Simula, созданный в Скандинавии при сотрудничестве с Нигардом (одним из первых авторов Simula). Он вводит конструкцию pattern для унификации понятий класса, процедуры, функции, типа и сопрограммы.Self основан не на классах, а на "прототипах", поддерживая наследование как отношение между объектами, а не типами.Ada 95 обсуждался в лекции, посвященной Ada.Borland Pascal и другие ОО-расширения Pascal упоминались при обсуждении Pascal.Java
Созданный в корпорации Sun Microsystems, язык Java привлек к себе большое внимание уже в первые месяцы своего появления в начале 1996 г. как способ, помогающий приручить Интернет. Журнал ComputerWorld отмечал, что количество упониманий о Java в прессе в первой половине 1996 г. составляло 4325 (что можно увеличить в 2-3 раза, поскольку имелась в виду только американская пресса). Кстати, для сравнения, Билл Гейтс упоминался только 5076 раз.
Основной вклад Java связан с технологией реализации. Сама идея уже существовала во многих других средах, но теперь была реализована на новом уровне. Текст Java-программ транслируется в специальный байт-код - низкоуровневый переносимый интерпретируемый формат. Спецификация этого кода общедоступна и хранится в Интернете. Для большинства платформ разработана виртуальная Java-машина, выполняющая интерпретацию программы на байт-коде. Виртуальная машина - это просто программа с доступными версиями для многих различных платформ, свободно загружаемая через Интернет. Это дает возможность почти всем выполнять программы на байт-коде, написанные почти кем угодно. Виртуальная машина поддерживается сетевыми браузерами, распознающими ссылки на программы на байт-коде. Например, ссылки, встроенные в Web-страницу, автоматически загружают программу и тут же выполняют ее.
Взрывной интерес к Интернету дал этой технологии огромный толчок, и корпорация Sun смогла убедить многих других основных игроков производить основанные на ней инструменты. Поскольку байт-код отделен от языка Java, он имеет хорошие шансы стать выходным языком компиляторов независимо от исходного языка. Создатели компиляторов для ОО-расширений Pascal и Ada, а также нотации этой книги сразу увидели возможность разработки ПО, способного работать без всяких изменений, и даже без необходимости перекомпиляции, на различных промышленных платформах.
Java - одна из инновационных разработок, дающая много причин восхищаться ею. Но язык Java не является и вряд ли станет основным языком разработки.
Будучи ОО- расширением С, язык не учитывает уроки, уже усвоенные с 1985 г. сообществом С++. Как в самой первой версии С++, в нем нет универсальности, и поддерживается только единичное наследование. Исправление этих упущений в С++ было долгим и болезненным процессом, годами создававшим неразбериху, поскольку компиляторы никогда полностью не поддерживали один и тот же язык, книги никогда не давали точной информации, преподаватели никогда не давали один и тот же материал, и программисты никогда не знали, что обо всем этом думать.
Как и все в мире C++, Java не стоит на месте. Этот язык имеет одно значительное преимущество перед С++: убрав понятие произвольного указателя, особенно для описания массивов, он, наконец, стал поддерживать сборку мусора. В остальном, он, кажется, не обращает внимания на современные идеи программной инженерии: нет поддержки утверждений (более того, Java дошел до устранения скромной инструкции assert С и С++), он лишь частично полагается на проверку типов во время выполнения, сбивает с толку модульная структура с тремя взаимодействующими понятиями (классы, вложенные пакеты, исходные файлы). Затемнен синтаксис, унаследованный от С. В качестве примера приведем несколько строк, взятых из книги по языку, написанной его разработчиками:
String [ ] labels = (depth == 0 ? basic : extended); while ((name = getNextPlayer()) != null) {В них вы видите функции, создающие побочные эффекты: использование присваивания =, конфликтующее с традицией математики, точка с запятой, иногда необходимая, иногда неправомерная, и т. д.
Однако тот факт, что в языке есть невдохновляющие свойства, не умаляет вклада технологии Java в разработку переносимого ПО. Если Java сумеет решить текущие проблемы эффективности, он может стать лучшим приближением к старой мечте компьютерной промышленности - действительно универсальной машине. И эта машина будет основываться на ПО, а не на оборудовании, хотя поговаривают и о "чипах Java".
Языковой стиль
Язык Smalltalk сочетает в себе идеи Simula и свободный, бестиповый стиль языка Lisp. Статическая проверка типов не производится, что противоречит подходу, предлагаемому в данной книге. Основное внимание в языке и окружении уделяется динамическому связыванию. Решение о том, можно ли применить подпрограмму к объекту, происходит во время выполнения.
У Smalltalk своя терминология. Подпрограмма называется "методом", применение подпрограммы к объекту называется "посланием сообщения" объекту (чей класс должен находить соответствующий метод для обработки сообщения).
Другой важной чертой отличия стиля Smalltalk от изучаемого в этой книге является отсутствие ясного различия между классами и объектами. В системе Smalltalk все - объекты, включая и сами классы. Класс рассматривается как объект класса более высокого уровня, называемого метаклассом. Это позволяет иерархии классов включать все элементы системы, в корне иерархии находится класс самого высокого уровня, называемый object. Корень поддерева, содержащий только классы, - это метакласс class. Аргументация для этого подхода такова:
Согласованность: все в Smalltalk идет от единого понятия, объекта.Эффективность окружения: классы становятся частью контекста во время выполнения, это облегчает разработку символических отладчиков, браузеров и других инструментов, нуждающихся в доступе к текстам классов во время выполнения.Методы класса: можно определить статические методы, применяемые к классу, а не к его экземплярам. Методы класса можно использовать для реализации таких стандартных операций, как new, размещающих экземпляры класса.Наше обсуждение в предыдущих лекциях рассматривало аргументацию в пользу других, статичных подходов, показывая другие способы получения тех же результатов.
Концепции сопрограмм
Наряду с базисными ОО-механизмами язык Simula предлагает интересное понятие - сопрограмма.
Понятие сопрограммы рассматривалось при обсуждении параллелизма. Дадим краткое напоминание. Сопрограммы моделируют параллельные процессы, существующие в операционных системах или системах реального времени. У процесса больше концептуальной свободы, чем у подпрограммы. Например, драйвер принтера полностью ответственен за то, что происходит с принтером, им управляемым. Он не только ответственен за абстрактный объект, но и имеет собственный алгоритм жизненного цикла, часто концептуально бесконечный. Форма процесса принтера может быть приблизительно такой:
from some_initialization loop forever "Получить файл для печати"; "Напечатать его" endВ последовательном программировании связь между единицами программы асимметрична: когда один программный блок вызывает другой, то последний выполняется, после чего возвращает управление вызывающему блоку в точке вызова. Процессы равноправны: каждый процесс выполняется сам по себе, прерываясь временами для предоставления информации другому процессу или ожидая ее получения.
Сопрограммы спроектированы подобным же образом, но для выполнения в одном потоке управления. (Последовательная эмуляция параллельного выполнения называется квази-параллелизмом.) Сопрограмма прерывает свое собственное выполнение и предлагает продолжить выполнение (resume) другой сопрограмме в ее последней точке прерывания; прерванная сопрограмма позже может продолжиться сама.
Рис. 17.1. Последовательное выполнение сопрограмм
Сопрограммы особенно полезны, когда каждая из нескольких связанных деятельностей имеет собственную логику. Каждая из них может быть задана последовательным процессом, и отношение "хозяин-слуга", характерное для обычных подпрограмм, является неадекватным. Типичным примером является преобразование входных данных в выходные, где на структуру входных и выходных файлов накладываются различные ограничения. Такой случай будет обсуждаться ниже.
Simula представляет сопрограммы как экземпляры классов. Это уместно, поскольку сопрограммы почти всегда нуждаются в длительно хранимых данных, и с ними ассоциируется абстрактный объект. Как отмечалось выше, класс в Simula может иметь тело. В классе, представляющем абстракцию пассивных данных, оно будет служить только для инициализации экземпляров классов (эквивалент нашей процедуры создания). В сопрограмме оно будет описанием процесса. Тело сопрограммы - это обычно цикл вида
while continuation_condition do begin ... Действия ...; resume other_coroutine; ...Действия ... endДля некоторых сопрограмм условием continuation_condition часто является True, что эквивалентно бесконечному процессу (несмотря на то, что хотя бы одна сопрограмма должна завершиться).
Система, основанная на сопрограммах, обычно имеет основную программу, сначала создающую ряд объектов - сопрограмм, а затем продолжает одну из них:
corout1 :- new C1; corout2 :- new C2; ... resume coroutiКаждое выражение new создает объект и приступает к выполнению его тела. Но квазипараллельная природа сопрограмм (в отличие от истинного параллелизма процессов) поднимает проблему инициализации. Для процессов каждое new порождает новый процесс, запускает его, возвращая тут же управление исходному процессу. Но здесь только одна сопрограмма может быть активной. Если выражение new запустило основной алгоритм сопрограммы, то исходный процесс не получит вновь управление - у него не будет возможности создать C2 после порождения C1.
Simula решает эту проблему посредством инструкции detach. Сопрограмма может выполнить detach, возвращая управление блоку, создавшему его посредством new. Тела сопрограмм почти всегда начинаются с detach (если необходимо, после инструкции инициализации), а дальше обычно следует цикл. После выполнения своего detach сопрограмма приостановится до тех пор, пока главная, или другая, сопрограмма не продолжит ее выполнение.
Моделирование
Верный своему прошлому язык Simula содержит набор примитивов для моделирования дискретных событий. Конечно, неслучайно, что первый ОО-язык создавался для моделирования внешнего мира. Сила объектного подхода проявляется особенно ярко именно в этой области.
Моделирующее ПО анализирует и предсказывает поведение некоторой внешней системы: линии сборки, химической реакции, компьютерной операционной системы.
Особенностью моделирования дискретных событий является то, что внешняя система представлена своими состояниями, способными изменяться в ответ на события, происходящие в дискретные моменты времени. При непрерывном моделировании жизнь системы рассматривается как непрерывный процесс, как непрерывно развивающееся состояние. Какой из подходов является лучшим для данной внешней системы, зависит не столько от природы системы - непрерывной или дискретной (часто такая постановка бессмысленна), сколько от моделей, для нее создаваемых.
Еще одним конкурентом моделирования дискретных событий является аналитическое моделирование, где строится математическая модель внешней системы, а затем решаются соответствующие уравнения. При моделировании дискретных событий для предсказания поведения системы на сколько-нибудь значимом периоде времени приходится увеличивать время моделирования и время работы программной системы. Аналитические методы позволяют получать решение на любой заданный момент времени и, следовательно, более эффективны. Однако, как правило, физические системы слишком сложны, чтобы можно было построить реалистичную математическую модель, допускающую аналитическое решение. Тогда моделирование остается единственной возможностью.
Многие внешние системы естественно укладываются в схему моделирования дискретных событий. Примером может служить линия сборки, где типичные события могут включать появление на линии новых деталей, рабочих или машин, выполняющих определенную операцию над деталями, снятие с линии готового продукта, сбой, приводящий к остановке. Моделирование можно использовать для нахождения ответов на вопросы о моделируемых физических системах.
Сколько времени ( в среднем, минимально, максимально, среднее отклонение) потребуется для производства конечного продукта? Как долго данный механизм остается неиспользованным? Каков оптимальный уровень запасов?
Исходные данные для моделирования - это последовательность событий и частота их появлений. Данные можно получить с помощью измерений, производимых на внешней системе. Зачастую их получают в соответствии с заданными статистическими законами, генерируя соответствующие последовательности случайных чисел.
При моделировании вводится понятие модельного времени. Его протекание определяет моменты совершения определенных событий, продолжительность выполнения определенной операции на определенной детали. Модельное время не следует путать со временем вычисления. Для моделирующей системы модельное время - это переменная, значение которой в программе дискретно изменяется. Доступный в языке Simula запрос time позволяет в период выполнения системы получать текущее время и управлять временными событиями.
Компонент time и другие компоненты моделирования содержатся в библиотечном классе SIMULATION, он может использоваться как предок любого класса. Будем называть "классом моделирования" любой класс, являющийся потомком SIMULATION.
В Simula наследование можно применять к блокам: блок, написанный в форме: C begin... end имеет доступ ко всем компонентам, объявленным в классе C. Класс SIMULATION часто используется таким образом как родитель всей программы, а не просто класса. Поэтому можно говорить о "моделирующей программе". |
Процесс может находиться в одном из четырех состояний:
активный - выполняемый в данный момент;приостановленный - ждущий продолжения;бездействующий - холостой, или не являющийся частью системы;завершенный.Любое моделирование ( то есть любой экземпляр потомка SIMULATION) поддерживает список событий (event list), содержащий уведомления о событиях (event notices). Каждое уведомление - это пара <process, activation_time>, где activation_time означает время активизации процесса process. (Здесь и далее любое упоминание о времени, так же как слова "когда" или "в настоящее время", относится к модельному времени - времени внешней системы, доступному через time.) Список событий сортируется по возрастанию activation_time; первый процесс активный, все остальные приостановлены. Незавершенные процессы, которых нет в списке, являются бездействующими.
Рис. 17.2. Список событий
Основная операция над процессами - активизация, она планирует активизацию процесса в определенное время, помещая уведомление о событии в список событий. Видимо по синтаксическим причинам эта операция не является вызовом процедуры класса SIMULATION, а специальной инструкцией, использующей ключевое слово activate или reactivate. (Вызов процедуры был бы более согласованным подходом, тем более что фактически стандарт определяет семантику activate в процедурных терминах.) Основная форма инструкции такова:
activate some_process scheduling_clauseгде some_process - непустая сущность типа PROCESS. Необязательный параметр scheduling_clause задается одной из следующих форм:
at some_time delay some_period before another_process after another_processПервые две формы указывают на позицию нового уведомления о событии, задавая время его активизации, вычисляемое как max (time, some_time) для формы at и max (time, time + some_period) в форме delay. Новое уведомление о событии будет внесено в список событий после любого другого события, уже находящегося в перечне с меньшим или таким же временем активизации, если оно не помечено prior.
Последние две формы определяют позицию по отношению к другому процессу в перечне. Отсутствие scheduling_clause эквивалентно delay 0.
Процесс может активизировать себя в более позднее время, указав себя как целевой процесс - some_process. В этом случае ключевое слово должно быть reactivate. Это полезно при запуске задачи внешней системы, требующей на свое выполнение некоторого модельного времени. Если запускается задача, решение которой занимает 3 минуты (180 сек.), то для соответствующего исполнителя - процесса worker - можно задать инструкцию:
reactivate worker delay 180Эта ситуация настолько типична, что для нее введен специальный синтаксис, позволяющий избежать явного вызова самого себя:
hold (180)с точно тем же эффектом.
Вы, вероятно, уже догадались, что процессы реализуются как сопрограммы. Примитивы моделирования внутренне используют рассмотренные выше примитивы сопрограмм. Эффект hold (some_period) можно приблизительно описать (в синтаксисе, похожем на нотацию этой книги, но с расширением resume) как:
-- Вставка нового уведомления о событии в список событий в требуемую позицию: my_new_time := max (time, time + some_period) create my_reactivation_notice.make (Current, my_new_time) event_list.put (my_reactivation_notice) -- Получить первый элемент списка событий и удалить его: next := event_list.first; event_list.remove_first -- Активизировать выбранный процесс, изменяя время при необходимости: time := time.max (next.when); resume next.whatпредполагая следующие объявления:
my_new_time: REAL; my_reactivation_notice, next: EVENT_NOTICE class EVENT_NOTICE creation make feature when: REAL - т.е. время what: PROCESS make (t: REAL; p: PROCESS) is do when := t; what := p end endЕсли процесс приостанавливается, задавая время своей последующей активизации, то выполнение продолжает приостановленный процесс с наиболее ранним временем активизации. Если указанное время активизации этого процесса позже текущего времени, то соответственно изменяется (увеличивается) текущее время.
Примитивы моделирования, хотя они и основаны на примитивах сопрограмм, принадлежат к более высокому уровню абстракции, потому лучше использовать их, а не полагаться непосредственно на механизмы сопрограмм. В частности, можно рассматривать hold (0) как форму resume, благодаря которой можно не определять явным образом процесс для продолжения, а поручить его выбор механизму списка событий.
Objective-C
Созданный в корпорации Stepstone (первоначально Productivity Products International) Бредом Коксом (Brad Cox) язык Objective-C представлял ортогональное дополнение концепций Smalltalk к языку C. Это был базовый язык для рабочей станции и операционной системы NEXTSTEP. Хотя успех C++ отчасти затмил популярность этого языка, Objective-C все же сохранил активное сообщество пользователей.
Как и в Smalltalk, акцент делается на полиморфизм и динамическое связывание, но современные версии Objective-C предлагают статическую типизацию, а некоторых из них и статическое связывание. Вот пример синтаксис Objective-C:
= Proceedings: Publication {id date, place; id articles;} + new {return [[super new] initialize]} - initialize {articles = [OrderedCollection new]; return self;} - add: anArticle {return [contents add: anArticle];} - remove: anArticle {return [contents remove:anArticle];} - (int) size {return [contents size];} =:Класс Proceedings определяется как наследник Publication (Objective-C поддерживает только единичное наследование). Скобки вводят атрибуты ("переменные экземпляра"). Далее описываются подпрограммы; self, как и в Smalltalk, обозначает текущий экземпляр. Имя id обозначает для варианта без статической типизации общий тип всех не-С объектов. Подпрограммы, вводимые знаком +, являются "методами класса". Здесь таким методом является конструктор new. Другие подпрограммы, вводимые знаком -, являются нормальными "методами объектов", посылающими сообщения экземплярам класса.
Objective-C корпорации Stepstone оснащен библиотекой классов, первоначально построенных по образцу аналогов Smalltalk. Для NEXTSTEP также доступны многие другие классы.
Окружение и производительность
Многие из достоинств Smalltalk пришли из поддерживающей среды программирования. Он одним из первых включил инновационную для того времени интерактивную технику. Многое пришло из других проектов Xerox PARC, разрабатываемых одновременно со Smalltalk. Ставшие теперь обычными окна, значки, соединение текста и графики, выпадающие контекстные меню, использование мыши - все это идет из Palo Alto тех лет. Такие современные инструменты ОО-среды, как браузеры, инспекторы и ОО-отладчики восходят корнями к окружению Smalltalk.
Как и в Simula, все коммерческие реализации языка поддерживают сборку мусора. Smalltalk-80 и последующие реализации признаны за их библиотеки базовых классов, охватывающие важные абстракции, такие как "коллекции" и "словари", ряд графических понятий.
Отсутствие статической типизации оказалось большим препятствием на пути к эффективности систем, разрабатываемых на Smalltalk. Современная среда Smalltalk в отличие от ранних версий предоставляет не только интерпретатор, но и компилятор языка. Однако непредсказуемость типов лишает ряда важнейших оптимизаций, доступных для статически типизированных языков. Неудивительно, что многие проекты, реализованные на Smalltalk, имели проблемы с эффективностью. В действительности, неверное представление о проблемах с эффективностью, характерных для объектной технологии, можно отчасти объяснить практикой Smalltalk.
Основные черты языка
Рассмотрим в общих чертах основные свойства Simula. Автор не обидится, если читатель перейдет к следующему разделу о Smalltalk. Но чтобы полностью оценить объектную технологию, стоит потратить время на изучение Simula. Концепции представлены в их первоначальной форме, и некоторые возможности еще и теперь, спустя тридцать лет, не полностью использованы.
Simula - ОО-расширение языка Algol 60. Большинство правильных программ на Algol также являются правильными на Simula. В частности, основные структуры управления такие же, как в Algol: цикл, условный оператор, переключатель (низкоуровневый предшественник команды case в Pascal). Основные типы данных (целые, действительные и т. д.) тоже взяты из Algol.
Как и Algol, Simula использует на самом высоком уровне традиционную структуру ПО, основанную на понятии главной программы. Выполняемая программа - это главная программа, содержащая ряд программных единиц (подпрограмм или классов). Программная среда Simula поддерживает независимую компиляцию классов.
Simula использует структуру блока в стиле Algol 60: программные единицы, такие как классы, могут быть вложены друг в друга.
Все реализации Simula поддерживают автоматическую сборку мусора. Есть маленькая стандартная библиотека, включающая, в частности, двусвязные списки, используемые классом SIMULATION, изучаемым далее в этой лекции.
Как и в нотации этой книги, большинство общих сущностей, не относящихся к встроенным типам, обозначают ссылки на экземпляры класса, а не сами экземпляры. Однако это их явное свойство, подчеркиваемое нотацией. Тип такой сущности объявляется как ссылочный ref(C), а не просто C, для некоторого класса C. Для них используются специальные символы для присваивания, проверки на равенство и неравенство (:-, ==, =/= ), в то время как целочисленные и действительные операнды используют другие символы для этих целей (:=, =, /=). Выше в одной из лекций даны обоснования за и против этого соглашения.
Для создания экземпляра используется выражение new, а не команда создания:
ref (C) a; ...;a :- new CВыражение new создает экземпляр C и возвращает ссылку на него. Класс может иметь аргументы (играющие роль аргументов процедур создания в нашей нотации):
class C (x, y); integer x, y begin ... end;В этом случае при вызове new следует передать соответствующие фактические аргументы:
a :- new C (3, 98)Аргументы могут использоваться в подпрограммах класса. Но в отличие от возможности использования нескольких команд создания (конструкторов), данный подход дает только один механизм инициализации.
Кроме подпрограмм и атрибутов, класс может содержать последовательность инструкций, тело класса. Если тело существует, то вызов new будет выполнять его. Мы увидим, как использовать эту возможность, чтобы заставить классы представлять не пассивные объекты, как в большинстве других ОО-языков, а активные элементы, подобные процессам.
Механизм утверждений в языке не поддерживается. Simula поддерживает единичное наследование. Вот как класс B объявляется наследником класса A:
A class B; begin ... endДля переопределения компонента класса в классе наследника, нужно просто задать новое объявление. Оно имеет приоритет над существующим определением (эквивалента оператора redefine нет).
Первоначальная версия Simula 67 не имела явных конструкций скрытия информации. В последующих версиях, компонент, объявленный как protected, недоступен клиентам. Защищенный компонент, объявляемый как hidden, недоступен потомкам. Незащищенный компонент может быть защищен потомком, но защищенный компонент не может экспортироваться потомками.
Отложенные компоненты задаются в форме "виртуальных подпрограмм", появляющихся в параграфе virtual в начале класса. Нет необходимости объявлять аргументы виртуальной подпрограммы, как следствие, разные эффективные определения виртуальной подпрограммы могут иметь разное число и типы аргументов. Например, класс POLYGON может начинаться так:
class POLYGON; virtual: procedure set_vertices begin ... endпозволяя потомкам задавать различное число аргументов типа POINT для set_vertices: три - для TRIANGLE, четыре - для QUADRANGLE и т.
д. Эта гибкость подразумевает некоторую проверку типов во время выполнения.
Пользователям C++ следует опасаться возможной путаницы: хотя С++ был инспирирован Simula, он использует другую семантику virtual. Функция С++ объявляется виртуальной, если целью является динамическое связывание (как отмечалось, это один из самых противоречивых аспектов С++, разумнее динамическое связывание подразумевать по умолчанию). Виртуальным процедурам Simula соответствуют "чистые виртуальные функции" C++. |
(a1 qua B). fКонечно, теряется автоматическая адаптация операции к ее целевому объекту. Однако можно получить желаемое поведение динамического связывания (его можно считать изобретением Simula), объявляя полиморфные подпрограммы как виртуальные. Во многих рассмотренных примерах полиморфная подпрограмма не была отложенной, но имела реализацию по умолчанию с самого начала. Для достижения того же эффекта разработчик Simula добавит промежуточный класс, где подпрограмма виртуальна.
В качестве альтернативы использования qua, инструкция inspect дает возможность выполнять различные операции на сущности a1, в зависимости от фактического типа соответствующего объекта, обязательно представляющего собой потомка типа A, объявленного для a1:
inspect a1 when A do ...; when B do ...; ...Этим достигается нужный эффект, но лишь при замороженном множестве потомков класса, что вступает в конфликт с принципом Открыт-Закрыт.
Основные понятия
Simula, по существу, является второй попыткой. В начале 60-х был разработан язык, известный как Simula 1, для поддержки моделирования дискретных событий. Хотя он не был ОО-языком в полном смысле термина, но суть он уловил. Собственно Simula - это Simula 67, созданный в 1967 г. Далом и Нигардом (Kristen Nygaard, Ole-Johan Dahl) из Университета Осло и Норвежского Компьютерного Центра (Norsk Regnesentral). Нигард потом объяснял, что решение сохранить название отражало связь с предыдущим языком и с сообществом его пользователей. К несчастью, это название долгое время для многих людей создавало образ языка, предназначенного только для моделирования событий, что было довольно узкой областью приложения, в то время как Simula 67 - это общецелевой язык программирования. Единственные его компоненты моделирования - это набор инструкций и библиотечный класс SIMULATION, используемый небольшим числом разработчиков Simula.
Название было сокращено до Simula в 1986 г., текущий стандарт датируется 1987 г.
Последовательное выполнение и наследование
Даже если класс Simula не использует механизмы сопрограмм (detach, resume), он помимо компонентов имеет тело (последовательность инструкций) и может вести себя как процесс в дополнение к своей обычной роли реализации АТД. В сочетании с наследованием это свойство ведет к более простой версии того, что в обсуждении параллелизма называлось аномалией наследования. Язык Simula, благодаря ограничениям (наследование единичное, а не множественное; сопрограммы, а не полный параллелизм), способен обеспечить языковое решение проблемы аномалии.
Пусть bodyC - это последовательность инструкций, объявленная как тело C, а actual_bodyC - последовательность инструкций, выполняемая при создании каждого экземпляра C. Если у C нет предка, actual_bodyC - это просто bodyC. Если у C есть родитель A (один, поскольку наследование одиночное), то actual_bodyC - по умолчанию имеет вид:
actual_bodyA; bodyCДругими словами, тела предков выполняются в порядке наследования. Но эти действия по умолчанию, возможно, не то, что нужно. Для изменения порядка действий, заданных по умолчанию, Simula предлагает инструкцию inner, обозначающую подстановку тела наследника в нужное место тела родителя. Тогда действия по умолчанию эквивалентны тому, что inner стоит в конце тела предка. В общем случае тело A выглядит так:
instructions1; inner; instructions2Тогда, если предположить, что само A не имеет предка, actual_bodyC имеет вид:
instructions1; bodyC; instructions2Хотя причины введения подобной семантики ясны, соглашение выглядит довольно неуклюже:
во многих случаях потомкам необходимо создать свои экземпляры не так, как их предкам (вспомните POLYGON и RECTANGLE);тела родителей и потомков, как, например C, становится трудно понять: прочтение bodyC еще ничего не говорит о том, что будет делаться при выполнении new;соглашение не переносится естественным образом на множественное наследование (хотя это не прямая забота Simula).Трудности с inner - типичное следствие активности объектов, о чем говорилось при обсуждении параллелизма.
Почти все ОО-языки после Simula отказались от соглашения inner и рассматривали инициализацию объекта как процедуру.
Следующие фрагменты классов показывают общий
Следующие фрагменты классов показывают общий колорит Simula. Они соответствуют классам системы управления панелями, проектирование которой рассмотрено в лекции 2.
class STATE; virtual: procedure display; procedure read; boolean procedure correct; procedure message; procedure process; begin ref (ANSWER) user_answer; integer choice; procedure execute; begin boolean ok; ok := false; while not ok do begin display; read; ok := correct; if not ok then message (a) end while; process; end execute end STATE; class APPLICATION (n, m); integer n, m; begin ref (STATE) array transition (1:n, 0:m-1); ref (STATE) array associated_state (1:n); integer initial; procedure execute; begin integer st_number; st_number := initial; while st_number /= 0 do begin ref (STATE) st; st := associated_state (st_number); st.execute; st_number := transition (st_number, st.choice) end while end execute ... end APPLICATION
Пример моделирования
Классы процессов и примитивы моделирования дают элегантный механизм моделирования процессов внешнего мира. Рассмотрим в качестве иллюстрации исполнителя, которому предлагается выполнять одну из двух задач. Обе требуют некоторого времени; вторая требует включения машины m, работающей 5 минут, и ожидания, пока машина выполнит свою работу.
PROCESS class WORKER begin while true do begin "Получить следующую задачу типа i и время ее выполнения d"; if i = 2 then activate m delay 300; reactivate this WORKER after m; end; hold (d) end while end WORKERОперация "получить тип и продолжительность следующей задачи" обычно получает запрашиваемые величины от генератора псевдослучайных чисел, используя определенное статистическое распределение. Библиотека Simula включает ряд генераторов для типичных законов распределения. Предполагается, что в данном примере m - это экземпляр некоторого класса процесса MACHINE, представляющий поведение машин. Все действующие субъекты моделирования равным образом представляются классами процессов.
Пример сопрограммы
Приведем пример некоторой ситуации, где сопрограммы могут оказаться полезными. Вам предлагается напечатать последовательность действительных чисел в качестве ввода, но каждое восьмое число в выводе нужно опустить. Вывод должен представлять собой последовательность строк из шести чисел (кроме последней строки, если для ее заполнения чисел не хватает). Если in обозначает n-й элемент ввода, вывод выглядит так:
i1 i2 i3 i4 i5 i6 i7 i9 i10 i11 i12 i13 i14 i15 i17 и т. д.Наконец, вывод должен включать только 1000 чисел.
Эта задача характерна для использования сопрограмм. Она включает три процесса, каждый со своей специфической логикой: ввод, где требуется пропускать каждое восьмое число, вывод с ограничением строки до шести чисел, главная программа, где требуется обработать 1000 элементов. Традиционные структуры управления неудобны при сочетании процессов с разными ограничениями. Решение, основанное на сопрограммах, будет проходить гладко.
Введем три сопрограммы: producer (ввод), printer (вывод) и controller. Общая структура такова:
begin class PRODUCER begin ... См. далее ... end PRODUCER; class PRINTER begin ... См. далее ... end PRINTER; class CONTROLLER begin ... См. далее ... end CONTROLLER; ref (PRODUCER) producer; ref (PRINTER) printer; ref (CONTROLLER) controller; producer :- new PRODUCER; printer :- new PRINTER; controller :- new CONTROLLER; resume controller endЭто главная программа, в обычном смысле этого слова. Она создает экземпляр каждого из трех классов - соответствующую сопрограмму и продолжает одну из них - контроллер. Классы приведены далее:
class CONTROLLER; begin integer i; detach; for i := 1 step 1 until 1000 do resume printer end CONTROLLER; class PRINTER; begin integer i; detach; while true do for i := 1 step 1 until 8 do begin resume producer; outreal (producer.last_input); resume controller end; next_line end end PRINTER; class PRODUCER; begin integer i; real last_input, discarded; detach; while true do begin for i := 1 step 1 until 6 do begin last_input := inreal; resume printer end; discarded := inreal end end PRODUCER;Тело каждого класса начинается с detach, что позволяет главной программе продолжать инициализацию других сопрограмм.
Функция inreal возвращает число, прочитанное из входного потока, процедура outreal его печатает, процедура next_line обеспечивает переход на следующую строку ввода.
Сопрограммы хорошо соответствуют другим понятиям ОО-построения ПО. Заметим, насколько децентрализована приведенная схема: каждый процесс занимается своим делом, вмешательство других ограничено. Producer заботится о создании элементов ввода, printer - о выводе, controller - о том, когда начинать и заканчивать. Как обычно, хорошей проверкой качества решения является простота расширения и модификации; здесь явно надо добавить сопрограмму, проверяющую конец ввода (как просит одно из упражнений). Сопрограммы расширяют децентрализацию еще на один шаг, что является признаком хорошей ОО-архитектуры.
Архитектуру можно сделать еще более децентрализованной. В частности, процессы в описанной структуре должны все же активизировать друг друга по имени. В идеале им не нужно ничего знать друг о друге, кроме передаваемой информации (например, принтер получает last_input от producer). Примитивы моделирования, изучаемые далее, позволяют это. После этого решение может использовать полный механизм параллелизма, описанный в одной из лекций. Его независимость от платформы означает, что он будет работать для сопрограмм, так же как истинный параллелизм.
Расширения C
Трансформацию объектной технологии в 1980-х гг. от привлекательной идеи к промышленной практике во многом можно объяснить появлением и огромным коммерческим успехом языков, добавивших ОО-расширения к стабильному и широко распространенному языку С. Первой такой попыткой, привлекшей широкое внимание, был язык Objective-C, а самой известной - C++.
Эти расширения отражают два радикально различных подхода к проблеме проектирования "гибридных" языков, называемых так по той причине, что при расширении приходится сочетать ОО-механизмы с механизмами языка, основанного совсем на других принципах. (Примерами других гибридных языков являются Ada 95 и Borland Pascal.) Язык Objective-C при построении объектного расширения иллюстрирует ортогональный подход: добавляя ОО-слой к существующему языку, сохраняя при этом обе части как можно более независимыми. Язык C++ иллюстрирует подход слияния, сближая, насколько это возможно, концепции. Потенциальные преимущества каждого стиля ясны: ортогональный подход облегчает переход, избегая непредвиденных взаимных влияний, а подход слияния ведет к более согласованному языку.
Фундаментом успеха в обоих случаях был язык С, ставший к тому времени одним из доминирующих языков в промышленности. Призыв к менеджерам был понятен - превратить С-программистов в ОО-разработчиков без особого "культурного" шока. Моделью такого подхода, востребованной Бредом Коксом, была модель препроцессоров C и Fortran, например Ratfor, позволившая познакомить в 70-х гг. часть ПО сообщества с концепциями "структурного программирования", оставаясь в рамках привычного языка.
Расширения C: Objective-C, C++
Objective-C описан его создателем в статье [Cox 1984] и в книге [Cox 1990] (первое ее издание относится к 1986 г.). Пинсон и Винер написали введение в ОО-концепции, основанные на Objective-C [Pinson 1991].
Есть сотни книг по C++. (См. описание истории языка его создателем в [Stroustrup 1994].) Первая статья была [Stroustrup 1984], она расширена в книгу [Stroustrup 1986], позже переработанную в [Stroustrup 1991], содержащую много учебных примеров и полезной информации. Справочник - [Ellis 1990].
Ян Йонер опубликовал книгу "C++ критика" [Joyner 1996], доступную на нескольких Интернет-сайтах и содержащую подробные сравнения с другими ОО-языками.
Расширения Lisp
Как и многие необъектные языки, Lisp послужил основой для нескольких ОО-расширений. После Simula и Smalltalk многие ОО-языки строились на основе Lisp или по его подобию. Это неудивительно, поскольку Lisp и его реализации долгое время предлагали механизмы, непосредственно помогающие в реализации ОО-концепций, и отсутствующие в языках и окружениях, относящихся к основному направлению развития программирования. К таким механизмам можно отнести:
высоко динамичный подход к созданию объектов;автоматическое управление памятью со сборкой мусора;доступная реализация древовидных структур данных;среды разработки с широкими возможностями, как, например, Interlisp в 70-х гг. и его предшественники в предыдущее десятилетие;выбор операций во время выполнения, облегчающих реализацию динамического связывания.Концептуальный путь от Lisp к ОО-языку короче пути, идущему от C, Pascal или Ada. Термин "гибридный", обычно используемый для ОО-расширений этих языков, менее уместен для расширений Lisp.
Приложения искусственного интеллекта - главная область применения Lisp, Prolog и подобных языков, нашли в ОО-концепциях преимущества гибкости и масштабируемости. Они используют Lisp-преимущества унифицированного представления программ и данных, расширяя ОО-парадигму такими понятиями, как "протокол мета-объектов" и "вычисляемое отражение". Теперь некоторые ОО-принципы применяются не только к описанию структур времени выполнения (объектов), но также к самой структуре ПО (классам), обобщая Smalltalk-понятие метакласса и продолжая Lisp-традицию самомодифицируемого ПО. Однако для большинства разработчиков эти возможности далеки от потребностей практики. Они плохо сочетаются с подходом программной инженерии, пытающимся разделить статическую и динамическую ипостась ПО.
Три главных соперника соревновались в 80-х гг. за внимание к ним в мире ОО Lisp: Loops, разработанный в Xerox первоначально для среды Interlisp, Flavors, разработанный в MIT, доступный на нескольких Lisp-ориентированных архитектурах, Ceyx, разработанный в INRIA. В Loops было введено интересное понятие "программирования, ориентированного на данные", где можно присоединить подпрограмму к элементу данных (такому как атрибут). Выполнение подпрограммы будет инициировано не только явным вызовом, но всякий раз, когда элемент становится доступным или модифицируется. Это открывает путь к вычислению, управляемому событиями, что является дальнейшей ступенью к децентрализации архитектур ПО.
Унификация различных подходов пришла с расширением Common Lisp (Common Lisp Object System или CLOS), ставшим первым ОО-языком, получившим стандарт ANSI.
Loops: [Bobrow 1982]; Flavors: [Cannon 1980], [Moon 1986]; Ceyx: [Hullot 1984]CLOS: [Paepske 1993].
Simula
Simula - это несомненный основатель Дома Классов (Дворца Объектов). Создание его было завершено (если не принимать во внимание небольшие более поздние обновления) в 1967 г. В это, возможно, трудно поверить: оформившийся ОО-язык существовал и был реализован до структурного программирования, до публикации Парнасом статей по скрытию информации, задолго до появления фразы "абстрактный тип данных". Война во Вьетнаме еще освещалась в газетах, мини-юбки еще могли возбуждать, а на северных берегах Балтики несколько удачливых разработчиков ПО, ведомые горсткой мечтателей, уже использовали силу классов, наследования, полиморфизма, динамического связывания и других прелестей ОО.
[Dahl 1966] описывает первую версию Simula, впоследствии ставшую известной как Simula 1. Язык Simula, известный как Simula 67, впервые описан в [Dahl 1970], где за основу принимался Algol 60, и описывались расширения Simula. Одна из лекций в известной книге "Структурное программирование" (авторы: Дал, Дейкстра, Хоар) [Dahl 1972] донесли эти понятия до более широкого круга читателей. Описание языка было пересмотрено в 1984 г., оно включало элементы Algol 60. Официальная ссылка - Шведский национальный стандарт [SIS 1987]. Описание истории Simula, данное его проектировщиками, см. в [Nygaard 1981].
Самая известная книга по Simula - [Birtwistle 1973]. Она остается отличным введением. Более современное издание - [Pooley 1986].
Simula: оценка
Как и Algol 60, язык Simula знаменателен не столько своим коммерческим успехом, сколько интеллектуальным влиянием. Это очевидно и в теории (абстрактные типы данных), и в практике, где большинство языковых разработок последних двух десятилетий является его потомками - либо детьми, либо внуками его идей. Большой коммерческий успех не пришел по ряду причин, но самая важная и очевидная, заслуживающая лишь сожаления, состоит в том что, как и многие значительные изобретения до него, Simula опередил свое время. Хотя многие сразу увидели потенциальную ценность его идей, в целом программистское сообщество не было к нему готово.
Спустя тридцать лет, как ясно из предыдущего описания, многие идеи языка все еще остаются актуальными.
Сложность
Размер C++ значительно вырос в сравнении с первой версией языка, и многие жалуются на его сложность. Для этого есть все основания. Для примера можно привести маленький отрывок из статьи учебного характера признанного авторитета в C и C++, председателя комитета по стандартам С Американского Института Национальных Стандартов (ANSI), автора словаря (Dictionary of Standard C) и нескольких уважаемых книг по C++. (Я надеялся научиться у него разнице между ссылкой и указателем в С++):
Хотя ссылка похожа на указатель, но указатель - это объект, занимающий память и имеющий адрес. Не константные указатели могут также быть применены для указания на различные объекты во время выполнения. С другой стороны, ссылка - это еще одно имя (псевдоним) объекта и сама не занимает памяти. Ее адрес и значение - это адрес и значение объекта, именуемого ею. И хотя вы можете иметь ссылку на указатель, но не можете иметь указатель на ссылку или массив ссылок, или объект некоторого ссылочного типа. Ссылки на тип void также запрещены.
Ссылки и указатель не взаимозаменяемы. Ссылка на int не может, например, быть присвоена указателю на int, и наоборот. Однако ссылка на указатель на int может быть присвоена указателю на int.
Клянусь, что я пытался понять. Я был почти уверен, что уловил суть, хотя, возможно, еще не готов к семестровому экзамену. (Приведите убедительные примеры случаев, когда уместно использовать: (1) только указатель, (2) только ссылку, (3) или то, или другое, (4) ни то, ни другое.) Потом я заметил, что пропустил начало следующего абзаца:
Из всего этого следует, что неясно, почему ссылки на самом деле существуют.
Защитники C++ несомненно заявят, что большинство пользователей могут игнорировать такие тонкости. Сторонники другой школы считают, что язык программирования, главный инструмент разработчиков ПО, должен основываться на разумном количестве надежных, мощных, полностью понятных концепций. Другими словами, каждый серьезный пользователь должен знать все о языке, и доверять всему. Но может быть невозможно примирить этот взгляд с самим понятием гибридного языка - понятием, в случае С++ непреодолимо напоминающем транскрипцию Листа восхитительной шубертовской "Фантазии странника", - трудно добавить целый симфонический оркестр и сохранить звучание фортепиано.
Smalltalk
Идеи языка Smalltalk были заложены в 1970 г. Аланом Кейем в Университете Юты, в то время его выпускником и членом группы, занимающейся графикой. Алана попросили познакомиться с компилятором с расширения языка Algol 60, только что доставленного из Норвегии. (Расширением языка Algol был, конечно, язык Simula.) Изучая его, он понял, что компилятор в действительности выходит за пределы Algol и реализует ряд идей, непосредственно относящихся к его работе над графикой. Когда Кей позднее стал сотрудником Xerox Palo Alto Research Center - PARC, он заложил те же принципы в основу своего видения современной среды программирования на персональных компьютерах. В первоначальную разработку Smalltalk в центре Xerox PARC также внесли вклад А. Гольдберг и Д. Инголс (Adele Goldberg, Daniel Ingalls).
Smalltalk-72 развился в Smalltalk-76, затем в Smalltalk-80. Были разработаны версии для ряда машин - вначале для Xerox, а затем как промышленные разработки. Сегодня реализации Smalltalk доступны на большинстве известных платформ.
Ссылки на самые первые версии Smalltalk (72 и 76) см. в [Goldberg 1981] и [Ingalls 1978].
Специальный выпуск Byte, посвященный Smalltalk - [Goldberg 1981] - стал ключевым событием, обратившим внимание на Smalltalk задолго до появления широко доступных поддерживающих сред. Основная ссылка на язык, [Goldberg 1983], служит и как педагогическое описание, и как ссылка. Дополняет ее [Goldberg 1985], описывая среду программирования
Хорошим современным введением и в язык Smalltalk, и в среду VisualWorks служит [Hopkins 1995]; подробности даются в двухтомном [Lalonde 1990-1991].
История изначального влияния Simula на Smalltalk ("Algol компилятор из Норвегии") отражена в интервью Алана Кея в TWA Ambassador (да, в журнале авиалиний), точный номер выпуска забыт - в начале или середине 80-х. Я в долгу перед Бобом Маркусом за то, что он отметил связь между упадком Lisp и возрождением Smalltalk.
Smalltalk: оценка
Smalltalk явился инструментом, соединившим интерактивную технику с концепциями объектной технологии, превратив абстрактные объекты Simula в визуальные объекты, вдруг ставшие понятными и привлекательными для публики. Simula повлиял на методологию программирования и произвел впечатление на экспертов языков; Smalltalk со времени знаменитого выпуска Byte в августе 1981 г. поразил массы.
Учитывая, насколько идеи Smalltalk современны сегодня, поражаешься коммерческому успеху языка в начале 90-х гг. Этот феномен отчасти можно объяснить двумя независимыми явлениями, оба из которых имеют природу "от противного":
Эффект "испытай следующего в списке". Многие, привлеченные к объектной технологии элегантностью концепций, были разочарованы смешанными подходами, существующими, например, в C++. В поисках лучшего воплощения концепций они часто обращались к подходу, представляемому в компьютерных публикациях как чистый ОО-подход: Smalltalk. Многие разработчики Smalltalk - те, кто "просто говорят нет" C и похожим на C разработкам.Упадок Lisp. Долгое время многие компании полагались на варианты Lisp (язык Prolog и другие подходы, основанных на искусственном интеллекте) для проектов, включающих быструю разработку прототипов и проведение экспериментов. Начиная с середины 70-х, однако, Lisp исчез со сцены; Smalltalk естественно занял образовавшуюся пустоту.Последнее замечание дает хорошее представление о месте Smalltalk. Smalltalk - отличный инструмент для прототипирования и экспериментов, особенно с визуальными интерфейсами (в этом он конкурирует с современными инструментами, такими как Delphi от Borland или Visual Basic от Microsoft). Но он во многом остался в стороне от более поздних разработок в методологии инженерии программ. Об этом свидетельствует отсутствие статической типизации, механизмов утверждений, дисциплинированной обработки исключений, отложенных классов - все это имеет значение для систем, решающих критически важные задачи, или просто любой системы, чье правильное поведение во время выполнения важно для разработавшей ее организации. Остаются и проблемы эффективности.
Урок ясен: было бы неразумным, по моему мнению, сегодня использовать Smalltalk для серьезных разработок.
Сообщения
Smalltalk определяет три основные формы сообщений (и связанных с ними методов): унарные, бинарные и заданные ключевым словом. Унарные сообщения задают вызовы подпрограмм без аргументов:
acc1 balanceЗдесь сообщение balance посылается объекту, связанному с acc1. Запись эквивалентна нотации acc1.balance, используемой в Simula и в данной книге. Сообщения могут, как в данном случае, возвращать значения. Сообщения с ключевыми словами вызывают подпрограммы с аргументами:
point1 translateBy: vector1 window1 moveHor: 5 Vert: -3Заметьте, используется ключевой способ при передаче аргументов. При этом частью установленного стиля Smalltalk является объединение имени вызываемого сообщения и первого аргумента, что порождает такие идентификаторы, как translateBy или moveHor. Соответствующая запись в Simula или нашей нотации была бы point1.translate (vector1) и window1.move (5, -3).
Бинарные сообщения, похожие на инфиксные функции Ada и нотацию этой книги, служат для примирения подхода "все является объектом" с традиционными арифметическими нотациями. Большинство людей, по крайней мере старшего поколения, изучавших арифметику до объектной технологии, скорее напишут 2+3, чем:
2 addMeTo: 3Бинарные сообщения Smalltalk дают первую форму записи как синоним второй. Однако здесь есть заминка: приоритет операций. Выражение a + b * c здесь означает не то, что вы думаете - (a + b) * c. Для изменения порядка разработчики могут использовать скобки. Унарные сообщения предшествуют по старшинству бинарным, так что window1 height + window2 height имеет ожидаемое значение.
В отличие от Simula и нотации данной книги классы Smalltalk могут экспортировать только методы (подпрограммы). Для экспорта атрибутов, являющихся закрытыми, необходимо написать функцию, дающую доступ к их значениям. Типичным примером является:
x | | xx y | | yy scale: scaleFactor | | xx <- xx * scaleFactor yy <- yy * scaleFactorМетоды x и y возвращают значения переменных экземпляров (атрибутов) xx и yy. Стрелка вверх означает, что следующее выражение - это величина, возвращаемая методом отправителю соответствующего сообщения.
Метод scale имеет аргумент scaleFactor. Вертикальные полосы | | будут ограничивать локальные переменные, если они есть.
Наследование - важная часть подхода Smalltalk, но кроме некоторых экспериментальных реализаций, оно ограничивается единичным наследованием. Чтобы дать возможность при переопределении метода вызывать оригинальную версию, Smalltalk позволяет разработчику ссылаться на объект, рассматриваемый как экземпляр класса родителя, посредством имени super, как в:
aFunction: anArgument |...| ... super aFunction: anArgument ...Интересно сравнить этот подход с техникой, основанной на Precursor, и дублирующим наследованием.
Всякое связывание в Smalltalk - динамическое. В отсутствии статического связывания, ошибки, вызванные пересылкой сообщения объекту, не снабженному соответствующим методом для его обработки, не будут обнаружены компилятором и приведут к сбою во время выполнения.
Динамический контроль типов делает неуместными некоторые концепции, развитые ранее в этой книге: Smalltalk не нуждается в языковой поддержке универсальности, поскольку структуры, подобные стеку, могут содержать элементы любого типа без какой бы то ни было статической проверки согласованности. Становятся ненужными отложенные подпрограммы, поскольку для вызова x f (эквивалент x.f) нет статического правила, требующего, чтобы определенный класс обеспечивал метод f. Если класс C получает сообщение, соответствующее методу, чьи эффективные определения появляются только в потомках C, то Smalltalk лишь обеспечит возбуждение ошибки во время выполнения, Например, в классе FIGURE можно реализовать rotate таким образом:
rotate: anAngle around: aPoint | | self shouldNotImplementМетод shouldNotImplement включается в общий класс объект и возвращает сообщение об ошибке. Нотация self означает текущий объект.
У17.1 Остановимся на коротких файлах
Адаптируйте пример сопрограммы Simula (Printer-Controller-Producer), чтобы она останавливалась, если вход исчерпан до получения 1000 элементов выхода. (Подсказка: один из возможных приемов - добавить четвертую сопрограмму, "читателя".)
У17.2 Неявный вызов
(Это упражнение связано с концепциями Simula, но можно использовать нотацию, принятую в книге, расширенную примитивами моделирования, описанными в этой лекции.) Перепишите предыдущий пример так, чтобы каждая сопрограмма не нуждалась в resume явным образом. Вместо этого объявите классы сопрограммы потомками PROCESS и замените явные инструкции resume на инструкции hold (0).Подсказка: вспомните, что уведомления о событиях с одним и тем же временем активизации появляются в перечне событий в порядке их создания. Свяжите с каждым процессом условие, необходимое для продолжения процесса.
У17.3 Эмулирующие сопрограммы
Придумайте механизм эмуляции сопрограмм в выбранном вами ОО-языке, не обеспечивающем поддержку сопрограмм. Примените ваше решение к примеру, рассматриваемому в У17.1.
Подсказка: напишите процедуру resume, реализуемую как цикл, содержащий разбор случаев с отдельной ветвью для каждого resume. Для этого упражнения нельзя использовать механизм параллелизма лекция 12, поддерживающий сопрограммы.
У17.4 Моделирование
Напишите классы для моделирования дискретных событий по образцу классов Simula: SIMULATION, EVENT_NOTICE, PROCESS.
Подсказка: можно использовать технику, разработанную для предыдущего упражнения.
У17.5 Ссылка на версию предка
Обсудите заслуги техники super Smalltalk в сравнении с методами, введенными в этой книге, дающими возможность при переопределении использовать первоначальную версию: конструкцию Precursor и, когда это уместно, дублируемое наследование.