eao197 on the Web
Сайт Евгения Охотникова
[ Главная | Проекты | Описания | Об авторе ]

В поисках лучшего языка / Языки / Eiffel / Мои впечатления от Eiffel

В поисках лучшего языка
Почему я ищу новый язык?
Что не так с C++?
Что хочется найти?
Прощай C++?
Связано ли это с SObjectizer?
Языки
Eiffel
Обзор языка Eiffel
Мои впечатления от Eiffel
Eiffel: ссылки
Тестовые программы

Что понравилось

Продуманность

Практически каждый момент в Eiffel указывает на то, что он появился в языке не просто так, что его введение было логически обоснованным. Это не удивительно, поскольку язык Eiffel создавался в качестве поддержки одноименной методологии разработки программ.

В качестве примера можно рассмотреть процедуру создания объектов. Напомню, что Eiffel создавался в середине 80-х, когда статически-типизированных языков с автоматическим выводом типов было не так уж и много. В следствии этого в таких языках, как C++/Java для создания объекта какого-то типа и связывания его с некоторой переменной тип объекта нужно указать в тексте программы дважды:

// C++
My_Window * window = new My_Window(...);

В Eiffel же действует правило, что раз нам один раз уже пришлось объявить тип переменной, то больше этого делать не нужно. Поэтому достаточно в конструкции create указать только имя переменной, а нужный тип компилятор сможет определить сам:

local
  window: MY_WINDOW
do
  create window.make (...)

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

Eiffel располагает к написанию пред-, постусловий и инвариантов

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

Не могу понять, откуда это, ведь я уже пользовался до Eiffel языком с поддержкой Design By Contract -- языком D. Но при программировании на D ты сам себя заставляешь: "Не плохо было бы определить здесь предусловия". В то время как в Eiffel это происходит само собой: "У этого метода вот такие предусловия и сейчас я их запишу".

Нужно еще сказать, что Design By Contrat действительно помогают выявлять ошибки на самых ранних стадиях отладки.

Якоря и like-определения

Очень интересный механизм. Например, в C++ время от времени сталкиваешься с задачей организации метода clone для входящих в некоторую иерархию классов. В C++ сделать это можно только так:

class base_t
  {
  public :
    virtual std::auto_ptr< base_t >
    clone() const = 0;
    ...
  };
class concrete_derived_t : public base_t
  {
  public :
    virtual std::auto_ptr< base_t >
    clone() { ... }
    ...
  };

И, если затем в коде нужно получить копию объекта concrete_derived_t для последующей модификации, то без приведений типов уже не обойтись:

const concrete_derived_t & src_obj = ...;
std::auto_ptr< base_t > new_obj = src_obj.clone();
concrete_derived_t & modifiable_obj =
    dynamic_cast< concrete_derived_t & >( *new_obj );

В то время как в Eiffel все это делается гораздо компактнее:

local
  src_obj, modifiable_obj: CONCRETE_DERIVED
do
  src_obj := ...
  modifiable_obj := clone (src_obj)
  ...
end

Хотя, в Eiffel с ковариантностью и заякоренными типами все не так однозначно. Есть даже специальное понятие, CATCALL, описывающие ситуацию, когда ковариантная типизация может привести к нарушению статической типизации. Но, чесно говоря, при первом знакомстве с Eiffel я не понял всей сути этого явления, его потенциальных симптомов и последствий.

Синтаксис

Синтаксис откровенно порадовал. Пожалуй, это первый язык после Ruby, синтаксис которого не вызывал у меня серьезного напряжения.

Во-первых, UPPER_CASE для имен классов и lower_case для имен компонентов. Очень удобно. Тем более, что при работе с CamelCase у меня быстро устают глаза и чтение текстов на Eiffel создает меньшую нагрузку на зрение.

Во-вторых, опциональность точек с запятой.

В-третьих, отсутствие открывающих и закрывающих фигурных скобок.

