Вместо введения
Добрый день, Читатели.
Хотел бы поделиться со всеми моим скромным опытом выбора языка программирования для своих проектов. Сразу хочу подчеркнуть – я выбирал язык исходя из собственных нужд, и, вполне вероятно, что ваш выбор в аналогичных условиях может быть другим. Все же я искренне надеюсь, что эта статья будет полезной, так как в ней достаточно подробно и аргументировано проводится сравнение D с C++ и C#, а так же упоминаются свыше десяти различных языков, принадлежащих к различным классам и реализующих различные парадигмы. Сам D разрабатывается как высокоуровневый язык для системного и прикладного программирования.
Так же следует отметить, что существуют две версии D: D1 и D2. D1 стабилен, его обновления затрагивают только исправления ошибок, а в D2 в настоящее время добавляются новые возможности. Так как D2 рекомендован для новых проектов, в статье будет рассматриваться именно он, если не указанно обратное.
Об области применения языков программирования вообще
Итак, приступим. Прежде всего хочу заметить, что выбор языка программирования в первую очередь зависит от круга задач – C/C++/Java/C# скорее всего не сможет заменить JavaScript для веб-страниц или SQL для реляционных баз данных. Хотя тут, пожалуй, нужно сделать оговорку: в принципе можно сделать компилятор или написать библиотеку классов, которые бы по заданным правилам преобразовывали один язык в другой. Так, например, поступила Microsoft при разработке платформы ASP.NET – скрипты для браузера могут быть написаны на любом языке платформы .NET (например, на C#) и затем автоматически преобразованы в соответствующий код на JavaScript. Впрочем, лично я проверял данный функционал только для относительно простых примеров генерации валидаторов форм для браузеров, так что возможно, что для какого-то нестандартного случая придется применять именно JavaScript. С другой стороны, учитывая количество свободно распространяемых библиотек и фреймворков (например, jQuery, MooTools, Prototype и др.), протестированных со всеми современными браузерами, возможно, не стоит изобретать велосипед. И раз уж зашла речь о применимости языков для различных технологий, то, например, можно выбрать функциональный язык программирования, скажем Haskell, для написания операционной системы, но выбор JavaScript в качестве системного языка программирования вероятно будет неразумным. К слову, одна ОС на Haskell’ле уже написана, называется House. Ну а на SQL прикладную программу написать не возможно, так как он не обладает полнотой по Тьюрингу. Transact-SQL уже обладает такой полнотой, но поддерживается только для Microsoft SQL Server и Sybase ASE, так что и этот язык не приспособлен для написания прикладных программ.
Второе – это, конечно же, выбор менеджера проекта. Есть мнение, что C++ настолько популярен только потому, что был популярным лет 5 назад. Другими словами, при выборе языка программирования менеджер проекта скорее всего примет решение в пользу более известного языка (те же Java и C++), чем менее известного (например, Haskell или Common Lisp), даже если последний подходит лучше именно для данного проекта. Спрашивается, почему? Философия очень простая: если менеджер завалит проект на C++, то может попробовать оправдаться тем, что ежегодно на C++ умирают сотни проектов. А с Haskell’ем так не получится, так как в этом случае менеджер сам настоял на использовании сравнительно редко используемой технологии. С моей точки зрения это вообще не оправдание, так как менеджер должен предложить наиболее подходящий язык программирования для данной проблемной области, а не руководствоваться какими-то возможными оправданиями из-за популярности языка. Конечно, это не значит, что нужно программировать на заброшенных и никому не нужных языках. Просто хочу подчеркнуть, что к выбору языка нужно подходить творчески. Например, для решения задач компьютерной алгебры может подойти какой-то из функциональных языков (Common Lisp, Scheme, Haskell и др.), так как математические формулы на таких языках выглядят в более привычном, «математическом» виде, чем на императивных языках. Вот пример кода на Haskell’ле для вычисления факториала в виде рекуррентной формулы:
factorial :: Integer -> Integer factorial 0 = 1 factorial n | n > 0 = n * factorial (n - 1)
Не правда ли, очень похоже на формулы из учебника? Собственно, свободная система компьютерной алгебры Maxima написана на Common Lisp’е и по своим возможностям сравнима с Maple и Mathematica.
Говоря все это, я лишь хочу подчеркнуть, что выбор языка действительно важен. Не все языки равны между собой, среди них есть более равные. Не верите? Вот пример программы, печатающей «Hello World!», на языке Brainfuck:
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++ .>+.+++++++..+++.>++.<<+++++++++++++++.>.+++. ------.--------.>+.>.
Сразу хочу пояснить – этот содержащий всего 8 команд эзотерический язык был создан Урбаном Мюллером забавы ради, но, тем не менее, является полным по Тьюрингу, а, значит, позволяет написать то, что не может сделать SQL. Вот только спрашивается: кому это нужно, когда есть десятки более достойных кандидатов? Возможно, именно эту идею хотел подчеркнуть Мюллер, давая языку название, созвучное с известным английским ругательством. Другое возможное объяснение – дать языку запоминающееся имя, чтобы хоть как-то увеличить его популярность.
И наконец, на выбор языка могут повлиять ваши собственные предпочтения. Ну, это если вдруг первых двух ограничений будет недостаточно.
Далее речь пойдет о выборе языка в первую очередь для прикладного программирования. Итак, чем же так привлекателен D? На самом деле каждый должен отвечать на этот вопрос сам, я же просто постараюсь описать, какими именно факторами руководствовался при его выборе. При этом чтобы понять, какой именно язык тебе нужен, следует разобраться, какие недостатки есть у существующих языков и определиться со списком возможных кандидатов.
Немного о недостатках C#
Начнем по порядку. Почему бы не выбрать C# в качестве языка для прикладного программирования, ведь именно для этого он и предназначен? Основная проблема, которая для меня крайне важна, это компиляция в промежуточный язык Common Intermediate Language (CIL), что делает декомпиляцию тривиальной задачей. Больше того, для этого существуют специальные инструменты. Например, .NET Reflector без ошибок генерирует исходный код на C#/VB.NET/F#, включая сохранение оригинальных названий переменных, функций и классов. Полученный исходный код отличается от авторского разве что отсутствием комментариев. Любители поэкспериментировать могут скачать trial-версию программы с официального сайта.
Итак, если для анализа скомпилированной программы на том же C++ необходимы достаточно глубокие знания, включая умение работы с дизассемблером, то с C# получение исходного кода – вопрос пары минут. Конечно, приверженцам Open Source всё это неважно, но тут тоже можно возразить: если я захочу сделать программу свободной, я опубликую её исходные коды, в противном случае у меня есть основания оставить программу закрытой. В конце концов, далеко не всё ПО – свободное. Тут нужно заметить, что существуют сторонние программы-обфускаторы, способные до неузнаваемости изменить исходный код программы различными дополнительными инструкциями, изменением внутренних интерфейсов и имен переменных, генерированием дополнительных исключительных ситуаций, шифрованием кода и т.п. Всё это позволяет сильно усложнить анализ программы на C#. Из существенных недостатков обфускаторов можно отметить: генерацию более медленного кода, чем исходный, потенциальную возможность внесения ошибки в программу и возможность взлома самого обфускатора (после чего вся его защита становится никому не нужной). Задумайтесь на мгновение: люди годами улучшали компиляторы, пытаясь создать все более качественные и оптимизирующие средства разработки, а тут происходит прямо какая-то регрессия.
Впрочем, вопрос важности компиляции именно в двоичные коды зависит от области применения – если программу не планируется применять за пределами компании-разработчика, то, скорее всего, опасность декомпиляции не будет решающим фактором при выборе языка программирования. И чтобы подвести черту, хотел бы добавить: в стандартной поставке компилятора C# от Microsoft существует инструмент для компиляции программ в двоичные коды, но без исходных кодов на CIL программа все равно работать не будет. Так что для защиты от взлома его использовать невозможно.
Второе, о чём стоит задуматься перед использованием C#, это переносимость полученного кода. Разрешите привести цитату Oktal’а: «Я думаю, что Microsoft назвал технологию .Net для того, чтобы она не показывалась в списках директорий Unix». Не знаю, прав ли автор, но из C# очень легко вызвать функции WinAPI, объекты COM и другие компоненты, которые сделают программу непереносимой. Даже без применения специфичных только для Windows компонентов программа сама по себе не будет запускаться в Unix-системах – для запуска потребуется Mono. Да, конечно же, Mono – это широко известный продукт, но при его использовании проблемы всё же могут появиться. Первое, о чем следует помнить – разработчики Mono будут всегда на шаг позади Microsoft, так как они начинают реализовывать уже выпущенные библиотеки и стандарты. Второе: а им случайно не надоест? Да, я прекрасно понимаю, что это – проект со свободной лицензией, который развивает сообщество, но риск прекращения поддержки все равно существует. Это не повлияет на существующие приложения, но новые программы скорее всего станут непереносимы из-за отсутствия поддержки новых библиотек. Третье, о чем следует вспомнить – отсутствие стандартов на компоненты WinForms, ADO.NET и ASP.NET со стороны Microsoft. Их использование может повлечь за собой юридические претензии со стороны Microsoft, поэтому не рекомендуется использовать их совместно с Mono. Последнее означает, что прекрасный компонент ASP.NET, служащий для построения сайтов различных масштабов, не имеет ровным счетом никаких преимуществ перед тем же PHP. Ну а если вдруг вам все же захочется использовать его в своих проектах, будьте морально готовы к покупке лицензий на серверную версию Windows.
И, наконец, третье, о чем стоить себя спросить: а будет ли Microsoft дальше развивать этот язык? В настоящее время все патенты на C#/CLI принадлежат Microsoft, и отказ от поддержки с её стороны будет означать смерть этому языку. Лично я считаю, что C# будет продолжать развиваться – Microsoft слишком много вложила в этот язык. Но гарантий никто дать не может: в свое время Microsoft отказалась от поддержки Visual Basic, создав вместо него совершенно новый язык Visual Basic .NET без обратной совместимости со своим предшественником. В результате это привело к трагедии для тысяч программистов, работающих на Basic’ке. К тому же, по сообщениям в прессе, в 2015-2016 году Microsoft планирует отказаться от бренда «Windows», создав новую ОС для планшетов, смартфонов, компьютеров, консолей, телевизоров и других устройств, а, значит, языки для разработки под эту ОС тоже могут кануть в небытие.
Лично для меня этих доводов достаточно, чтобы отказаться от использования C#. Не поймите меня неверно: C# – это прекрасный современный язык, но подходит он в первую очередь только для Windows, и то не во всех случаях. Не много подумав, можно прийти к выводу, что использование платформы .NET так же опасно, как и C#. Поэтому лично моё мнение – не стоит писать на языках, ориентированных в первую очередь на эту платформу. Например, Nemerle – хороший язык, в чем-то превосходящий C#, главная особенность которого – развитая система метапрограммирования, и сочетающий в себе возможности функционального и объектно-ориентированного программирования. Но дизайн языка ориентирован в первую очередь на платформу .NET, что ставит под вопрос возможность применения его в ряде проектов. F# – прекрасный пример функционального языка программирования для платформы .NET, в чём-то похожий на Haskell, может подойти для разработки математически-ориентированных систем. Но, опять-таки, платформа .NET ограничивает возможность его применения.
Уважаемые приверженцы C# и платформы .NET! Я прекрасно знаю, что у данных технологий много плюсов, и, в первую очередь, это огромная библиотека классов, с помощью которой можно сделать (почти) всё что угодно. К сожалению, подробный обзор преимуществ платформы .NET потребует написания статьи примерно такого же объёма, поэтому прошу меня извинить за заведомо неполное освящение этой технологии и её возможностей.
Немного о недостатках C++
А по чему бы не выбрать C++? Конечно же, это промышленный стандарт, про который слышали все. Но на самом деле у этого языка есть ряд существенных недостатков, некоторые из которых я попробую рассмотреть. Первое, что приходит в голову – этот язык сложен. Точнее, очень сложен. По сравнению с ним работа на C# начинает казаться детской забавой. Судите сами: стандарт только на C занимает около 500 станиц, C++ – около 800, C++11 – около 1300. Если сравнить объем технической документации – этот язык по сложности явно превосходит миксер, швейную машинку и автомобиль, приближаясь скорее к самолётам. Для сравнения, стандарт C# 4.0 занимает всего 505 страниц. В этот момент хочется вспомнить цитату Алана Кёртиса Кэйя (Alan Curtis Kay): «Я изобрел понятие «объектно-ориентированный», но могу заявить, что не имел в виду C++ при этом». В противовес этому можно, конечно, вспомнить создателя C++ Бьерна Страуструпа (Bjarne Stroustrup): «Существуют только два вида языков: те, на которые все жалуются и те, которые никто не использует», но эта цитата звучит скорее как оправдание. D в этом смысле достаточно удобный язык – его дизайн был спроектирован в первую очередь на основе C# и Java. Объём спецификации D1 составляет 223 страницы. Спецификация для D2 поставляется виде html-страниц вместе с компилятором, которая так же доступна на официальном сайте www.d-programming-language.org. Кроме того, есть книга Андрея Александреску (Andrei Alexandrescu) «The D Programming Language», которая фактически является описанием стандарта D2 (объем – 492 страницы, в настоящий момент переводится на русский язык). Так вот, сложность самого языка отнюдь не облегчает программирование на нём. В D всё сделано проще, разумней и понятней.
Безопасность прежде всего
Следующий серьёзный минус C++ – плохая проверка кода во время компиляции на предмет ошибок. Другими словами, на C++ очень легко написать код, который будет скомпилирован, но не будет работать верно. Оставим составление полного перечня таких спорных возможностей языка специалистам, ограничившись лишь оператором получения элемента массива operator[] (операция индексирования). То, что этот язык не проверяет границы элементов массива, думаю, ни для кого не секрет. Однако, несмотря на это, ошибки переполнения буфера были, есть и будут появляться в программах на C и C++. И хотя эти ошибки допускают программисты, я считаю, что причина именно в языке, который способствует их появлению. Например, C# всегда делает проверки выхода за пределы массива, что делает подобные ошибки гораздо менее вероятными, но теряя при этом в производительности (переполнение буфера всё ещё потенциально возможно из-за возможных ошибок в реализации компилятора или стандартных библиотек). Библиотека STL решает многие проблемы, но это верно только при её правильном использовании. Например, шаблонный класс vector не проверяет выход за свои границы при использовании операции индексирования (operator[]), для проверки границ необходимо использовать функцию at (Х.М. Дейтел, П.Дж. Дейтел, «Как программировать на C++»). Другими словами, STL – это не панацея.
На самом деле ошибки переполнения буфера – проблема более серьёзная, чем кажется на первый взгляд. Знаменитый Червь Морриса, парализовавший интернет в 1988 году, использовал именно этот тип ошибки (ущерб оценивается в 96 миллионов долларов). Еще один пример: при использовании техники Возвратно-ориентированного программирования (Return-oriented programming) достаточно внедрения всего 2-3 инструкций ассемблера для последующего взлома системы. Другими словами, переполнение буфера даже на один байт может представлять собой уязвимость. Разрешите привести цитату экспертов по безопасности Майкла Ховарда и Дэвида Лебланка из книги «Защищенный код»: «В процессе многих кампаний по безопасности в Microsoft мы настойчиво пропагандировали выявление и при необходимости перенос опасных компонентов, написанных на C или C++, на C# или другой управляемый язык. Это не означает, что код автоматически станет безопасным, но некоторые классы нападений – скорее всего атаки с переполнением буфера – станет намного труднее эксплуатировать. Это же верно по отношению к DoS-атакам на серверы, которые возможны из-за утечки памяти и других ресурсов». Задумайтесь на мгновение: предлагается использовать более медленный, менее функциональный и легко поддающийся дизассемблированию язык только из-за наличия встроенной проверки границ массивов и сборщика мусора.
Тут нужно подчеркнуть разницу между DoS-атакой (Denial of Service, Отказ от обслуживания) и DDoS-атакой (Distributed Denial of Service, Распределённый отказ от обслуживания). DoS-атака, как правило, использует «умные» уловки и уязвимости в сервисе и проводится с небольшого числа компьютеров (возможно, даже с одного). Например, как писалось выше, DoS-атака может быть основана на утечке памяти. Другой пример – атака на файловый сервер, в котором все загружаемые файлы проверяются на наличие вирусов. Сначала создаётся файл размером несколько гигабайт, состоящий из одних нулей, архивируется (размер в архиве – несколько килобайт), закачивается на атакуемый сервер, после чего сервер его распаковывает и пытается проверить антивирусом… DDoS-атакой является попытка вызвать отказ в обслуживании просто за счет перегрузки сервиса таким количеством запросов, на которые он заведомо не рассчитан. Другими словами, это – атака «в лоб», проводящаяся сразу с большого количества машин. Несмотря на то, что существует ряд способов защиты от DDoS-атак, от правильно организованной DDoS-атаки защититься на все 100% невозможно, поэтому становится крайне важной производительность самой службы: чем больше запросов мы можем обработать, тем меньшими будут последствия атаки. В результате этого возникает парадокс: C# за счет лучшего контроля ресурсов позволяет заметно усложнить проведение DoS-атаки, но делает службу более уязвимой к DDoS-атаке из-за более низкой скорости работы.
Но вернёмся к проверке границ массивов в C++. Я прекрасно понимаю, что это – системный язык программирования, и любые дополнительные издержки могут сделать программу в конечном итоге недостаточно производительной. Однако следует подчеркнуть, что критерий производительности важен лишь для конечной realize-версии продукта. Версия для отладки предназначена для работы в узком кругу разработчиков программы на тестовых примерах, следовательно, может (и должна) содержать разнообразные проверки и отладочные сообщения, способные повысить её качество и снизить вероятность ошибки (подробнее смотрите в книге Стива Макконнелла «Совершенный код»). Поэтому меня огорчает отсутствие в стандарте C++11 подобных средств хотя бы для debug-версии программы. Однако следует отметить, что существуют сторонние продукты для защиты стека от изменений с целью обнаружения ошибок переполнения в нём, например, расширения компилятора gcc Stackguard и Stack-Smashing Protector (старое название — ProPolice), компиляторы Microsoft Visual Studio и IBM Compiler, которые способны значительно усложнить эксплуатацию уязвимостей. Но потенциальная возможность взлома системы всё же остаётся. К тому же кроме переполнения в стеке возможно переполнение в куче, которое является ничуть не менее опасным. Вывод: единственный надежный способ защититься от взлома – писать правильный код.
Приятно осознавать то, что D выучил урок: в нём есть встроенная поддержка проверки границ массивов для отладочной версии программы, которая отключается при компилировании оптимизированной версии. Другими словами, D сочетает в себе качества двух миров – идеальный выбор для языка системного и прикладного программирования. Но возможности по обеспечению безопасности в D на этом только начинаются. Было бы просто неправильным не упомянуть SafeD – подмножество языка D, запрещающее потенциально опасные операции с памятью, такие как арифметические операции над указателями. Это позволяет гарантировать, что память останется неповрежденной. В сочетании со встроенным сборщиком мусора язык приобретает черты, характерные для C#: ошибки переполнения буфера и DoS-атаки станут вас беспокоить гораздо реже. Разрешите немного перефразировать совет Ховарда и Лебланка: пишите потенциально опасные фрагменты программы на D, и наслаждайтесь безопасным, эффективным и компилируемым языком.
Возможность вызова кода на C/C++ – промышленный стандарт
Пришло время упомянуть еще одну важную черту D: он полностью совместим с кодом C/C++ на уровне объектных файлов, что позволяет получить прямой доступ к функциям и классам, написанным на C/C++ и наоборот. Фактически, стандартная библиотека C является частью стандарта D, правда вместо неё лучше использовать соответствующие функции библиотеки D. Это сделано просто потому, что никто не собирается переписывать тонны C++ кода на D – просто вооружитесь своим любимым компилятором C++ – и готово. Для сравнения, вызов управляемого кода, написанного, например, на C#, из неуправляемого, написанного, например, на C++, возможен через COM-объекты, что с моей точки зрения сложнее, чем просто линковка. Правда тут необходимо отметить, что стандарт C++/CLI добавляет расширенные способы взаимодействия C++ с управляемым кодом, однако это означает использование только компилятора Visual C++ от Microsoft.
Сборка мусора против деструктора
Тему сборки мусора следует осветить подробней. Мне не раз приходилось слышать фразу: «если в язык добавили сборку мусора, значит испортили производительность». Сразу хочется спросить у таких людей: «а тесты вы делали?». Будучи системным языком программирования, D позволяет применять ручное управление памятью (правда, в рамки подмножества SafeD эта возможность уже не входит). В вашем инструментарии появятся перегрузка операторов new и delete и C-стиль управления памятью функциями malloc и free. Кроме того, как правило, быстродействие можно повысить на 90% за счет изменения всего лишь 10% кода (по материалам книги Мартина Фаулера «Рефакторинг. Улучшение существующего кода»). Другими словами, большая часть программы может быть написана с помощью инструкций, безопасно работающих с ресурсами, а небольшой критичный к быстродействию фрагмент может быть тщательно проверен. При этом важно осознавать, что используется один и тот же язык программирования, что упрощает создание, поддержку и сопровождение кода. Кроме того, в больших программах сборка мусора ускоряет разработку, так как программист на C++ тратит много времени (по некоторым источникам – до 50%) на управление памятью. Так что вот вам рекомендация: пишите код, а вопросы производительности оставьте профайлеру, который поможет вам выявить проблемный с точки зрения производительности фрагмент кода.
Так же существует ряд алгоритмов, которые просто не могут работать правильно без сборки мусора. Разрешите привести простейший пример: у вас есть абстрактный класс Number, символизирующий какое-то число, и его подклассы: Integer, Long, BigInt, Float, Double, Real, Complex и т.д. Теперь представьте, что где-то в вашей программе появляется примерно такая строчка:
Number* c = a + b;
где a и b – указатели на Number, т.е. фактический тип переменных не известен. При этом предполагается, что если a и b – Long, то в результате получим Long или BigInt (чтобы избежать ошибок переполнения), если Integer и Complex, то Complex, если Double и Double, то Integer, Long, Float или Double (в зависимости от полученного числа знаков после запятой, например 0,5 + 0,5 = 1) и т.д. Скажите, как правильно реализовать функцию operator+(Number* a, Number* b)? Подробный анализ реализаций выходит за рамки этой статьи, но все желающие могут ознакомиться с несколькими возможностями в книге Джеффа Элджера «C++ для настоящих программистов», в главах, посвящённых множественной передаче (и двойной передаче в частности). Хочу лишь отметить, что функция operator+ должна будет создать объект в куче с помощью оператора new, так как вызывающий её код не может ничего знать о фактическом типе объекта c, и поэтому не может выделить память в стеке. Как следствие, нам нужен механизм сборки мусора для освобождения памяти. В данном случае можно применить умные указатели с подсчётом ссылок, но они имеют принципиальные ограничения. Другими словами, встроенной в язык сборки мусора ничто не заменит, и то, что в стандарте C++11 указана поддержка лишь его базовых функций (реализация расширенной сборки мусора в стандарт не вошла), меня отнюдь не радует. Под конец хочется вспомнить цитату Френсиса Бэкона: «Тот, кто не хочет прибегать к новым средствам, должен ожидать новых бед». Да, если вдруг вас заинтересовал предыдущий пример с числами, рекомендую так же обратить внимание на CLOS – объектную систему Common Lisp’а, в которой уже имеется встроенная поддержка множественной диспетчеризации.
Деструктор против сборки мусора
Следующее, о чём нужно знать – в D есть RAII (получение ресурса есть инициализация) – характерная для C++ модель управления ресурсами, но отсутствующая в C# и Java. Другими словами, в D есть деструктор, и вызывается он независимо от сборщика мусора. Как следствие, эта парадигма облегчает управление ресурсами (в первую очередь – их освобождение).
Как известно, C# и Java отказались от этой модели, заменив деструкторы завершителями, которые вызываются уже после сборки мусора. Последнее делает невозможным использовать завершители для освобождения критичных для системы ресурсов, так как вызов завершителя может произойти через длительный промежуток времени. Следовательно, освобождать ресурсы нужно явно. «А как же сокрытие информации? Ведь я теперь должен помнить, какие классы требуют явного освобождения ресурсов, а какие – нет! Именно здесь появляются ошибки», – спросите вы, и будете правы. В ответ на это остается разве что развести руками и ответить, что оно было в C++ и есть в D. Правда, в случае неверно написанной программы освобождение ресурсов «неизвестно когда» всё же лучше, чем «никогда», так что однозначный выбор между моделями управления ресурсов в C++ и C#/Java сделать сложно. Возможно, именно поэтому между приверженцами C++ и Java возникает столько споров.
Так же в C# возможна ситуация, когда в результате пропуска кода, освобождающего ресурсы явно, вызов завершителя не сможет освободить ресурсы верно (произойдет потеря данных, генерация исключения и т.п.). Например, рассмотрим класс FileStream, являющийся подклассом от Stream и позволяющий писать двоичные данные в файловый поток, и класс StreamWriter, позволяющий писать произвольные данные (например, строки, различные числа, и т.д.) в различные двоичные потоки (в любой из подклассов Stream, например, в FileStream, MemoryStream, NetworkStream и т.д.). В результате мы можем писать произвольные данные в любые потоки, что фактически реализует паттерн проектирования Мост (Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес, «Приемы объектно-ориентированного проектирования. Паттерны проектирования»). Теперь представьте, что во всех этих классах с целью повышения производительности есть встроенные буферы, и данные отправляются на запись только после заполнения буфера (именно так реализованы эти классы в .NET). Теперь добавим ко всему этому ошибку в программе: программист забыл явно освободить ресурсы. После сборки мусора, когда объекты классов FileStream и StreamWriter больше не будут использоваться, произойдёт вызов их завершителей. Но порядок выполнения завершителей в .NET не определён! Это означает, что примерно в 50% случаев завершитель объекта FileStream закроет файл до того, как завершитель объекта StreamWriter финализирует свой буфер. Что теперь должен сделать завершитель объекта StreamWriter, ведь ему просто некуда писать данные? Есть две очевидных возможности: просто проигнорировать, что приведёт к потере данных, или сгенерировать исключение, что приведет к потере данных и аварийному завершению программы (так как данное исключение останется необработанным). С моей точки зрения генерация исключения предпочтительней, так как она упростит отладку программы, но программисты Microsoft со мной не согласны: в своей реализации данных классов они предпочли молчаливую потерю данных. Больше того, для совершения подобной ошибки необязателен пропуск явного освобождения ресурсов, достаточно лишь освободить ресурсы объекта типа FileStream до освобождения ресурсов объекта типа StreamWriter… В такие моменты начинаешь тосковать по правилу C++: «вызовы деструкторов выполняются в порядке, обратном вызовам конструкторов». Вывод: одного лишь сборщика мусора или RAII недостаточно для эффективного управления ресурсами, именно поэтому D поддерживает обе этих технологии.
Вместо заключения
К сожалению, в этой статье не хватило места для обзора ряда особенностей D. Однако хотелось бы пусть и бегло, но всё же их перечислить:
- Эффективная модель создания многопоточных программ, значительно превосходящая C++ в плане безопасности
- Мощнейшая система метапрограммирования, сильно упрощающая создание нетривиальных шаблонов. Сравнима по функциональности с макросами в языке Nemerle (которые ничего общего с макрокомандами C/C++ не имеют).
- Константность и неизменяемость переменных
- Вычисления на этапе компиляции
- Наличие черт, характерных для функциональных языков. В частности, в языке есть чистые функции, ленивые вычисления, лямбды, замыкания, функции высших порядков и т.д.
- Наличие возможности запрета наследования классов и переопределения методов, примерно так же, как в C#/Java
- Контрактное программирование, способное сильно упростить отладку программ и помочь компилятору лучше оптимизировать код. То, что таких возможностей нет в стандартах C++ и C#, меня огорчает.
- Встроенная поддержка unit-тестирования, способная помочь при проведении рефакторинга и сопровождении программы
- Поддержка понятия «модуль» как единицы сборки программы, присутствующая в Ada, C# и Java, но не вошедшая в стандарт C++11
- Перегрузка операторов, которую использовать проще, чем в C++
- Поддержка минус бесконечности, бесконечности и NaN (не число) для числовых типов, добавленных в стандарт C99, но не включенных в C++
- Поддержка кросплатформенности программ (стандартная библиотека D сделана переносимой)
- Очень быстрая компиляция программ, особенно для больших проектов
- Генератор документации встроен в компилятор (как, например, и в C#)
- Профайлер встроен в компилятор, что упрощает поиск «узких» мест в программе
- Анализ покрытия кода встроен в компилятор, что упрощает отладку программы
- и многое другое…
Что еще можно почитать?
- Официальный сайт языка
- Официальный сайт разработчика
- Книга Андрея Александреску (Andrei Alexandrescu) «The D Programming Language»
- Описание языка в Википедии
- Описание языка в английской Википедии (находится в более актуальном состоянии)
- Перевод статьи Андрея Александреску «Место для D»
- Статья «Место для D» и список часто задаваемых вопросов
- Статья с беглым описанием языка
- Статья с описанием метапрограммирования и вычислений на этапе компиляции
- Статья с более подробным описанием метапрограммирования
- Статья с примерами перегрузки операторов
- Статья с описанием чистых функций
- Англоязычная вики, посвящённая D
- Список IDE и редакторов с поддержкой D. Могу порекомендовать Mono-D, но так же следует обратить внимание на D-IDE, Poseidon и Visual D.
- Список отладчиков для D (фактически подойдёт любой отладчик с поддержкой C++)
- Список open source проектов, написанных на D (там же можно найти ряд дополнительных библиотек)
Дополнительная литература
- Стив Макконнелл, «Совершенный код»
- Майкл Ховард и Дэвид Лебланк, «Защищенный код»
- Майкл Ховард, Дэвид Лебланк и Джон Виега, «19 смертных грехов, угрожающих безопасности программ. Как не допустить типичных ошибок»
- Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес, «Приемы объектно-ориентированного проектирования. Паттерны проектирования»
- Мартин Фаулер, «Рефакторинг. Улучшение существующего кода»
- Х.М. Дейтел, П.Дж. Дейтел, «Как программировать на C++»
- Джефф Элджер, «C++ для настоящих программистов»
Под конец хочется пожелать всем успехов в программировании и в разумном подборе языка для своих будущих программ.
Автор: Vseslav