Язык программирования D: различия между версиями

Материал из Викиучебника — открытых книг для открытого мира
Содержимое удалено Содержимое добавлено
Строка 325: Строка 325:


Компиляторы языка D не генерируют предупреждений при встрече неоднозначного кода. Код может быть понятным компилятору или непонятным, это избавляет от необходимости решать, какие предупреждения относятся к ошибкам программы, а какие — нет. Использование предупреждений компилятора является признаком плохого дизайна языка.
Компиляторы языка D не генерируют предупреждений при встрече неоднозначного кода. Код может быть понятным компилятору или непонятным, это избавляет от необходимости решать, какие предупреждения относятся к ошибкам программы, а какие — нет. Использование предупреждений компилятора является признаком плохого дизайна языка.

[[Категория:Программирование]]
[[Категория:Языки программирования]]
[[Категория:Объектно-ориентированное программирование]]

[[en:D Programming]]

Версия от 10:41, 23 сентября 2006

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

D не является ни языком скриптования, ни интерпретируемым языком. Он не требует наличия виртуальной машины, не навязывает новой философии программирования. D позиционируется, как практичный язык для практичных программистов, которым важно сделать работу быстро, надежно, сохранив гибкость и понятность кода.

В D собран опыт разработки десятков компиляторов для самых разнообразных языков программирования, а также попыток создания крупных проектов на этих языках. Язык D вобрал в себя лучшее из тех языков (больше всего из C++) и практично применил эти возможности.

Почему D?

И действительно, почему? Кому нужен другой язык программирования?

Индустрия программного обеспечения прошла долгий путь с тех пор, как изобрели язык C. Много новых концепций было реализовано в языке C++, но его обратная совместимость со своим предком заставила унаследовать не только достоинства, но и недостатки языка C. Тем временем, и в C и в C++ реализуются новые возможности, которые опять же были внедрены в существующую структуру языков таким образом, чтобы не было необходимости переписывать старый код. Результат получился довольно неоднозначный и запутанный — стандарт C насчитывает 500 страниц, а стандарт C++ — около 750 страниц! C++ является сложным и слишком разнообразным для его полной реализации, в результате чего существует множество его реализаций, что лишает возможности писать переносимый код.

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

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

Основные особенности D

  • Облегчает написание кода, который без особых усилий может быть перенесен от компилятора к компилятору, от компьютера к компьютеру, от одной операционной системы к другой;
  • Поддержка нескольких подходов к программированию: структурный и объектно-ориентированный, как минимум;
  • Легкость изучения языка для тех, кто имел дело с языками C и C++;
  • Обеспечение прямого низкоуровневого доступа к оборудованию;
  • Наличие контекстно-независимой грамматики;
  • Легкость создания интернациональных программ;
  • Возможность создания легковесных и самостоятельных программ.

Возможности, унаследованные от C/C++

Общий синтаксис языка D схож с синтаксисом C и C++. Это делает его легким его изучение и портирование кода. Переход от C/C++ к D должен проходить без проблем. Программисту не придется привыкать к другому подходу написания кода.