Последние два момента можно было бы считать мелочами и таковыми они являются. Но это настолько частоупотребительные и разражающие (в других языках) мелочи, что удовольствие от использования какого-либо языка они способны сильно испортить. Здесь же этого не происходит.

С другой стороны, в Eiffel есть драконовские требования к оформлению кода. Например, вызов метода с параметрами должен записываться только как:

a.f (x, y, z)

но не как:

a.f(x, y, z)

или:

a.f( x, y, x )

(нелегитимность последней записи вообще меня очень сильно огорчает, т.к. я использую ее уже лет 8-9).

Неоднозначные моменты

Здесь я попытался расположить моменты, которые, с одной стороны, мне не нравятся. Однако, есть большая вероятность, что это просто от непривычности к Eiffel. Вероятно, при длительном использовании Eiffel они бы уже перестали казаться таковыми и, наоборот, выглядели бы достоинствами языка.

Однопарадигменность: наследование, только наследование и ничего кроме наследования

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

В качестве примера можно рассмотреть понадобившуюся мне функцию преобразования массива 8-ми битовых значений в строку с их шестнадцатиричными представлениями (т.е. сделать hex-dump). В Pascal/C/C++/D я мог бы создать свободную функцию hex_dump и все. Эта функция зависит только от своих аргументов, возвращает результат и не имеет никаких побочных эффектов. В C++/D/Java ее можно было бы сделать статическим методом какого-нибудь утилитарного класса. И все. Когда она нужна -- она просто вызывается.

В Eiffel ситуция в корне иная. Нет свободных функций, нет статических методов. Поэтому в Eiffel нужно идти по одному из двух путей.

1. Создать вспомогательный класс HEX_DUMPER с одним компонентом: hex_dump_string:

class
  HEX_DUMPER

feature
  hex_dump_string (what: ARRAY [NATURAL_8]): STRING is ... end
end

который бы наследовался в нужном мне классе:

class
  APPLICATION_SPECIFIC_CLASS

inherit
  SOME_DOMAIN_SPECIFIC_BASE
  YET_ANOTHER_DOMAIN_SPECIFIC_BASE
  ...
  HEX_DUMPER
...
feature
  some is
    local
      hex_dump: STRING
    do
      hex_dump := hex_dump_string (data)
      ...
    end
  ...
end

2. Не наследоваться от HEX_DUMPER, а создавать его экземпляр там, где мне необходимо формирование шестнадцатиричного представления:

class
  APPLICATION_SPECIFIC_CLASS

inherit
  SOME_DOMAIN_SPECIFIC_BASE
  YET_ANOTHER_DOMAIN_SPECIFIC_BASE
  ...
...
feature
  some is
    local
      hex_dump: STRING
      hex_dumper: HEX_DUMPER
    do
      create hex_dumper
      hex_dump := hex_dumper.hex_dump_string (data)
      ...
    end
  ...
end

или чуть в более короткой форме:

some is
  local
    hex_dump: STRING
  do
    hex_dump := (create {HEX_DUMPER}).hex_dump_string (data)
    ...
  end

Любой из этих подходов не кажется мне прагматичным. Скорее это напоминает принесение практичности в жертву стройности Метода (коим считается метод проектирования программ Eiffel).

Разделение методов по принципу Command/Query

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

Т.е. если у меня есть класс MAIL_BOX с операцией send (которая, естественно, меняет состояние объекта), то я не могу из send возвратить результат операции. Т.е. вместо привычного мне подхода:

// C++
mail_send_result_t send_result = mail_box.send( message );
if( send_result.is_ok() )
  ...

я должен использовать подход с командой send из запросом last_send_result:

-- Eiffel
mail_box.send (message)
if mail_box.last_send_result.is_ok then
  ...

Т.е. MAIL_BOX помимо своей прикладной начинки должен хранить еще и дополнительный атрибут last_send_result. Что ведет к двум не очень хорошим, как мне представляется, последствиям:

