eao197 on the Web Сайт Евгения Охотникова |
[ Главная | Проекты | Описания | Об авторе ] |
В поисках лучшего языка / Языки / Eiffel / Обзор языка Eiffel
В поисках лучшего языка Почему я ищу новый язык? Что не так с C++? Что хочется найти? Прощай C++? Связано ли это с SObjectizer? Языки Eiffel Обзор языка Eiffel Мои впечатления от Eiffel Eiffel: ссылки Тестовые программы |
Небольшое введениеEiffel -- это уникальный язык, появившийся в 1986 году. Не смотря на свои довольно продвинутые возможности он так и не привлек к себе значительного количества программистов, как говорят, не стал мейнстримом. Тем не менее, он не исчез и развивается до сих пор. Ранее существовало несколько реализаций языка Eiffel. В настоящий момент, судя по всему, осталась только одна реализация -- EiffelStudio компании Eiffel Software. Есть так же отдельная ветка эволюции Eiffel -- SmartEiffel, но это уже практически новый язык, развивающийся независимо от своего родителя. Существует стандарт ECMA 367 для языка Eiffel, но описанный там язык на данный момент пока никем не реализован. Eiffel Software декларирует намерение выпускать новые версии EiffelStudio два раза в год последовательно приближаясь к реализации Eiffel согласно ECMA 367. Это состояние перехода Eiffel от одного варианта к другому значительно усложняет его изучение. Основной толмуд по Eiffel: Object Oriented Software Construction. 2nd Edition 1997 года (изданный на русском языке под названием Объектно-ориентированное конструирование программных систем в 2005 году) описывает состояние языка десятилетней давности. В состав EiffelStudio входит пара небольших руководств по языку, но они довольно поверхносны. Поэтому информацию об актуальном состоянии имеющейся в моем распоряжении версии языка приходится вытаскивать из разных источников, в том числе из ChangeLog-ов предшествующих релизов EiffelStudio. Eiffel кажется более простым языком, чем C++. Однако, в отличии от некоторых других языков, Eiffel поддерживает всего лишь одну парадигму -- объектно-ориентированную. Причем у разработчиков Eiffel имеется собственный взгляд на ООП. Поэтому Eiffel требует значительного времени на освоение (частично этому способствует и изрядный объем толмуда Object Oriented Software Construction). Из-за чего я не могу сказать что я хорошо освоил Eiffel. А посему данный обзор будет слишком поверхностным. Основные черты языкаПрежде всего нужно сказать, что Eiffel позиционируется не как язык программирования сам по себе. Это часть некой одноименной методологии разработки программ. Но рассказ о методологии Eiffel явно выходит за рамки моих возможностей, поэтому придется органичится только рассмотрением языковых особенностей. Сборка мусора. Eiffel поддерживает автоматическое управление памятью. Причем, в качестве положительной черты Eiffel декларируется то, что в многопоточной программе сборщики мусора для каждой из нитей работают независимо друг от друга. Это возможно, поскольку в Eiffel нет глобальных и статических переменных. Только классы. Все типы в Eiffel представляются классами. Будь то целые числа, строки, массивы или хеш-таблицы -- все это классы. Для эффективности в Eiffel есть разделение на ссылочные и расширенные (extended) классы. Главное различие между ними: если есть переменная ссылочного типа, то переменная содержит только ссылку на объект, а сам объект создается где-то там, в памяти. В случае же расширенного типа переменная содержит значение объекта. Такие классы как INTEGER_8 или CHARACTER_32 являются примерами расширенных классов. Design By Contract. Основная отличительная черта языка. Для либого метода могут быть назначены специальные условия, которые проверяются при входе в метод (предусловия, precondition) и/или после выхода из метода (постусловия, postcondition). Так же для класса можно задавать инварианты (invariants) -- условия, которые проверяются после создания экземпляра класса и при обращениях к любому из экпортированных (т.е. доступного клиентами класса) методов. Утверждения. Специальная конструкция check, эквивалентная assert в C/C++/D, позволяющая вставлять дополнительные проверки в код. Поддержка исключений. Но сильно отличающаяся от таковой в других языках. Сокрытие информации. Eiffel предоставляет гибкие механизмы указания того, кто будет иметь возможность обращаться к методам и атрибутам класса. Самодокументируемость. Стиль программирования на Eiffel располагает к написанию самодокументируемого кода. На это влияет соглашения об именовании, разделение методов по принципу команда/запрос, пред- и постусловия, инварианты. В дополнение к этому EiffelStudio позволяет строить из исходного текста документацию в различных форматах (что-то похожее на работу JavaDoc или Doxygen). Наследование. Это вообще отдельная тема для разговора. Поддержка наследования в Eiffel является его такой же фундаментальной отличительной чертой, как и Design By Contract. Eiffel поддерживает множественное (именно от слова "много") наследование, по сравнению с которым множественное наследование в C++ -- это только бледное подобие настоящего множественного наследования. Обобщенное программирование. Это не шаблоны C++, скорее это похоже на механизм generic-ов в Java/C#. Тем не менее, контейнерные классы в Eiffel с самого начала были реализованы именно посредством обобщенного программирования. Агенты. Механизм, очень похожий на указатели на функции в C/C++, делегаты в D. Позволяет передать метод объекта как параметр в какую-нибудь подпрограмму. Однократные (once) методы. Уникальная по сравнению с C++/D/Java возможность, которая позволяет исполнять тело метода только один раз -- при первом обращении к нему. Все остальные вызовы приводят к возврату ранее определенного значения. Основные черты дизайна Eiffel программПри программировании на Eiffel необходимо придерживаться ряда принципов (т.н. design principles) методологии Eiffel. Поскольку Eiffel не мультипарадигменный язык, он не позволяет сочетать в одной программе несколько стилей. Поэтому важно с самого начала придерживаться стиля Eiffel (который приверженцы Eiffel явно считают единственным правильным), в противном случае есть риск вообще не получить хорошего результата (проверено на собственном опыте). Command/Query Separation PrincipleМетод объекта не должен одновременно делать две вещи: изменять состояние объекта и возвращать какое-либо значение. Отсюда разделение на команды -- методы, которые только изменяют состояние объекта, но ничего не возвращают, и запросы -- методы, которые возвращают результат, но ничего не изменяют (т.е. не имеют побочных эффектов). Из-за этого принципа программы на Eiffel выглядят как последовательность операций "сделал" и "спросил результат". Т.е. если в C++/D можно открыть файл и сразу получить результат операции: File file; if( -1 != file.open( file_name, file_mode ) ) ... то в Eiffel эта операция будет выглядеть несколько иначе: create file.make (file_name) file.open_read if file.is_open_read then ... end Information Hiding PrincipleЗдесь ничего нового: разработчик класса должен определить, какая часть класса будет свободно доступной (публичной), а какая будет скрытой. Отличие состоит в том, что в Eiffel можно гибко управлять видимостью компонентов класса. Например: class A feature a is ... end b is ... end feature {B} c is ... end feature {C,D} d is ... end feature {NONE} e is ... end end Методы a и b будут доступны всем, метод c -- только классу B, метод d -- только классам C и D, метод e -- только классу A и его наследникам. Т.е. никаких public, protected, private или package -- компонент либо виден кому-нибудь, либо нет. При этом если класс A делает метод f видимым классу B, то говорят, что класс A экспортирует f классу B. Uniform Access PrincipleКомпоненты (features в терминологии Eiffel) в классе могут быть двух видов: атрибуты (поля) и методы (подпрограммы). Принцип унифицированного доступа говорит о том, что по записи обращения к компоненту (например, такой: x.f) не должно быть понятно, является ли компонент атрибутом или же методом. Данный принцип начинает работать в полной мере, когда производный компонент переопределяет атрибут методом: class A feature name: STRING -- name является атрибутом. end class B inherit A redefine name end feature name: STRING is ... end -- name уже стало методом. end В этом случае клиенты класса A, получившие через полиморфные ссылки ссылку на объект B не увидят разницы при работе с компонентом name. Некоторые дополненияВ книге Object Oriented Software Construction декларируется несколько иной набор принципов:
Однако, это общие принципы проектирования программ в соответствии с методологией Eiffel. К исходным текстам программ гораздо большее отношение имеют три ранее перечисленных принципа. Особенности синтаксиса языка EiffelЯзык Eiffel не различает регистр символов, т.е. Class, class и CLASS -- это один и тот же идентификатор. При этом в Eiffel существует весьма жесткое соглашение об именовании:
Точки с запятыми в качестве разделителей выражений в Eiffel необязательны. Они должны использоваться только если несколько выражений пишутся на одной строке. Но, поскольку это в Eiffel не принято делать, точки с запятыми используется только как разделители в списках параметров методов. Нужно отметить, что соглашения об оформлении кода в Eiffel весьма жесткие. Некоторые вещи, например, комментарии к отдельным секциям в описании классов или к методам, должны располагаться на определенных местах. Хотя синтаксисом языка это и не определено. Тем не менее, расположенные правильным образом комментарии используются EiffelStudio для построения документации и формирования дерева доступных классов/методов. Что из себя представляет программа на EiffelПрограмма на Eiffel собирается из классов (class). Каждый класс находится в одном файле с расширением .e. Совокупность классов объединяется в кластер (cluster). Обычно все классы кластера находятся в одном подкаталоге, но это не обязательное требование -- просто так удобнее. Программа -- система (system) -- собирается из необходимых кластеров. При этом в программе нужно указать т.н. корневой класс (root class). Исполняющая система Eiffel создает экземпляр этого класса посредством специальной процедуры создания (root creation procedure). Всю свою работу программа должна выполнить внутри этой процедуры создания. Вот так, к примеру, выглядит приложение "Hello, World" в Eiffel: class APPLICATION create make feature -- Initialization make is -- Run application. do io.put_string ("Hello, world") io.put_new_line end end -- class APPLICATION Здесь класс APPLICATION является корневым классом, а его процедура make -- процедура создания корневого класса. Выход из make означает завершение работы программы. Компилируется Eiffel программа в нативный код, но через промежуточную трансляцию в C с последующей компиляцией полученного C-кода. За счет этого достигается кроссплатформенность реализации Eiffel. Процесс трансляции состоит из нескольких (порядка шести) стадий, на которых компилятор Eiffel выискивает все используемые Eiffel классы из всех использованных кластеров, анализирует и оптимизирует вызовы, генерирует C-код, запускает C компилятор и линкер. Компиляция возможна в двух режимах -- рабочем (workbench) и финальном (finalized). Примечательной чертой рабочего режима является использование т.н. технологии "тающего льда" (Melting Ice Technology) -- при внесении изменений в исходный код не выполняется полная перекомпиляция, изменения транслируется в байт-код, который располагается рядом с ранее скомпилированным исполнимым файлом. Видимо, когда исполнимый файл запускается, он определяет наличие нового байт-кода и подгружает его в себя. За счет этого компиляция в рабочем режиме выполняется очень быстро, но полученный код работает заметно медленее. Компиляция же в финальном режиме занимает ощутимо больше времени (вероятно даже больше, чем C++), зато скорость работы сравнима со скоростью C++/D программ (хотя это сильно зависит от количества оставленных в финальном коде процедур проверки). Последние версии EiffelStudio умеют транслировать Eiffel в .NET приложения. Документация утверждает, что в этом случае EiffelStudio генерирует сразу .NET код, без промежуточного C-представления. КлассыВ Eiffel программах нет ничего кроме классов и объектов этих классов. Обычное описание класса выглядит как перечисление всех компонентов (т.е. атрибутов и методов) в нескольких секциях feature: class CLASS_NAME feature -- Header ... feature -- Another Header ... end Если класс нуждается в конструкторах (т.н. creation procedures), то имена методов, которые выступают в качестве конструктором должны перечисляться в специальной секции create (в предыдущих версиях Eiffel она называлась creation): class SAMPLE create make, make_with_something feature -- Initialization make is ... end make_with_something (arg: SOME_ARG_TYPE) is do ... end ... end В Eiffel нет перегрузки методов по типам аргументов. Поэтому, если нужно выполнить какое-то действие с разным набором параметров или с параметрами разных типов, то нужно написать функцию с уникальным именем. Что видно на приведенных выше методах make и make_with_something. Создание объекта в Eiffel выполняется специальной конструкцией create. Так, если создаваемый объект не имеет конструктора, его создание выглядит как: local obj: SOME_OBJECT do create obj end Если же объект имеет конструктор, то вызов конструктора указывается в конструкции create: local obj: SOME_OBJECT_WITH_CONSTRUCTOR do create obj.make (some_arguments) end Если нужно создать объект производного типа, а ссылку на него сохранить в объекте базового типа, то актуальное имя типа должно указываться в конструкции create: local obj: BASE_TYPE do create {DERIVED} obj.make end Специальное ключевое слово Current является аналогом this в C++/D/Java. Отложенные классы и отложенные методыВ Eiffel аналоги абстрактных классов и абстрактных методов называются отложенными (deferred). deferred class STACK feature full: BOOLEAN -- Возвращает true, если стек полон. deferred end ... end Нельзя создавать объекты отложенных классов. Вызов отложенного метода приведет к возникновению ошибки. Отложенные классы могут содержать инварианты, а отложенные методы -- пред- и постусловия. Замороженные классы и методыВ Eiffel cуществует понятия замороженных (frozen) классов и методов. Т.е. классов, от которых нельзя наследоваться и методов, которые нельзя переопределять. НаследованиеНаследование -- это единственный способ распространения функциональности между классами (поскольку нет статических методов классов или свободных функций). Т.е. если какому-то классу потребовалась функциональность из другого класса, получить ее можно только наследованием. В связи с этим в Eiffel невозможно написать ничего более-менее серьезного без наследования. А, зачастую, и без множественного наследования. Список базовых классов указывается в секции inherit описания класса: class DEMO inherit FIRST_BASE SECOND_BASE ... LAST_BASE feature ... end Переопределение методовЕсли производный класс хочет переопределить какой-то метод базового класса, то он должен перечислить все переопределения в специальной секции redefine: class DEMO inherit FIRST_BASE redefine hello, bye end feature hello is ... end bye is ... end end Переименования методовПри множественном наследовании может оказаться так, что унаследованные методы желательно было бы переименовать. Сделать это можно с помощью секции rename: class DEMO inherit FIRST_BASE rename hello as first_hello end feature hello is ... end end Объявление методов отложеннымиИногда бывает необходимо объявить унаследованный из базового типа метод отложенным. Сделать это можно с помощью секции undefine: class DEMO inherit FIRST_BASE undefine hello end ... end Выбор методов при множественном наследованииМожет оказаться, что какой-то базовый класс будет унаследован несколько раз через разные пути наследования. Например, пусть FIRST_BASE и SECOND_BASE наследуются от SUPER_BASE. Тогда SUPER_BASE будет входить в состав DEMO дважды. Соответственно, возникает вопрос -- будут ли дублироваться компоненты SUPER_BASE в DEMO. Те компоненты, которые не были переопределены в FIRST_BASE и SECOND_BASE, войдут в DEMO в единственном экземпляре. Но, если какие-то компоненты были переопределены в одном из производных классов, то возникнет конфликт выбора версии компонента. Разрешить который можно с помощью секции select: class DEMO inherit FIRST_BASE select hello end SECOND_BASE select bye end ... end В этом случае в качестве hello будет использоваться версия из FIRST_BASE, а в качестве bye -- версия из SECOND_BASE. Управление видимостью унаследованных компонентовEiffel позволяет в производном классе изменить описанные в базовом классе параметры экспорта компонентов. Например, если базовый класс экспортировал a классу A, b классу B, а c вообще не был никому доступен: class BASE feature {A} a is ... end feature {B} b is ... end feature {NONE} c is ... end end то производный класс посредством секции export может полностью изменить картину экспорта: class DERIVED inherit BASE export {NONE} a -- a уже никому не доступна. {B,C} b -- b уже доступна и B, и C. {ANY} c -- c уже доступна всем. end ... end При множественном наследовании может возникнуть необходимость скрыть от клиентов все методы, унаследованные от какого-то базового класса. В этом случае используется ключевое слово all: class DEMO inherit FIRST_BASE SECOND_BASE export -- Никому не будут доступны унаследованные методы SECOND_BASE. {NONE} all end ... end Обращение к версии из базового классаЕсли нужно вызвать реализацию переопределенного метода из базового класса, то используется специальное ключевое слово Precursor: class DEMO inherit FIRST_BASE redefine hello end feature hello is Precursor ... end end Приведения типов и попытка присваиванияВ Eiffel нет специального синтаксиса для приведения типов. Если речь идет о том, чтобы преобразовать значение INTEGER_8 в NATURAL_8, то нужно воспользоваться методом to_natural_8 класса INTEGER_8. Если речь идет о преобразовании типа ссылки (например, down-casting от базового типа к производному), то в Eiffel для этой цели предназначена специальная конструкция -- попытка присваивания (assignment attempt): do_something (b: BASE) is local d: DERIVED -- DERIVED является наследником BASE. do d ?= b if d /= Void then -- Точно известно, что b является экземпляром DERIVED. ... else -- Точно известно, что b не является экземпляром DERIVED. end end ЦиклыВ Eiffel есть только одна конструкция для организации циклов: from [init_actions] until <condition> [invariant <inv_conditions>] [variants <var_condition>] loop <body_actions> end Секция from может содержать набор инструкций для инициализации цикла, а может и быть пустой. Секция until содержит условие выхода из цикла. Что вызывает серьезную ломку сознания при переходе на Eiffel из C/C++/Java/D. Секция loop содержит тело цикла. Все. Ничего другого для циклов в Eiffel нет. ФункцииФункции -- это методы, которые возвращают результат. Отличие функций Eiffel от функций в других языках состоит в том, что
Например: is_item_in_array (where: ARRAY [INTEGER]; what: INTEGER): BOOLEAN is -- Ищет элемент what в векторе where. local i: INTEGER found: BOOLEAN do found := false from i := where.lower until (i > where.upper) or (found) loop if what = where [i] then found := true end i := i + 1 end Result := found end Этот пример можно переписать короче учитывая тот факт, что любая переменная или атрибут в Eiffel получает начальное значение. Для BOOLEAN -- это false, для числовых типов -- 0, для ссылочных типов -- специальное значение Void (аналог null в Java/D). Это же правило распространяется и на переменную Result, поэтому можно обойтись без вспомогательной переменной found: is_item_in_array (where: ARRAY [INTEGER]; what: INTEGER): BOOLEAN is -- Ищет элемент what в векторе where. local i: INTEGER do from i := where.lower until (i > where.upper) or (Result) loop if what = where [i] then Result := true end i := i + 1 end end Если функция возвращает ссылку, то где-то в теле функции должна применяться конструкция create с использованием переменной Result: string_to_binary (what: STRING): ARRAY [NATURAL_8] is -- Возвращает вектор ASCII кодов символов исходной строки. local i: INTEGER do create Result.make (1, what.count) from i := 1 until i > what.count loop Result.put (what.item_code (i), i) i := i + 1 end end Design By ContractМеханизм Design By Contract позволяет задавать различные типы условий, проверяемых во время работы программы. Нарушение любого из условий приводит к выбрасыванию исключения. Т.е. нормальная работа прерывается и, если данное исключение должным образом не обрабатывается, приложение завершается. Предусловия, постусловия и инвариантыМеханизм Design By Contract базируется на трех основных конструкциях: предусловиях, постусловиях и инвариантах. Предусловия записываются в специальной секции require реализации метода, постусловия -- в секции ensure. А инварианты в специальной секции invariant описания класса. Общий формат таков: [tag:] condition где tag -- это необязательный идентификатор, а condition -- это условие. Подобных утверждений в пред-, постусловиях и инвариантах может быть сколько угодно. Предусловия проверяются перед началом выполнения тела метода. Т.е. о выполнении предусловий должна позаботиться вызывающая сторона и метод не должен отрабатывать, если его предусловия не выполняются. Постусловия проверяются после завершения тела метода. Т.е. о выполнении постусловий должна позаботиться реализация метода. В постусловиях можно использовать специальную конструкцию old, которая позволяет возвращает значения атрибутов, предшествовавшие вызову метода. Инварианты проверяются перед и после завершения работы публичных методов. Вот пример пред- и постусловий: string_to_binary (what: STRING): ARRAY [NATURAL_8] is -- Возвращает вектор ASCII кодов символов исходной строки. require valid_argument: what /= Void local i: INTEGER do create Result.make (1, what.count) from i := 1 until i > what.count loop Result.put (what.item_code (i), i) i := i + 1 end ensure valid_result: Result /= Void result_has_same_size: what.count = Result.count end Теги для условий (т.к. valid_argument и valid_result), если заданы, включатся в описание ошибки, выдаваемое исполнительной системой Eiffel при обнаружении нарушения условия. Что довольно удобно при отладке. Инварианты записываются в специальной секции invariant в описании класса: class OPTIONS_PARSING_RESULT create make_ok, make_failed feature -- Initialization make_ok (parsed_options: OPTIONS) is -- Creation when parsing successful. do is_ok := true options := parsed_options end make_failed( failure_reason: STRING ) is -- Creation when parsing failed. require failure_reason /= Void do is_ok := false reason := failure_reason.twin end feature -- Access is_ok: BOOLEAN options: OPTIONS reason: STRING invariant options_iff_ok: is_ok implies (options /= Void) reason_iff_failure: (not is_ok) implies ((reason /= Void) and (options = Void)) end Здесь инвариант указывает, что если атрибут is_ok равен true, то атрибут options не может быть пустой ссылкой. И напротив, если is_ok равен false, то options обязан быть пустой ссылкой, зато reason быть пустой ссылкой не может. Предусловия и постусловия при наследованииПред- и постусловия задают публичный контракт для метода класса. Поэтому, будучи однажды определенными, они не могут быть просто так изменены в производных классах. Предусловия задают требовани к клиенту класса. Поэтому предусловия нельзя ужесточать -- ведь тогда клиент, отлично работавший с базовым классом, перестанет работать с производным. Можно только ослабить предусловия. Из-за этого, если производный класс переопределяет метод и определяет свои предусловия, то новые предусловия должны быть записаны в секции require else: class BASE feature demo is require ... do end end class DERIVED inherit BASE redefine demo end feature demo is require else ... do end end Соответственно, при вызове demo из DERIVED будет проверяться предусловия базового класса. И, только если они не выполняются, предусловия DERIVED. Постусловия, напротив, определяют обязательства поставщика класса. Следовательно, в производном классе нельзя ослабить постусловия, ведь это означало бы отказ от части обязательств. Поэтому в производном классе постусловия можно только усилить. Из-за чего новые постусловия записываются в секции ensure then: class BASE feature demo is do ensure ... end end class DERIVED inherit BASE redefine demo end feature demo is do ensure then ... end end Соотвественно, при вызове demo из DERIVED будут проверяться и постусловия из базового класса, и постусловия из DERIVED. УтвержденияКонструкция: check [tag:] <condition> ... end позволяет задавать утверждения, которые будут проверяться во время работы программы. Т.е. являются полными аналогами assert в C/C++/D. Инварианты и варианты циклаСекции invariant и variant задают условия, которые позволяют проверять корректность цикла. Условия из секции invariant должны выполняться на каждой итерации цикла. Интересна секция variant. Она задает выражение, производящее целочисленую величину. Эта величина должна оставаться положительной, но должна уменьшаться на каждой итерации цикла. Нужно отметить, что мне секция variant очень помогла. Поскольку я привык с C-шным циклам for, в которых на автомате пишется ++i, то в Eiffel я постоянно забывал инкрементировать переменную цикла. Т.е. я писал так: from i := 1 until i > what.count loop Result.put (what.item_code (i), i) end Соответственно, программа зависала, но требовалось некоторое время чтобы понять, где именно. Добавив секцию variant в каждый такой цикл я стал сразу же получать от Eiffel указания, где мой цикл зависает: from i := 1 until i > what.count variant what.count + 1 - i loop Result.put (what.item_code (i), i) i := i + 1 end Все это дело в финальной версии программыЕстественно, что такие тотальные проверки не могут не сказаться на скорости работы программы. И они сказываются. Очень серьезно. Поэтому в финальной версии программы, по умолчанию, все проверки отключены. Но в свойствах проекта можно указать уровень проверок, которые нужно оставить в программе. Например, только предусловия или предусловия и постусловия, или предусловия+постусловия+инварианты, и т.д. Чем больше проверок оставлено, тем медленнее будет работать финальная версия. По моим впечатлениям, скорость финальной версии вообще без проверок сравнима со скоростью C/C++/D. Даже включение предусловий не сильно сказывается на скорости работы. Поэтому я бы оставлял в финальной версии проверку предусловий. Хотя Бертран Мейер советует писать так, чтобы программа работала вообще без каких-либо проверок в финальной версии. Так же проверки серьезно сказываются на размере результирующего кода. Например, рабочая версия со всеми проверками оказывается порядка 9Mb (да, девять мегабайт, даже если это "Hello, World"). Финальная версия без проверок уменьшается до 700Kb. Включение предусловий увеличивает объем где-то на 2-2.5Mb. ИсключенияИсключения в Eiffel есть, но они особенные. Скорее это похоже на механизм сигналов в Unix, хотя при порождении исключения просходит раскрутка стека до первого обработчика, само исключение -- это не объект. Скорее это набор неких атрибутов: код исключения и связанное с ним сообщение. Обработчик исключения в методе может быть только один -- он пишется в специальной секции rescue. Обработчик может содержать обычные инструкции и специальную команды retry. Использование retry предписывает выполнить метод повторно с самого начала (при этом все промежуточные значения, полученные при предшествующем запуске остаются на своих местах). Если команды retry нет, то по завершении секции rescue исключение выбрасывается наружу. Т.е. либо функция завершается нормально (возможно после нескольких повторных попыток), либо исключение выбрасывается наружу. Т.е. код с использованием исключений выглядит одним из следующих образов: some_method is local attempts: INTEGER do ... -- какие-то действия. rescue if attempts < Max_attempts then -- Можно попробовать повторить операцию еще раз. attempts := attempts + 1 retry end -- В противном случае все попытки исчерпаны и -- исключение выпускается наружу. end another_method is do ... -- какие-то действия. rescue ... -- какая-то очистка ресурсов без retry. -- После чего исключение выпускается наружу. end Обобщенное программированиеEiffel поддерживает обобщенное программирование как в неограниченной (unconstrained), так и в ограниченной (constrained) форме. В первом случае, когда на родовой параметр не накладывается никаких ограничений, обобщенный класс не может вызывать никаких методов у объектов типа родового параметра: class ARRAY [G] -- G является родовым параметром. feature item (index: INTEGER): G is ... end put (v: G; index: INTEGER) is ... end ... end Т.е. класс ARRAY [G] способен манипулировать ссылками на объекты типа G, но не способен вызывать у этих объектов никаких методов. В случае ограниченного обобщенного программирования требуется указать, от какого класса должен наследоваться тип родового параметра: class MAP [V, K -> HASHABLE] ... end Здесь на тип V не накладывается никаких ограничений, но требуется, чтобы тип K наследовался от HASHABLE (т.е. реализовал контракт HASHABLE). Поскольку классу MAP нужно уметь получать хеш-код ключа посредством метода hash_code. ТуплыEiffel поддерживает понятие тупла -- набора значений разных типов, например, запись вида: TUPLE [INTEGER; STRING; REAL] определяет тип тупла с элементами INTEGER, STRING, REAL. Экземпляр тупла в программе записывается в виде заключенного в квадратные скобки набора значений: [10, "Hello, World", 3.1415] Элементы тупла могут быть именованными: local my_type: TUPLE [age: INTEGER; name: STRING; height: REAL] do my_type := [10, "Bob", 1.34] my_type.age := my_type.age + 1 my_type.name := "Robert" end К элементам тупла можно обращаться по порядковым номерам с помощью метода item. Тупл вида TUPLE [A, B, C] определяет тип последовательности из, как минимум, трех значений. Поэтому TUPLE [A, B, C] является поддтипом последовательности из, как минимум, двух значний -- TUPLE [A, B]. В свою очередь, TUPLE [A, B] является подтипом TUPLE [A]. Имена элементов тупла не учитываются при выводе общего типа тупла. Т.е. два тупла, у которых отличаются только имена элементов, но совпадают количество, типы и порядок следования элементов, считаются одинаковыми: class APPLICATION create make feature make is local t1, t2: TUPLE [n: STRING; a: INTEGER] t3: TUPLE [STRING] do t1 := query_tuple ("first", 21) t2 := query_tuple ("second", 22) t3 := t2 end feature {NONE} query_tuple (s: STRING; a: INTEGER): TUPLE [name: STRING; age: INTEGER] is do create Result.make Result.name := s Result.age := a end end AгентыEiffel позволяет сохранять ссылку на метод в переменной или передавать ее в виде параметра в какой-то метод. Выглядит это так: x.f (agent obj.method) это означает, передачу ссылки на метод method объекта obj в метод f объекта x. Где метод f может иметь формат: f( a: PROCEDURE [ANY, TUPLE [ARGS]] ) или f( a: FUNCTION [ANY, TUPLE [ARGS], RESULT] ) Внутри f можно сделать вызов агента посредством метода call. Например, пусть есть: class AGENT_PROVIDER feature demo (i: INTEGER; s: STRING) is do io.put_string ("i=") io.put_integer (i) io.put_string ("; s=") io.put_string (s) io.put_new_line end end class AGENT_CONSUMER feature call_agent (a: PROCEDURE [ANY, TUPLE [INTEGER, STRING]]) is do a.call ( [0, "Hello, World"] ) end end class APPLICATION create make feature make is local producer: AGENT_PROVIDER consumer: AGENT_CONSUMER do create producer create consumer consumer.call_agent (agent producer.demo) end end Это был пример который использовал открытые аргументы, т.е. значение аргументов задавала вызыващая сторона. Eiffel так же поддерживает закрытые аргументы, т.е. когда часть значений аргументов задается в конструкции agent. Например: class AGENT_PROVIDER feature demo (who: STRING; i: INTEGER; s: STRING) is do io.put_string ("Who: " + who + "; ") io.put_string ("i=") io.put_integer (i) io.put_string ("; s=") io.put_string (s) io.put_new_line end end class AGENT_CONSUMER feature call_agent (a: PROCEDURE [ANY, TUPLE [INTEGER, STRING]]) is do a.call ( [0, "Hello, World"] ) end end class APPLICATION create make feature make is local producer: AGENT_PROVIDER consumer: AGENT_CONSUMER do create producer create consumer consumer.call_agent (agent producer.demo ("Application", ?, ?)) end end Здесь аргумент who сделан закрытым, а аргументы i и s -- открытыми. Как и аргументы, открытыми и закрытыми могут быть цели агентов (т.е. объекты, на чей метод в агентах хранятся ссылки). Выше были показаны примеры закрытых, т.е. заранее определенных целей. Запись вида: agent obj.f указывает, что агент связан с методом f объекта obj и ни с кем иным. Запись же: agent {TYPE}.f указывает, что агент связан с методом f любого объекта типа TYPE. Однако, в этом случае тип агента будет несколько иным: в метод call агента нужно будет передать тупл, первым элементом которого будет является ссылка на цель вызова: class AGENT_PROVIDER create make feature make (my_name: STRING) is do name := my_name end feature demo (who: STRING; i: INTEGER; s: STRING) is do io.put_string ("I'm: " + name + "; ") io.put_string ("Who: " + who + "; ") io.put_string ("i=") io.put_integer (i) io.put_string ("; s=") io.put_string (s) io.put_new_line end feature {NONE} name: STRING end class AGENT_CONSUMER feature call_agent (a: PROCEDURE [ANY, TUPLE [INTEGER, STRING]]) is do a.call ( [0, "Hello, World"] ) end call_agent_from_object ( obj: AGENT_PROVIDER; a: PROCEDURE [ANY, TUPLE [ANY, INTEGER, STRING]]) is do a.call ( [obj, 0, "Hello, World"] ) end end class APPLICATION create make feature make is local first, second: AGENT_PROVIDER consumer: AGENT_CONSUMER do create first.make ("First") create second.make ("Second") create consumer consumer.call_agent (agent first.demo ("Application", ?, ?)) consumer.call_agent_from_object ( second, agent {AGENT_PROVIDER}.demo ("Application", ?, ?)) end end Существует так же возможность создавать т.н. inline agents, т.е. агентов тело которых определяется непосредственно в конструкции agent: consumer.call_agent ( agent (i: INTEGER; s: STRING) do io.put_string ("inline agent!%N") end) В этом случае целью агента будет объект Current. Ковариантность и якоряПри переопределении методов в производных классах в Eiffel разрешается изменять типы аргументов и/или возвращаемого значения на более подходящие, хотя и совместимые с оригинальными. Например, если вначале были классы: class MESSAGE ... end class ENVELOPE ... feature msg: MESSAGE end class ENVELOPER feature enveloping_result (m: MESSAGE): ENVELOPE is -- Упаковывает сообщение в конверт и возвращает -- получившийся конверт. do ... end end То затем может появиться набор производных от них классов для поддержки подписанных сообщений: class SIGNED_MESSAGE inherit MESSAGE ... end class SIGNED_ENVELOPE inherit ENVELOPE redefine msg end feature msg: SIGNED_MESSAGE end class SIGNED_MESSAGE_ENVELOPER inherit ENVELOPER redefine enveloping_result end feature enveloping_result (m: SIGNED_MESSAGE): SIGNED_ENVELOPE is ---Упаковывает подписанное сообщение в подписанный -- конверт и возвращает получившийся конверт. do ... end end Такая специализация компонентов msg в классе SIGNED_ENVELOPE и enveloping_result в классе SIGNED_MESSAGE_ENVELOPER является примером ковариантной типизации (covariant typing) -- когда типы изменяются по мере спуска по иерархии наследования. Ковариантная типизация могла бы приводить к лавинообразной модификации прототипов компонентов в производных классах. Например, раз класс SIGNED_ENVELOPE хранит SIGNED_MESSAGE вместо просто MESSAGE, значит в своем конструкторе он должен получать ссылку не на MESSAGE, а на SIGNED_MESSAGE. Равно как если бы базовый класс ENVELOPE имел другие методы, получающие или возвращающие MESSAGE, то SIGNED_ENVELOPE должен был бы предоставить их новые версии (даже если бы новые версии полностью совпадали с предыдущими). Т.е. если был класс: class ENVELOPE create make feature make (m: MESSAGE) is do msg := m end msg: MESSAGE is_same_message (m: MESSAGE): BOOLEAN is do Result := msg.is_equal (m) end clone_message: MESSAGE is do Result := clone (msg) end end то в новом классе SIGNED_ENVELOPE могло бы потребоваться создание точных копий make, is_same_message и clone_message, только оперирующих типом SIGNED_MESSAGE. Для того, чтобы избежать этого бесполезного дублирования кода в Eiffel существует понятие закрепленных типов или, в дословном переводе, якорных типов (anchored types). Т.е. тип какого-то компонента объявляется якорем, а все остальные объявляются подобными на него, для чего используется специальная конструкция like. Так, в примере с ENVELOPE все вращается вокруг атрибута msg, следовательно его можно сделать якорем. А все остальные ссылки на тип MESSAGE заменить конструкцией like msg: class ENVELOPE create make feature make (m: like msg) is do msg := m end msg: MESSAGE is_same_message (m: like msg): BOOLEAN is do Result := msg.is_equal (m) end clone_message: like msg is do Result := clone (msg) end end При таком подходе все, что нужно сделать в классе SIGNED_ENVELOPE -- это сменить тип якоря msg, смена актуальных типов для make, is_same_message и clone_message произойдет автоматически: class SIGNED_ENVELOPE inherit ENVELOPE create make end И все, больше ничего в SIGNED_ENVELOPE изменять не нужно (если только не добавить в него какую-то специфическую функциональность). Конструкцию like можно применять и к Current, например: class ENVELOPE ... feature copy (other: like Current) is do msg := clone (other.msg) end ... end т.е. метод copy класса ENVELOPE ожидает, что тип аргумента other будет соответствовать типу ENVELOPE. Константы и once-методыКонстанты и уникальные значенияКонстанты декларируются в Eiffel в виде: Const_name: TYPE is VALUE Например: Ok: INTEGER is 0 File_not_found: INTEGER is 2 File_busy: INTEGER is 3 Ready_question: STRING is "Are you ready?" Если нужно определить несколько целочисленных констант, каждое из которых должно иметь уникальное значение (но не важно какое именно), то можно использовать конструкцию unique: Ok, File_not_found, File_busy: INTEGER is unique При работе с константами в Eiffel сказывается то, что Eiffel объектный язык без глобальных переменных, статических методов или свободных функций. Так, если какой-нибудь класс, например, FILE определяет набор констант, то получить доступ к значению конкретной константы можно одним из следующих подходов: Через объект типа FILE: local f: FILE do f.open ("myfile.txt") if f.Ok = f.open_result then ... end ... end Через специальный синтаксис {TYPE}.Constant: local f: FILE do f.open ("myfile.txt") if {FILE}.Ok = f.open_result then ... end ... end Через наследование от класса FILE: class MY inherit FILE export {NONE} all end feature update_my_file is do open ("myfile.txt") if Ok = open_result then ... end ... end end Once-методыOnce-методы в Eiffel выполняются всего один раз -- во время первого обращения к ним. Если once-метод является функцией, то возвращенное ей значение будет сохранено и будет возвращаться при всех последующих обращениях к ней. Декларация once-метода отличается от деклараций обычных методов всего лишь использованием ключевого слова once вместо do: feature console_window: WINDOW is once ... end Once-функции являются способом создания значений, разделяемых всеми экземплярами класса. Например, io является once-функцией базового для всех класса ANY, благодоря чему любое обращение к io в любом из объектов приводит к использованию одного и того же значения. Переопределение операторов и псевдонимы методовEiffel позволяет определять одинаковую реализацию для нескольких методов с разными именами: class OUPUT_STREAM feature put_real, putreal (v: REAL) is ... end put_string, put_line, putline (v: STRING) is ... end ... end При этом определяются независимые друг от друга методы с одинаковыми сигнатурами и одинаковой первоначальной реализацией. В последствии любой из них может быть переопределен независимо от остальных. Поскольку в Eiffel-программе есть только объекты, которые взаимодействуют друг с другом посредством вызова методов, в Eiffel разрешено переопределение операторов для того, чтобы было удобно записывать математические выражения. Переопределение выполняется посредством назначения псевдонимов методам: class COMPLEX feature plus alias "+" (v: COMPLEX) is ... end ... end Переопределять можно не только стандартный набор арифметических операторов. В качестве псевдонима может быть задана любая последовательность символов, если только она начинается с одного из стандартных символов. Например, можно определить собственный оператор |<<: class MESSAGE create make feature make (msg_text: STRING) is require valid_text: msg_text /= Void do text := msg_text end text: STRING end class ENVELOPE create make feature make (a_receiver: STRING; a_msg: MESSAGE; a_sender: STRING) is require valid_receiver: (a_receiver /= Void) and then (0 < a_receiver.count) valid_msg: a_msg /= Void valid_sender: (a_sender /= Void) and then (0 < a_sender.count) do receiver := a_receiver msg := a_msg sender := a_sender end receiver: STRING msg: MESSAGE sender: STRING end class ENVELOPER create make feature make (a_receiver: STRING; a_sender: STRING) is require valid_receiver: (a_receiver /= Void) and then (0 < a_receiver.count) valid_sender: (a_sender /= Void) and then (0 < a_sender.count) do receiver := a_receiver sender := a_sender end enveloping_result alias "|<<" (msg: MESSAGE): ENVELOPE is require valid_msg: msg /= Void do create Result.make (receiver, msg, sender) end feature {NONE} receiver: STRING sender: STRING end class APPLICATION create make feature make is local enveloper: ENVELOPER enveloped_msg: ENVELOPE do create enveloper.make ("Alice", "Bob") enveloped_msg := enveloper |<< (create {MESSAGE}.make ("Hello!")) io.put_string ("Receiver: " + enveloped_msg.receiver + "; ") io.put_string ("Sender: " + enveloped_msg.sender + "; ") io.put_string ("Msg: " + enveloped_msg.msg.text) io.put_new_line end end В данном примере для класса ENVELOPER переопределен нестандартный оператор |<<, который является синонимом метода enveloping_result. Возможность назначения псевдонимов с нестандартными именами предназначена для того, чтобы можно было придумывать обозначения, специфические для какой-нибудь конкретной прикладной области (например, для физических расчетов, где могут потребоваться операторы вида |-| или <->). Примечание. Я столкнулся с тем, что в книге Object Oriented Software Construction вводится один набор специальных символов, которые могут начинать переопределенный оператор. В ECMA стандарте вообще не делается никаких ограничений на этот счет. Но EiffelStudio 6.0.6 (GPL-Windows версия) не позволяет, например, начинать оператор с символа <. В другом источнике утверждается, что переопределенные операторы могут начинаться с одного из следующих символов: @ # | &. Eiffel выделяет особую форму псевдонимов, т.н. bracket alias, для переопределения []. Эта форма удобна для организации общепринятого доступа к векторам, матрицам, ассоциативным таблицам: class VECTOR [G] feature item alias "[]" (index: INTEGER): G is ... end ... end что позволяет извлекать элементы вектора традиционным способом: local v: VECTOR [REAL] do ... io.put_real (v [1]) ... end Для того, чтобы предоставить общеупотребительную форму для изменения элемента по индексу используется специальный синтаксис определения псевдонима с использованием ключевого слова assign: class VECTOR [G] feature item alias "[]" (index: INTEGER): G assign put do ... end put (v: G; index: INTEGER) is do ... end end Отличительной чертой переопределения операторов в Eiffel является то, что реализация оператора должна быть выполнена только в форме запроса, а не команды. Т.е. если следовать заповеди, что запросы не могут иметь побочных эффектов, то и переопределенные операторы в Eiffel не могут приводить к побочным эффектам. |
© 2007 Е.А. Охотников |
К сожалению я не силен в грамматике, поэтому если вы увидели здесь какие-либо орфографические или синтаксические ошибки, то не сочтите за труд -- сообщите мне. Ваша помощь поможет мне сделать этот текст гораздо лучше. |