Для того чтобы использовать язык D, программисту не обязательно ограничивать себя специализированной виртуальной машиной (VM), как это сделано с Java и Smalltalk. Для D не существует виртуальной машины. Программы, написанные на D, компилируются в файлы объектов, которые затем могут быть объединены линкером в исполняемый файл. D взаимодействует с операционной системой так, как это делает C. Знакомые инструменты, например make, подойдут и для разработки на языке D.

  • Сохранены синтаксис и семантика языков C и C++. D использует те же формы выражений и общий план программ;
  • Программы на D могут быть написаны с использованием функционального и объектно-ориентированного подходов, а также с использованием шаблонов (template metaprogramming) или любой комбинации этих трех подходов;
  • Поддерживается модель разработки по этапам компиляция/линкование/отладка (compile/link/debug), но ничто не мешает откомпилировать код D в байт-код и интерпретировать его;
  • Обработка исключений (exception handling). Опыт использования обработки исключений показывает, что существует более совершенный стиль обработки ошибок, чем традиционное для языка C использование кодов ошибок и переменной errno;
  • Приведение типов в реальном времени. Отчасти это было реализовано и в C++. Способ приведения типов в языке D позволяет улучшить процесс сбора мусора и облегчить процесс отладки;
  • D поддерживает совместимость с соглашением о вызове функций в языке C, что делает возможным прямой доступ из кода на языке D напрямую к программному интерфейсу операционной системы. В работе с API в D могут быть использованы уже имеющиеся знания и опыт;
  • Перегрузка операторов. В языке D можно перегружать операторы с целью расширения списка операций над пользовательскими данными;
  • Использование шаблонов. Шаблоны дают возможность создавать обобщенные функции, оперирующие независящими от типа данных переменными. В языке D шаблоны лишены недостатков шаблонов из языка C++.