1. Усложняет класс MAIL_BOX функциональностью, которой могло бы и не быть вообще.

2. Разделение объектов в многопоточной программе усложняется. Я не успел попробовать многопоточность в Eiffel, но разделение на Command/Query, на мой взгляд, усложняет разработку разделяемых между потоками объектов. Экземпляр класса MAIL_BOX может быть в программе один (доступ к нему осуществляется через once-функцию). Но как потокам блокировать этот экземпляр для эксклюзивной работы с ним? В Java/D есть понятие synchronized метода (в С++ его нужно реализовывать самостоятельно), что упрощает как сам метод send класса MAIL_BOX, так и его использование:

class MailBox {
  public synchonized SendResult send( Message msg ) { ... }
  ...
}
...
// Где-то в каком-то потоке.
SendResult result = mail_box.send( msg );

и все. В Eiffel же придется заставлять каждого клиента MAIL_BOX соблюдать некоторую политику блокировки доступа к экземплярам MAIL_BOX. Например, самостоятельно захватывать некий семафор:

class
  MAIL_BOX

feature
  lock: THREAD_MUTEX
    -- Замок объекта.

  send (m: MESSAGE) is
    -- Отправка сообщения.
    -- Должна вызываться при захваченном замке объекта.
    do
      ...
    end

  last_send_result: SEND_RESULT is
    -- Результат последней отправки.
    -- Должна вызываться при захваченном замке объекта.
    do
      ...
    end
  ...
end
...
-- Где-то в каком-то потоке.
mail_box.lock.acquire
mail_box.send
if mail_box.last_send_result.is_ok then
  ...
end
mail_box.lock.release

Еще одним примером вызывающим сомнения в безусловной правильности разделения на Command/Query является штатный способ итерации по элементам такого контейнера, как LIST. До появления в языке агентов это был вообще единственный способ, но даже после добавления в класс LIST метода do_all (аналога Ruby-ового each) этот способ остался и используется. Выглядит итерация по содержимому объекта LIST так:

from
  list.start
until
  list.after
loop
  ...
  list.forth
end

Смысл таков: в объекте LIST есть некий курсор, который указывает на текущий элемент списка. Метод start передвигает курсор к самому первому элементу списка. Метод after возвращает True если курсор находится за самым последним элементом, а метод forth передвигает курсор к следующему элементу.

Т.е. состояние итерации хранится в самом объекте! Даже в однопоточной программе это может привести к нежелательным побочным эффектам, если внутри одной итерации будет вызван метод, инициирующий новую итерацию по этому же списку.

Отсутствие перегрузки по типам параметров

Очень ярко проявляется при организации ввода/вывода:

io.put_string ("Age: ")
io.put_integer (age)
io.put_string ("; Height: ")
io.put_real (height)
io.put_string ("; Weigh: ")
io.put_real (weight)
io.put_new_line

вместо:

// C++
std::cout << "Age: " << age << "; Height: " << height
    << "; Weight: " << weight << std::endl;

// D + Tango
Stdout.formatln( "Age: {0}; Height: {1}; Weight: {2}", age, height, weight )

// Scala
Console.println( "Age: " + age + "; Height: " + height + "; Weight: " + weight )

# Ruby
puts "Age: #{age}; Height: #{height}; Weight: #{wight}"

И сомнение вызывают не столько количество строк в Eiffel-варианте, сколько два следующих момента.

Во-первых, в Eiffel-варианте происходит дублирование информации -- ведь типы переменных age/height/weight уже были продекларированы при их объявлении. Тем не менее, эту информацию приходится повторять в именах соответствующих методов объекта io. Соответственно, при изменении типа любой из этих переменных придется производить гораздо больше изменений в Eiffel программе, нежели в C++/D/Scala/Ruby программах.

