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 декларируется несколько иной набор принципов:

Linguistic Modular Units
модули должны соответствовать синтаксическим единицам языка;
Self-Documentation
разработчик модуля должен стремиться к тому, чтобы вся информация о модуле содержалась в самом модуле;
Uniform Access
все службы, предоставляемые модулем, должны быть доступны в унифицированной нотации, которая не подведет вне зависимости от реализации, использующей память или вычисления;
Open-Closed
модули должны иметь возможность быть как открытыми, так и закрытыми;
Single Choice
всякий раз, когда система программного обеспечения должна поддерживать множество альтернатив, их полный список должен быть известен только одному модулю системы.

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

Особенности синтаксиса языка Eiffel

Язык Eiffel не различает регистр символов, т.е. Class, class и CLASS -- это один и тот же идентификатор. При этом в Eiffel существует весьма жесткое соглашение об именовании:

  • названия классов должны использовать исключительно прописные буквы, т.е. используется UPPER_CASE нотация;
  • названия методов должны использовать исключительно строчные буквы, т.е. используется lower_case нотация;
  • константы и once-методы должны использовать в качестве первой прописную букву, а все остальные -- строчные, т.е. First_upper_letter_only нотация.

Точки с запятыми в качестве разделителей выражений в 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 от функций в других языках состоит в том, что

  1. Возвращаемое функцией значение нужно присвоить специальной встроенной переменной с именем Result.
  2. В Eiffel нет инструкции для принудительного выхода из функции, т.е. нет аналога C-шного return -- функция всегда исполняется от начала до конца (если только она не прерывается исключением).

Например:

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 Е.А. Охотников
LastChangedDate: 2007-08-06 13:32:55
e-mail

К сожалению я не силен в грамматике, поэтому если вы увидели здесь какие-либо орфографические или синтаксические ошибки, то не сочтите за труд -- сообщите мне. Ваша помощь поможет мне сделать этот текст гораздо лучше.

Hosted by uCoz