Чего нет в языке D

  • Совместимость с исходным кодом языка C. Уже существует языки программирования, совместимые с исходным кодом, написанным на языке C (C++ и ObjectiveC). Дальнейшая работа в этом направлении препятствует реализации существенных возможностей;
  • Препроцессор. Для расширения языка удобно использовать макросы. Компиляция с условиями (#if, #elif, #ifdef), включения кода (#include), макросы (#define), конкатенация строк по существу формируют дополнительный язык, не связанный синтаксисом с основным языком программирования. Препроцессор в C/C++ является довольно примитивным макроязыком. Самое время сделать шаг назад и посмотреть, для чего используется препроцессор, а затем внедрить поддержку этих возможностей в собственно язык программирования;
  • Множественное наследование. Это запутанная возможность сомнительной полезности. Множественное наследование может быть заменено обычным наследованием с использованием интерфейсов и агрегированием;
  • Пространства имен (namespace). Пространства имен были попыткой решить проблему, возникающую при объединении разработанных независимо друг от друга кусков кода, когда пересекаются имена переменных, типов данных и так далее. Модульный подход выглядит проще и удобнее для использования;
  • Файлы включения кода (include files). Главной причиной медленной работы компиляторов заключается в том, что в каждом модуле исходного кода необходимо обработать огромное количество заголовочных файлов. Включение исходного кода целесообразнее реализовать в виде импортирования таблицы символов;
  • Не виртуальные функции-члены классов. В языке C++ разработчик класса должен наперед решить, виртуальной будет функция-член или нет. Достаточно трудно отловить появившиеся ошибки, когда разработчик забывает объявить в базовом классе виртуальной функцию-член, которая переопределяется в производном классе. Более гибким решением является автоматическое объявление всех функций-членов классов виртуальными, а компилятор уже сам будет конвертировать функцию в не виртуальную, если она не переопределяется в производных классах;
  • Битовые поля (bit fields) произвольного размера. Битовые поля сложны, неэффективны и достаточно редко используются;
  • Поддержка 16-битных компьютеров. В языке D нет никаких решений для генерации качественного 16-битного кода, зато возможен плавный переход с использования 32-битного плоского пространства памяти на 64-битную архитектуру;
  • Взаимная зависимость проходов компилирования (compiler passes). В языке C++, успешная обработка исходного кода основывается на таблице символов (symbol table) и различных командах препроцессора. Это делает невозможным предварительную обработку кода и значительно усложняет работу анализаторов кода;
  • Различие между операторами . и ->. В их различии нет необходимости, поскольку оператор . в языке D служит также для разыменования указателей.

Для кого и чего предназначен язык D

  • Для программистов, регулярно пользующихся анализаторами кода для выявления ошибок еще до компиляции;
  • Для людей, которые компилируют код с максимальным количеством включенных уровней предупреждений (warning levels), что означает интерпретацию предупреждений как ошибок;
  • Для тех, кто решил, что обещание легкого объектно-ориентированный подхода к программированию в языке C++ не выполняется из-за его сложности и запутанности;
  • Для программистов, которых впечатляет мощь языка C++, но разочаровывает необходимость ручного управления памятью и ловля багов, связанных с указателями;
  • Для проектов, нуждающихся во встроенных средствах тестирования и верификации;
  • Для команд разработчиков, разрабатывающих приложения, исходные тексты которых содержат миллионы строк кода;
  • Для программистов, которые считают, что язык программирования должен поддерживать достаточное количество возможностей для абстрактной работы с указателями;
  • Для математических программистов. Язык D поддерживает работу с комплексными числами и операторами определения NaN’ов (not a number) и бесконечностей (infinity). Они были добавлены в стандарт C99, но не в C++.

Для кого и чего D не предназначен

  • Вообще-то никто не собирается конвертировать миллионы строк исходного кода на C/C++ в код языка D. А поскольку компилятор D не будет работать с не модифицированным исходным кодом C/C++, язык D не предназначен для уже существующих программ (однако, D поддерживает существующие API для языка C);
  • Для очень маленьких программ, для которых больше подойдут языки скриптования или интерпретируемые языки, например Python или Perl;
  • В качестве первого языка программирования. Basic и Java больше подойдут для начинающих программистов, а язык D предназначен для тех, кто уже имеет опыт программирования и для профессиональных программистов;
  • Для борцов за чистоту языка. D является практичным языком и каждая его особенность реализуется таким образом, чтобы сохранить эту практичность. Например, в языке D нет необходимости использовать указатели в ординарных ситуациях, но работа с указателями поддерживается.

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

Объектно-ориентированное программирование

Классы

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

Перегрузка операторов

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

Эффективность

Модули

Файлы исходного кода взаимно однозначно соответствуют модулям. Вместо включения (#include) файлов исходного кода достаточно импортировать модуль. В этом случае нет необходимости беспокоиться о том, что один и тот же модуль будет импортирован несколько раз, а, значит, и нет необходимости обрамлять код в заголовочных файлах с использованием макросов препроцессора #ifndef/#endif или #pragma once.

Объявление против описания

C++ обычно требует, чтобы функции и классы были объявлены дважды — объявление происходит в заголовочных файлах (*.h), а описание происходит в файлах исходного кода (*.cpp). Это утомительный и подверженный ошибкам процесс. Очевидно, что программисту достаточно объявить функцию или класс лишь однажды, а компилятор должен впоследствии извлечь информацию об объявлении и сделать ее доступной для импортирования. Именно так работает язык программирования D, например:

class ABC
{
    int func() { return 7; }
    static int z = 7;
}
int q;

И более нет необходимости отдельного описания функций-членов, атрибутов и спецификаций внешних объявлений (extern), как в языке C++:

int ABC::func() { return 7; }

int ABC::z = 7;

extern int q;

Заметка: Конечно же, в C++ тривиальные функции вроде { return 7; } тоже описаны внутри класса, но более сложные должны быть описаны отдельно. Вдобавок, если нужны опережающие ссылки (ссылки на класс или функцию, которые объявлены, но еще не определены), то для этих объектов нужны прототипы (prototype). Следующий код не будет работать в C++:

class Foo
{
    int foo(Bar *c) { return c->bar(); }
};

class Bar
{
    public:
    int bar() { return 3; }
};

Но эквивалентный код на языке D будет рабочим:

class Foo
{
    int foo(Bar c) { return c.bar; }
}

class Bar
{
    int bar() { return 3; }
}

А то, будет ли функция встраиваемой (при компиляции вызов такой функции заменяется ее кодом) или нет, в языке D зависит от настроек оптимизатора.

Шаблоны

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

Ассоциативные массивы

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

Реальный typedef

В языках C и C++ оператор typedef на самом деле просто создает синоним типа данных и никакого нового типа данных не объявляется. В языке D оператор typedef объявляет новый тип данных. Таким образом, код

typedef int handle;

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

int foo(int i);
int foo(handle h);

Тип данных Bit

Бит является фундаментальным типом данных, поэтому в D существует такой тип данных. Следующий код используется для создания массива битов:

bit[] foo;

Документация

Документирование традиционно происходит в два этапа — написание комментариев, описывающих, что делают функции, а затем перенос этих комментариев вручную в отдельные HTML-файлы и файлы документации man. И, естественно, со временем, то, что описывается в документации, будет отличаться от того, что на самом деле происходит в исходном коде. Возможность генерации документации напрямую из комментариев в файлах исходного кода не только экономит время при подготовке документации, но и облегчает поддержку соответствия документации исходному коду. Для языка D существует единая спецификация для генератора документации.

Для генерации документации в языке C++ существуют инструментальные средства, разработанные третьими лицами. Использование этих инструментальных средств имеет ряд недостатков:

  • Очень сложно обработать C++ код на 100 % правильно, для этого на самом деле потребуется компилятор. Инструменты от третьих лиц (third party tools) корректно работают только с подмножеством языка C++;
  • Разные компиляторы поддерживают разные версии языка C++ и разные расширения языка. Очень трудно соответствовать всем этим вариациям поддержки языка C++;
  • Инструменты для генерации документации из C++ могут не быть реализованы для некоторых платформ или могут не соответствовать последней версии компилятора;

Будучи встроенным в компилятор, генерация документации является единой для всех реализаций языка D.

Функции

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

Вложенные функции

Функции могут быть объявлены в теле других функций, что очень полезно для ограничения области видимости вспомогательных функций.

Функциональные литералы (function literals)

Анонимные функции могут быть встроены напрямую в выражение.

Динамические делегирования (closure)

Встроенные функции и методы классов могут использоваться при делегировании обязанностей, что делает обобщенное программирование проще.

Спецификаторы доступа к параметрам функций: in, out и inout

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

Все эти возможности D позволяют использовать больше различных программных интерфейсов, а также избавляет от необходимости использовать различные искусственные приемы, как, например IDL (Interface Definition Languages).

Массивы

Массивы в языке C имеют несколько недостатков, которые приходится корректировать:

  • Информация о размерности массива не прилагается к массиву, поэтому должна извлекаться дополнительно и передаваться отдельно. Классический пример – это объявление функции main(int argc, char *argv[]). В языке D, функция main объявляется так: main(char[][] args);
  • Когда массив передается в функцию, на самом деле передается ссылка на него, даже когда прототип функции говорит, что должен быть передан массив. Когда происходит это преобразование, вся информация о типе массива теряется;
  • Размерность массивов языка C не может быть изменена. Это означает, что даже такое простое множество, как стек, должно быть реализовано в виде сложного класса;
  • В языке C нельзя произвести проверку на нарушение границ массива, просто потому, что размерность массива неизвестна;
  • Массивы объявляются с использованием оператора [] после названия массива. Это ведет к использованию очень неуклюжего синтаксиса при объявлении, скажем, ссылки на массив:
int (*array)[3];

В языке D оператор [] при объявлении массива ставится после типа данных:

 int[3]* array; // объявляется ссылка на массив из трех целых чисел
 long[] func(int x); // объявляется функция, возвращающая массив длинных целых

Этот код более легок для восприятия. В языке D можно использовать несколько видов массивов: указатели, статические массивы, динамические массивы и ассоциативные массивы.

Строки

Манипулирование строками является стандартным средством при программировании, поэтому поддержка строк должна быть встроена в язык программирования. В языках C и C++ работа со строками выглядит неуклюже. Современные языки поддерживают конкатенацию, копирование и другие операции со строками. Поддержка строк ведет к облегчению работы с массивами строк.

Управление ресурсами (resource management)

Автоматическое управление памятью

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

Существует мнение, что сбор мусора нужен только ленивым и начинающим программистам. В конце концов, в C++ нет ничего такого, чего нельзя было бы сделать в C или в ассемблере.

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

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

Явное управление памятью

Несмотря на то, что язык D поддерживает автоматический сбор мусора, операторы new и delete могут быть перегружены для определенных классов.

RAII

RAII — это современная методика разработки для управления распределением и освобождением ресурсов. Язык D поддерживает методику RAII в контролируемой и предсказуемой манере, независимой от цикла сбора мусора.

Производительность

Легковесные составные типы данных

Язык D поддерживает простые структуры в стиле языка C не только для совместимости с этим языком, но и потому что они очень полезны в тех случаях, когда возможностей классов слишком много.

Встроенный ассемблер

Драйвера устройств, высокопроизводительные системные приложения, а также встраиваемые системы (embedded systems) требуют углубления до уровня команд ассемблера. Программирование на языке D не требует использования ассемблера, но он реализован и является частью языка.

Надежность

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

Отладочный код

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

Обработка исключений

Модель try-catch-finally предпочтительнее, чем просто try-catch, потому что не требует создания временных (dummy) объектов, деструктор которого будет выполнять то, что может сделать finally.

Синхронизация

Многопоточное программирование становится все более распространенным, поэтому в языке D реализованы базовые возможности для создания многопоточных приложений. Синхронизация может быть применена ко всему объекту или к отдельным его методам.

synchronized int func() { ... }

Синхронизированные функции разрешают в один момент времени исполнять код функции только одному потоку.

Устойчивые к ошибкам методики

  • Динамические массивы вместо указателей;
  • Переменные-ссылки вместо указателей;
  • Ссылки на объекты вместо указателей;
  • Сбор мусора вместо явного управления памятью;
  • Встроенные возможности синхронизации потоков;
  • Встраиваемые функции вместо макросов;
  • Уменьшение необходимости использовать указатели;
  • Явные размеры целого типа данных;
  • Отсутствие неопределенности, касающейся наличия знака у символьного типа;
  • Отсутствие необходимости повторного объявления;

Проверки во время компиляции

  • Более строгая проверка на соответствие типа данных;
  • Никаких пустых условий в цикле for;
  • Присвоения не возвращают булевого значения;
  • Проверка использования устаревших API;

Проверки во время выполнения

  • Выражения assert();
  • Проверка на выход за пределы массива;
  • Исключение при нехватке памяти;

Совместимость

Приоритеты операторов и правила вычисления

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

Прямой доступ к API языка C

Язык D не только имеет типы данных, соответствующие типам данных языка C, но и обеспечивает прямой доступ к функциям языка C. В таком случае нет необходимости писать функции-обертки (wrapper functions) или копировать значения членов составных типов по одному.

Поддержка всех типов данных языка C

Это делает возможным взаимодействие с API языка C или с кодом существующей библиотекой языка C. Эта поддержка включает структуры, объединения, перечисления, указатели и все типы данных, введенные в стандарте C99.

Обработка исключений операционной системы

Механизм обработки исключений языка D с механизмом обработки исключений операционной системы.

Использование существующих инструментариев

Код на языке D преобразовывается в объектный файл стандартного формата, что делает возможным использование стандартных ассемблеров, линкеров, дебаггеров, профайлеров и компрессоров исполняемых файлов.

Управление проектом

Контроль версий

В языке D реализована встроенная поддержка генерации нескольких версий программ из одного исходного кода. Это заменяет использование команд препроцессора #if и #endif.

Устаревание

Код со временем изменяется, поэтому старый код со временем заменяется новым, улучшенным. Старая версия кода должна быть доступна для обратной совместимости, но может быть отмечен как не рекомендованный к использованию (deprecated). Это облегчает работу команды поддержки по определению зависимостей от устарелого кода.

Отсутствие предупреждений (warnings)

Компиляторы языка D не генерируют предупреждений при встрече неоднозначного кода. Код может быть понятным компилятору или непонятным, это избавляет от необходимости решать, какие предупреждения относятся к ошибкам программы, а какие — нет. Использование предупреждений компилятора является признаком плохого дизайна языка.