Во-вторых, я не понимаю, как с таким подходом адаптировать уже существующие схемы (ввода/вывода, например) к появлению новых типов данных. Так, в C++ оператор сдвига объекта в выходной поток является свободной функцией. Поэтому, при появлении какого-то моего нового класса, скажем, polar_coordinate_t я могу создать еще один оператор сдвига:

std::ostream &
operator<<( std::ostream & to, const polar_coordinate_t & what ) { ... }

и в C++ тип polar_coordinate_t станет по отношению к std::cout таким же родным типом, как int, float или std::string.

Но что будет в Eiffel? Точного ответа я не знаю. Может быть можно будет создать собственный тип выходного потока, который будет содержать компонент put_polar_coordinate, а затем в собственном объекте мне придется переопределить унаследованный метод io для того, чтобы он возвращал мне объект нужного мне типа с методом put_polar_coordinate.

Может быть мне придется добавить в POLAR_COORDINATE метод put_to и писать так:

io.put_string ("point a:")
a.put_to (io)
io.put_string ("point b:")
b.put_to (io)

но тогда нарушается общая схема использования объекта io.

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

Хотя, нужно признать, что некоторые методы, в частности, с процедурами инициализации вида make, make_empty, make_from_array и т.д., повышают читабельность программы.

Отсуствие возможности преждевременно покинуть метод

Т.е. отсутствие конструкции return.

С одной стороны, отсутствие return означает наличие одной точки выхода из метода. Но с другой стороны, в некоторых случаях наличие return сделало бы метод короче и более понятным. К примеру, вот так мог бы выглядеть цикл с преждевременным выходом в C++:

void
do_some()
  {
    while( true )
      {
        ...
        if( some_condition )
          return;
        ...
      }
  }

и аналогичный в Eiffel:

do_some is
  local
    completed: BOOLEAN
  do
    from
    until
      completed
    loop
      ...
      if some_condition then
        completed := True
      else
        ... -- Оставшуюся часть цикла обязательно
            -- придется писать под веткой else.
      end
    end
  end

Многословность

По моим впечатлениям, программы на Eiffel оказываются длинее аналогичных на C++, и гораздо длинее таковых на D и Ruby. Причин здесь несколько.

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

-- Eiffel
read_lines is
    -- Read lines from standard input and parses each.
  local
    input: KL_STDIN_FILE
  do
    create input.make
    from
      input.read_line
    until
      input.end_of_file
    loop
      parse_line (input.last_string)
      input.read_line
    end
  end

// D + Tango
void
readLines()
  {
    foreach( line; new LineIterator!(char)( Cin.stream ) )
      parseLine( line );
  }

Во-вторых, за счет деления методов на команды, не возвращающие результата, и возвращающие результат функции. Т.е., если в D/C++ я могу в одну строку и выполнить действие и получить ее результат:

auto send_result = mail_box.send( msg );
if( send_result.is_ok ) ...

То в Eiffel мне потребуется на это несколько больше строк:

local
  send_result: SEND_RESULT
do
  mail_box.send (msg)
  send_result := mail_box.last_send_result
  ...
end

И при написании кода на Eiffel ловишь себя на мысли, что такое происходит постоянно.

В-третьих, отсутствие return, которое заставляет писать дополнительный код, для избежания некоторых веток или прерывания циклов.

Еще один интересный эффект производит необходимость объявления локальных переменных в специальной секции. С одной стороны это так же увеличивает объем кода -- если мне нужен счетчик цикла, то в C++/D я могу объявлять его непосредственно по месту использования, а в Eiffel я должен вносить его в секцию локальных переменных. Но с другой стороны Eiffel заставляет писать короткие методы, в которых просто нет большого количества локальных переменных. Более того, именно необходимость их декларации и является одним из факторов, который заставляет в Eiffel писать короткие методы.

