D (Ди) (В крайних случаях Dlang) — мультипарадигмальный статически типизированный компилируемый язык программирования, созданный Уолтером Брайтом из компании Digital Mars. Начиная с 2006 года соавтором также является Андрей Александреску. D является потомком языка C++, но существенно доработан по сравнению с ним. Также он заимствует ряд концепций из языков программирования Python, Ruby, C#, Java, Eiffel.
D доступен для операционных систем Windows, Linux, macOS, FreeBSD, OpenBSD и Android.
История В предисловии к книге А. Александреску «Язык программирования D» Уолтер Брайт пишет, что начал разработку этого языка в 1999 году. Проект задумывался как реинжиниринг языка C++ с целью избавиться от наиболее существенных недостатков исходного языка и внедрить в него современные архитектурные решения. При создании языка D была сделана попытка соединить производительность компилируемых языков программирования с безопасностью и выразительностью динамических.
Первоначально автор предполагал назвать язык «Mars», но из-за преемственности по отношению к C++ в обсуждениях язык постоянно называли «D», в результате именно это название и закрепилось за проектом.
Стабильная версия компилятора 1.0 вышла 2 января 2007DMD 1.00 — here it is! (digitalmars.D.announce) . Вскоре после выхода компилятора 17 июня 2007 года автор перевёл версию 1 в режим поддержки и приступил к разработке версии 2.0, которая изначально не гарантировала обратной совместимости. Эта версия (последняя на сегодняшний день мажорная версия D) развивается и по сей день.
Обзор языка В языке D реализовано множество синтаксических средств и концепций, отсутствующих в C++: контрактное программирование, встроенные юнит-тесты, модули вместо заголовочных файлов (до C++ 20), поддержка сборки мусора (при сохранении доступности ручного управления памятью), встроенные ассоциативные массивы, замыкания, анонимные функции, значительно переработан механизм шаблонов.
Синтаксис D относится к семейству C-подобных языков, в общих чертах его синтаксис похож на C/C++/C#, Java. При разработке языка соблюдается принцип: код, одинаково валидный и в C, и в D, должен вести себя одинаково.
«Hello, world!» на D: import std.stdio;
void main() { writeln ("Hello, world!"); }
Так же, как в C, функция main() является точкой входа.
Конструкции if, for, while, do-while выглядят и работают аналогично C/C++. Инструкция множественного выбора switch выглядит аналогично C++, но допускает переменные в метках ветвей case и требует, чтобы каждая ветвь case завершалась break или return; для перехода на следующую ветвь после обработки текущей необходимо использовать специальную конструкцию goto case. Также запрещены конструкции switch без ветви default.
Из дополнительных управляющих конструкций можно отметить static if — инструкцию для условной компиляции (условие проверяется статически и в код включается содержимое той ветви, которая ему соответствует), оператор полного множественного выбора final switch — в отличие от обычного switch, он работает только со значениями enum, а компилятор статически проверяет, что в выборе учтены все возможные варианты и выдаёт ошибку в противном случае. Также есть цикл по коллекции foreach.
Модульность В D встроена система разбиения программы на модули (пакеты), обеспечивающая раздельную компиляцию и контролируемый импорт-экспорт. Система пакетов напоминает принятую в Java или Go: пакеты образуют иерархическую структуру, естественно отображаемую на дерево файловой системы. В отличие от C++, в D нет глобального пространства имён, каждое имя определяется в каком-либо пакете. С помощью инструкции import модуль программы может импортировать пакет, сделав доступными все имеющиеся в нём определения. Обращение к импортированным именам может выполняться с квалификацией: «имя_пакета.имя_объекта».
Язык предусматривает ряд средств, направленных на обеспечение удобной работы с импортируемыми именами. Есть возможность переименования пакета при импорте, задания альтернативного имени (алиаса) импортируемого пакета, импорта конкретных имён. Кроме того, язык разрешает без каких-либо дополнительных инструкций использовать импортированные имена без квалификации именем пакета. Однако действует ограничение: если в области видимости есть более одного подходящего определения встреченного в программе имени, то компилятор выдаёт ошибку и требует, чтобы имя было явно квалифицировано. Это предотвращает так называемый «угон имён», когда при добавлении в списки импорта нового пакета компилятор начинает связывать некоторое имя в программе не с тем определением, с которым оно связывалось ранее.
Универсальный синтаксис вызова функций (UFCS) В D реализован механизм UFCS (Uniform function call syntax), позволяющий вызывать функции для любого объекта так, как будто они являются его методами. Например:
import std.stdio; import std.algorithm; import std.array;
void main() { auto a = [2, 4, 1, 3];
// все три следующих варианта корректны и работают одинаково writeln(a); // "классический" C-подобный вариант a.writeln(); // функция вызывается так, как будто является методом объекта "a", хотя и не является таковой a.writeln; // функцию без параметров можно вызывать без скобок
// это позволяет использовать цепочки вызовов, характерные для функциональных языков int[] e = a.sort().reverse;
// многострочная цепочка вызовов также возможна stdin .byLine(KeepTerminator.yes) .map!(a => a.idup) .array .sort; }
Атрибуты функций Функции в D могут быть определены с дополнительными необязательными атрибутами, которые позволяют явно указывать некоторые аспекты поведения этих функций. Например, функция, помеченная атрибутом pure, гарантированно является функционально чистой (с некоторыми оговорками). Функциональная чистота при этом проверяется на этапе компиляции. Пример объявления функции с атрибутом:
pure int sum (int first, int second) { return first + second; }
int sum (int first, int second) pure // атрибуты можно указывать и после списка аргументов { return first + second; }
Примеры атрибутов функций: pure — функциональная чистота @safe — гарантия безопасной работы с памятью nothrow — функция гарантированно не генерирует исключений @nogc — гарантия того, что функция не содержит операций, выделяющих память на сборщике мусора @property — атрибут метода класса, позволяющий избежать использования «наивных» геттеров-сеттеров
Параллельные вычисления В язык встроен механизм запуска параллельных подпроцессов с помощью встроенной функции spawn() и обмена данными между параллельно исполняемыми фрагментами кода путём передачи сообщений (функции send() и receive() / receiveTimeout()). Использование обмена сообщениями считается авторами D предпочтительнее обмена данными через общую память.
Тем не менее, в случаях, когда это необходимо (например, при передаче между сопрограммами больших объёмов данных) имеется возможность применять традиционный для императивных языков подход с разделяемыми областями памяти и синхронизацией доступа через семафоры и мьютексы. Для поддержки такого обмена: c помощью модификатора shared могут объявляться данные, разделяемые между потоками, при этом на уровне компилятора блокируются попытки работать с этими данными не-атомарными операциями; библиотека языка предоставляет стандартные синхронизационные примитивы; библиотека языка предоставляет специальные блокировочные классы, которые используются для контекстной блокировки: объявление экземпляра такого класса в методе синхронизирует код от данной точки до конца метода; с помощью модификатора synchronized можно объявить целый класс синхронизированным: все методы этого класса компилятор автоматически синхронизирует, так что никакие два из них не могут выполняться параллельно для одного и того же экземпляра класса, объявленного как shared. Для всех встроенных средств синхронизации компилятор автоматически отслеживает и запрещает попытки изменять внутри синхронизируемого кода неразделяемые данные, доступные более чем одному потоку.
Встроенные юнит-тесты В D юнит-тесты являются частью языка, их можно использовать без подключения дополнительных библиотек или фреймворков.
import std.stdio;
int first (int[] arr) { return arr; }
unittest { int[] arr1 = [1, 2, 3]; int[] arr2 = [10, 15, 20];
assert(first(arr1) == 1); assert(first(arr2) == 10); }
void main() { // ... }
Парадигмы программирования D реализует пять основных парадигм программирования — императивное, ООП, метапрограммирование, функциональное программирование и параллельные вычисления (модель акторов).
Управление памятью D использует сборщик мусора для управления памятью, однако возможно и ручное управление с помощью перегрузки операторов new и delete, а также с помощью malloc и free, аналогично C. Сборщик мусора можно включать и выключать вручную, можно добавлять и удалять области памяти из его видимости, принудительно запускать частичный или полный процесс сборки. Существует подробное руководство, описывающее различные схемы управления памятью в D для тех случаев, когда стандартный сборщик мусора неприменим.
SafeD SafeD — название подмножества языка D, использование которого гарантирует безопасность доступа к памяти.
Типы данных Язык имеет богатый набор определённых типов данных и средств для определения новых типов. Типы в языке D разделяются на типы-значения и типы-ссылки.
Базовые типы Набор базовых типов можно разделить на следующие категории: void — специальный тип для пустых значений bool — логический тип целочисленные типы: знаковые byte, short, int, long и соответствующие им беззнаковые ubyte, ushort, uint, ulong типы для чисел с плавающей точкой: float, double, real. Для типов с плавающей точкой есть соответствующие им варианты для мнимых и комплексных чисел: мнимые: ifloat, idouble, ireal комплексные: сfloat, сdouble, сreal знаковые (символьные) типы: char, wchar, dchar, обозначающие кодовые единицы кодировок UTF-8, UTF-16 и UTF-32 соответственно. В отличие от C++ все размеры целочисленных типов определены спецификацией. То есть, тип int будет всегда размером 32 бита. Целочисленные литералы можно записывать в десятичной, двоичной (с префиксом 0b) и шестнадцатеричной (с префиксом 0x) системе счисления. Способ записи литералов в восьмеричной системе в стиле C (то есть с префиксом 0) был убран, так как такую запись легко спутать с десятичной. Если всё-таки нужно использовать восьмеричную систему, можно воспользоваться шаблоном std.conv.octal.
Производные типы pointer — указатель array — массив associative array — ассоциативный массив function — функция delegate — делегат string, wstring, dstring — удобные псевдонимы для неизменяемых массивов знаковых (символьных) типов immutable(char)[], immutable(wchar)[] и immutable(dchar)[], обозначающие неизменяемые (квалификатор immutable) строки Юникода в одной из кодировок UTF-8, UTF-16 и UTF-32 соответственно.
Пользовательские типы alias — псевдоним enum — перечисление struct — структура union — объединение class — класс
Вывод типов, ключевые слова «auto», «typeof» и безымянные («Voldemort») типы В D реализован механизм вывода типов. Это значит, что тип, как правило, может быть вычислен на этапе компиляции и его не обязательно указывать явно. Например, выражение: auto myVar = 10 на этапе компиляции будет преобразовано в int myVar = 10. Использование вывода типов дает несколько преимуществ: Более лаконичный и читаемый код, особенно если в нём используются длинные имена структур или классов. Например, выражение VeryLongTypeName var = VeryLongTypeName(/* ... */);
может быть заменено на
auto var = VeryLongTypeName(/* ... */);
с помощью ключевого слова typeof можно создать переменную такого же типа, как у существующей переменной, даже если её тип неизвестен. Пример: // file1.d int var1;
// file2.d typeof(var1) var2; // var2 получает тип int
использование безымянных типов. Пример:
// Функция фактически возвращает результат типа TheUnnameable, но, поскольку этот тип определен внутри функции,