Язык программирования D

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

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++ является довольно примитивным макроязыком. Самое время сделать шаг назад и посмотреть, для чего используется препроцессор, а затем внедрить поддержку этих возможностей в собственно язык программирования.
  • Множественное наследование. Это запутанная возможность сомнительной полезности. Множественное наследование может быть заменено обычным наследованием с использованием интерфейсов и агрегированием.
  • Пространства имён (namespaces). Пространства имён были попыткой решить проблему, возникающую при объединении разработанных независимо друг от друга кусков кода, когда пересекаются имена переменных, типов данных и так далее. Модульный подход выглядит проще и удобнее для использования.
  • Включаемые файлы (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, от которого все классы наследуют базовый набор функциональности. Экземпляры классов работают по ссылке, поэтому после обработки исключений не требуется писать сложный код для очистки памяти.

Перегрузка операторов[править]

Классы могут быть приспособлены для работы с уже существующими операторами. Благодаря перегрузке операторов можно создавать новые типы данных. Например, можно создать тип данных для работы с большими числами, создав класс и перегрузив операторы +, -, * и /, чтобы использовать алгебраические операции с этими числами.(Данная возможность реализована и в языке С++(https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B0_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2))

Эффективность[править]

Модули[править]

Файлы исходного кода взаимно однозначно соответствуют модулям. Вместо включения (#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);

Документация[править]

Документирование традиционно происходит в два этапа — написание комментариев, описывающих, что делают функции, а затем перенос этих комментариев вручную в отдельные 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++ требуют последующего освобождения памяти. Методика сбора мусора делает жизнь проще.

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

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

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

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

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

RAII[править]

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

Производительность[править]

Легковесные составные типы данных[править]

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

Встроенный ассемблер[править]

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

Надёжность[править]

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

Отладочный код[править]

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

Обработка исключений[править]

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

Синхронизация[править]

Многопоточное программирование становится всё более распространенным, поэтому в языке 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 не генерируют предупреждений при встрече неоднозначного кода. Код может быть понятным компилятору или непонятным, это избавляет от необходимости решать, какие предупреждения относятся к ошибкам программы, а какие — нет. Использование предупреждений компилятора является признаком плохого дизайна языка.

Ссылки[править]