Как следствие, необходимость в локальных переменных в Eiffel возникает не так часто, как в C++/D. Чему способствует еще и то, что в функциях уже заготовленна специальная встроенная переменная Result, поэтому в Eiffel часто не нужны вспомогательные локальные переменные для формирования результата функции.

В итоге Eiffel программы оказываются более многословными, что плохо. Но эта многословность не сказывается на читабельности программ, временами читабельность даже выигрывает, что хорошо. Вот такая неоднозначная особенность. Хотя я бы предпочел писать меньше.

Безопасность?

Создатели языка Eiffel позиционируют его как безопасный язык. Может быть он и был таковым во времена соперничества с С++, Pascal и Ada. Однако, на данном историческом этапе, тезис о безопасности Eiffel выглядит не столь убедительным.

По сравнению с C++ Eiffel предлагает ряд механизмов, устраняющих целые классы опасных и трудно обнаруживаемых ошибок: сборка мусора, отсутствие адресной арифметики и приведений типов. Плюс Design By Contract.

Однако, практически тот же самый набор предоставляет разработчикам язык D. Да, в D есть адресная арифметика и приведения типов, но необходимость их использования в D возникает гораздо реже, да и если компилировать програмы без опции -release, то выход за границы массивов контролируется языком D. А проблемы битых, неинициализированных или повисших указателей в D не актуальны, из-за той же сборки мусора и присваивания всем неинициализированным явно переменным значений по умолчанию. Плюс Design By Contract.

Если же сравнивать D и Eiffel в release-версиях программ, когда все проверки были принесены в жертву производительности, то окажется, что Eiffel ничуть не безопаснее D. Ведь обращения к массивам по неверным индексам отлавливается в Eiffel при помощи предусловий, которые не попадают в release-версию! Что уже говорить о Java/C#, где подобные проверки даже из release-версий не изымаются.

Итак, получается, что если взять на вооружение тезис о том, что в программах всегда остаются невыявленные ошибки, то в release версиях Eiffel не более безопасен чем D, а может и C++. Но менее безопасен чем Java.

Единственным способом сохранения безопасности программы на Eiffel я вижу в сохранении проверок хотя бы предусловий даже в release-версиях. Но этот же уровень безопасности можно получить и в D, если не использовать опцию -release.

Однако, если ситуация вынуждает принести безопасность в жертву производительности, то в Eiffel есть возможность это сделать. Что хорошо.

Что не понравилось

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

Отсутствие пространств имен

Я с удовольствием пользуюсь пространствами имен в C++ уже много лет. Java и D поддерживают понятие пакета и вложенных пакетов, позволяющих при необходимости использовать уточненные имена типов для разрешения конфликтов. В Ruby есть механизм модулей, которые играют, в том числе, и роль пространств имен.

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

Если взглянуть на имена классов из библиотек Gobo Eiffel Project, то можно увидеть: AP_PARSER, KL_SHARED_ARGUMENTS, RX_PCRE_REGULAR_EXPRESSION, ET_LACE_ERROR_HANDLER, UT_ERROR_HANDLER и т.д. Что обозначают префиксы AP, KL, RX, ET, UT -- вопрос. И как Eiffel позиционируется в качестве языка для разработки больших систем большими командами, если механизмы разрешения конфликтов имен в нем находятся на таком примитивном уровне?

А для меня еще больший вопрос -- как в Eiffel создавать альтернативы, например, вот таким именам (взятым из реального проекта):

aag_3::smpp_smsc::impl::delivery_receipt::sorting_criteria_t
aag_3::smsc_map::routing::send_trx_modifications_t
so_4::disp::reuse::work_thread::demand_queue_t
so_sysconf_2::breakflag_handler::a_handler_t

и пр.?

Один вид цикла

Да, единственный цикл в Eiffel способен, в принципе, заменить как традиционный C-шный for, так и еще более традиционный универсальный while. Хотя уже реализация C-шного do-while (Pascal-евского repeat-until), когда тело цикла должно выполниться хотя бы один раз, будет не столь тривиальной.

