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