|
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 его современным конкурентам. |
|
© 2007-2008 Е.А. Охотников |
К сожалению я не силен в грамматике, поэтому если вы увидели здесь какие-либо орфографические или синтаксические ошибки, то не сочтите за труд -- сообщите мне. Ваша помощь поможет мне сделать этот текст гораздо лучше. |