Но более всего, после Ruby, D и Scala, напрягает отсутствие foreach для более декларативного описания обхода коллекций. Поскольку foreach не просто делает намерения разработчика более явными, но и снимает с разработчика необходимость организовать переменную цикла, проверять условие выхода и переходить к следующему элементу коллекции.

Система исключений

Восстановление после исключений

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

Единственным способом восстановиться после исключения является повторение всего тела метода с самого начала посредством инструкции retry. При этом все внутренности метода (т.е. все его локальные переменные и все задействованные в нем объекты) остаются в состоянии, предшествовавшему возникновению исключения. Поскольку исключение, в принципе, может возникнуть в любом месте, то предсказать состояние переменных невозможно. Поэтому, если метод пытается восстановиться после исключения, то код этого метода практически наверняка будет содержать либо переменную-признак восстановления после исключения:

do_something is
  local
    exception_caught: BOOLEAN
  do
    if not exception_caught then
      -- Первый проход по методу, исключений еще не было.
      ...
    else
      -- Режим восстановления после исключения.
      ...
    end
  rescue
    exception_caught := True
    retry
  end

либо счетчик повторений тела метода:

do_something is
  local
    attempts: INTEGER
  do
    ... -- Основные действия метода.
  rescue
    if attempts < Max_attempts then
      -- Можно повторять попытки.
      attemps := attempts + 1
      retry
    end
  end

Не тот, ни другой вариант не кажутся удобным. Хотя, возможно, стоит обратить внимание на то, что исключения в Eiffel используются несколько не так, как в других языках. Если, к примеру, в Ruby попытка окрыть файл завершается неудачно, то порождается исключение -- и это считается нормальным подходом для Ruby. Но это не правильный подход в Eiffel. В Eiffel если разработчик знает, что операция может завершиться неудачно, то неудачное завершение не должно приводить к порождению исключения -- то, что файл не удалось открыть -- это тоже нормальный исход операции. Т.е. там, где в Ruby было бы:

begin
  f = File.new( "myfile", "r" )
  ... # Работа с открытым файлом.
rescue SystemError => x
  ... # Обработка ошибки открытия файла.
end

в Eiffel будет:

local
  f: FILE
do
  create f.make_open_read ("myfile")
  if f.is_open_read then
    ... -- Работа с открытым файлом.
  else
    ... -- Обработка ошибки открытия файла.
  end
end

т.е. без лишних исключений. Так что, с одной стороны, исключения в Eiffel -- это гораздо более редкая штука. Но с другой стороны, мне больше нравится подход, при котором неудачные операции завершаются иключениями -- такие ситуации невозможно проигнорировать. А вот в Eiffel подходе очень легко забыть вызвать f.is_open_read чтобы проверить результат предыдущей операции.

Построение логики на исключениях

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

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

  • для возврата кода ошибки клиенту, от которого был получен проблемный объект;
  • для логирования описания ошибки для последующего анализа в случае претензий клиента.

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

На исключениях Eiffel такой подход вряд ли удалось применить. Пришлось бы создавать объект для хранения результатов проверки и передавать его от шага к шагу процедуры проверки.

Очистка ресурсов

Если два предыдущих момента еще можно было трактовать по разному (либо как достоинство Eiffel, либо как недостаток, в зависимости от предпочтений разработчика), то вот как организовать в Eiffel простую и удобную очистку ресурсов при возникновении исключения я понять не смог.

Сложность в том, что в Eiffel нет какого-либо аналога деструкторов объектов, автоматически вызываемых при выходе из области видимости (т.е. невозможно реализовать C++/D идиому RAII). А так же нет специальной секции, аналогичной finaly в D/Java или ensure в Ruby. Т.е. секции, которая вызывалась бы при выходе из метода вне зависимости от причины выхода (в результате нормального выхода или из-за исключения).

В качестве примера можно рассмотреть простую задачу: нужно подключиться к устройству (COM порт, PC/SC ридер, внешний контроллер и т.д.), проинициализировать устройство, записать в него некоторые данные и закрыть подключение. Причем подключение нужно закрывать всегда -- даже если операция завершается неудачно из-за исключения.

Лучшее, что я смог придумать:

connect_and_write_to_device (
    device_params: DEVICE_PARAMS;
    data_to_send: DATA_TO_SEND) is
  local
    device_connector: DEVICE_CONNECTOR
    device: DEVICE
  do
    create device_connector.make (device_params)
    device_connector.connect
    if device_connector.is_connected then
      create device.make (device_connector.connection)
      device.initialize
      device.write_data (data_to_send)
      device_connector.close_connection
    end
  rescue
    if device_connector /= Void and then device_connector.is_connected then
      device_connector.close_connection
    end
  end

Т.е. вызов device_connector.close_connection нужно делать дважды! Естественно, что один из них рано или поздно обязательно будет забыт.

Даже если сравнивать с менее безопасным (по версии создателя Eiffel) C++, очистка ресурсов в Eiffel выглядит более сложной и подверженной ошибкам, чем RAII в C++:

void
connect_and_write_to_device(
  const DeviceParams & device_params,
  const DataToSend & data_to_send )
{
  DeviceConnector connector( device_params );
  connector.connect();
  Device device( connector.connection() );
  device.initialize();
  device.write_data( data_to_send );
}

В D, кроме RAII и finally, есть и еще один способ очистки ресурсов - scope_exit:

void
connect_and_write_to_device(
  DeviceParams device_params,
  DataToSend data_to_send )
{
  auto connector = new DeviceConnector( device_params );
  connector.connect;
  scope(exit) connector.close_connection;

  auto device = new Device( connector.connection );
  device.initialize();
  device.write_data( data_to_send );
}

Исходя из вышесказанного вопрос очистки ресурсов в Eiffel остается открытым. А сам механизм исключений оказывается гораздо менее удобным, чем в других языках программирования.

Поклонение Eiffel

Изучая Eiffel и посвященные ему материалы лично у меня сложилось впечатление, что приверженцы Eiffel считают его Единственно Правильным Воплощением Единственно Правильного Метода разработки программ. Именно так, каждое слово с большой буквы. Это вызывает некоторое раздражение. Особенно когда в ответ на вопрос о том, что в Eiffel используется вместо C++ного typedef, получаешь вот такие вот перлы:

Congratulations on having the intellectual curiosity to discover Eiffel and on the courage to open up in a forum such as this. Listen to the advice you get here, work hard to understand _why_ we use Eiffel and you will be rewarded with a level of maturity as a software engineer that would otherwise take many many years to achieve, if ever.

Понятно, что фанатичные приверженцы есть у любого языка, но я не встречался с таким ощущением собственного превосходства у пользователей C++, D или Ruby.

Резюме

Eiffel, определенно, заставляет разработчика концентироваться на проектировании и строго следовать правилам, принятым в Eiffel. Что хорошо.

Eiffel стимулирует писать код с использованием Design By Contract. Что хорошо.

Программы на Eiffel легко читать. Что хорошо.

Eiffel проще C++, но хотя и он требует изрядного времени на изучение, освоить Eiffel, мне кажется, проще. Что хорошо.

Но все эти достоинства с лихвой перекрываются его недостатками. Если добавить сюда еще и:

  • высокую стоимость Eiffel для коммерческого использования;
  • малое количество доступных библиотек;
  • довольно примитивную, по современным меркам, IDE;
  • большое время компиляции,

то я не вижу весомых оснований предпочесть Eiffel его современным конкурентам.

© 2007-2008 Е.А. Охотников
LastChangedDate: 2008-09-12 14:29:13
e-mail

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

Hosted by uCoz