Так как это мой первый пост, то расскажу что уже довольно долго занимаюсь программированием игр на C++. Иной раз, окинув взглядом мое творение, я ужасаюсь. Виной тому шаблонное программирование — настолько запутанное и уродливое. Но, может существует более правильный и красивый путь? Многие пытаются ответить на этот вопрос и ищут альтернативу C++ в области «generic programming»… Я к примеру, перепробовала C#, Java, Python, JavaScript. Уже было решила писать на Lisp'е с его CLOS моделью, но тут я наткнулась на язык D, о нем и пойдет речь под катом.
Эффективность
Первое, что я проверила, было эффективность. Язык обладет собственным сборщиком мусора GC и у меня были сомения относительно автоматического управления памятью. Удивительно, но тесты показали, что D практически всегда быстрее C++, а на тестах с созданием множества мелких объектов, вроде векторов, обгонял в 2-3 раза (как потом я выяснил это сильная сторона любого хорошего сборщика мусора). Конечно с чистым С с включенной оптимизацией -O3 D проиграл (тесты проводились под Debian gcc), но не намного.
Надо заметить, для любителей оптимизации в ди есть встроенный переносимый ассемблер.
Также меня поразила скорость компиляции, она практически мгновенная. Благодаря этому язык может использоваться как скриптовый (есть стандартный интерпретарор rdmd), что позволило мне вообще отказаться в проектах от .sh и .batch файлов и внедрить кроссплатофрменные скрипты (о кроссплатформенности ниже).
Синтаксис
С эффективностью разобрались, а что насчет синтаксического сахара? По синтаксису D наиболее близок к С и Java, на нем можно сходу писать небольшие программы, даже не подозревая, что это D. Я не смогу рассмотреть все, поэтому отмечу самые важные моменты.
Не говоря уже о детской радости от использования вменяемого foreach (В других языках это уже давно), D использует технологию принципиально противоположную итераторам: Ranges. Благодаря очень мощным встроенным массивам, взятие подстроки реализуется очень просто:
string s = "some awesome string";
...
s = s[6..$];
assert(s == "awesome string"); // Думаю ассерты знакомы многим, о поддержке обработки ошибок ниже
При этом не создается новых массивов, в D массив является просто паком из двух указателей на начало и конец массива, что позволяет отлавливать выход за пределы и делать вот такой «slicing». Нужно упомянуть, что язык изначально поддерживает Unicode, но и имеет инструменты для работы с байтами. Вернемся к foreach и концепции Ranges, пример обработки введенного текста пословно:
import std.stdio;
import std.algorithm;
void main()
{
foreach (line; stdin.byLine())
foreach(word; splitter(line, " "))
{
writeln(word);
}
}
D2 имеет очень мощную и хорошо документированную стандартную библиотеку Phobos (в эпоху D1 их было две, но это уже история), благодаря которой прикладные программки пишутся легко и с удовольствием.
В отличии от нового стандарта C++ в D2 есть полноценные лямбды с замыканиями и вывод типов, пример объяснит лучше:
void someFunc(double a1, dobule a2)
{
// Типы выводятся на этапе компиляции
auto temp = new int[5];
// здесь мы передаем лямбду некой функции, которая будет использовать ее позже
sendAlgorithm(
(double arg) // возвращаемые типы выводятся автоматически
{
// мы имеем доступ к переменным, где была создана лямбда
writeln(a1,"+",a2,"?=",arg);
return a1+a2 == arg;
});
}
Обработка ошибок
Язык имеет очень много инструментов для обработки ошибок, начиная от привычных исключений с try/catch/finally блоками, заканчивая встроенными юнит-тестами и контрактным программированием. Рассмотрим примеры:
// Две абсолютно одинаковых функций с классической обработкой исключений и scope выражением:
void sendFile(Stream file)
{
try
{
// Выдуманная функция, которая кидает исключение
sendByBlocks(file);
} catch(Exception e)
{
writeln("Передача файла не удалась!");
} finally
{
// Вообще файл сам закроется, как только выйдет из зоны видимости, но это пример
file.close();
}
}
void sendFile(Stream file)
{
scope(failure) // есть также success, который выполняется при отсутствии исключений
{
writeln("Передача файла не удалась!");
file.close();
}
sendByBlocks(file);
}
Практически везде можно объявить блок с юнит-тестами и провести доскональное испытание вашего кода:
unittest // В релизную версию тесты не попадают
{
// Ассерты позволяют легко проверить работоспособность функции и вывести корретное сообщение об ошибке
assert(myFunc() == expectedResult, "Тест моей функции провален!");
}
Также очень полезно контрактное программирование, когда функция представляется в виде трех блоков: in, body, out. in и out проверяют входные и выходные параметры функции, очень полезно для интерфейсов, где объявлются только in out блоки. Все контракты вырезаются из релизной сборки приложения для ускорения работы.
// В языке отсутствует множественное наследование классов, но можно наследовать много интерфейсов как в Java
interface SomeInterface
{
double someFunc(double a1, double a2)
in
{
// Проверяем входные данные
}
out(result)
{
// Проверяем выходные данные
}
}
Обобщенное программирование
Теперь самое вкусное. В D расширили концепцию программирования на этапе компиляции, теперь любая функция с модификатором static может выполняться во время компиляции, а каждая функция имеет два! списка параметров, один ей передается в compiletime, другой в runtime.
// Тип T передается во время компиляции
// ref обозначет, что аргументы переданы по ссылке
void swap(T)(ref T a1, ref T a2)
{
T temp = a1;
a1 = a2;
a2 = a1;
}
Также улучшена перегрузка обобщенных функций, теперь вместо придумывания всяких костылей можно делать вот так:
import std.traits;
// Если guard выражение вернет false, то эта функция даже не рассматривается как кандидат для вызова
string convert(T)(T arg)
if( isFloatingPoint!T )
{
//...
}
// Эта перегрузка будет вызвана только для класса myClass
string convert(T)(T arg)
if( is( T == myClass) )
{
//...
}
Хотя множественное наследование классов запрещено в D. Есть очень интересная возможность делать «примеси» (классические mixin из Scala):
// Эта функция может выполняться в compiletime
static string constructField(T)(string name)
{
return T.stringof~" m"~name~";";
}
class MyClass
{
// mixin внедряет строку как код на этапе компиляции
// Результат: double mTime;
// Знак "!" используется для указания compiletime аргументов
mixin constructField!(double)("Time");
}
В стандартной библиотеке есть множество функций для получения списка типов параметров функции, списка всех методов класса, всех наследников класса и тому подобное, это то, чего мне жизненно не хватало в С++. Также ди поддерживает безопасные variadic функции:
// В функцию можно передать сколько угодно интов, все они упакуются в аккуратный массив
int func1(int[] args...)
{
int temp; // у каждого типа есть свойство init, которым инициализируется переменная
foreach(arg; args)
temp += arg;
return temp;
}
// Это уже интереснее, в функцию можно передать сколько угодно различных аргументов различных типов
// все будет упаковано в специальный массив, такую нотацию использует writeln
int func2(T...)(T args...)
{
foreach(i,arg; args)
{
// Типы аргументов тоже упакованы в массив
writeln(i, " type: ", T[i].stringof, " ", arg);
}
}
Многопоточность
Язык перенял все положительные тенденции в современном многопоточном программировании. Используется модель языка Erlang, все потоки изолированы (даже имеют по своей копии всех глобальных переменных) и общение происходит через посылку ассинхронных сообщений. Также используются immutable типы, которые гарантированно никогда не меняются, например строки это immutable(char[]).
Однако можно явно объявить некоторые классы или переменные с модификатором shared, и компилятор будет вас доставать, когда идет явно ошибочное использование расшаренных данных, приводящее к дедлоками или к гонкам. Можно добавить к классу модификатор synchronized (Java подход) и к объекту будет привязан свой мьютекс, который следит за блокировками при входе и выходе из методов.
До D я не знала о существовании такого подхода, как locking-free programming, когда используются встроенные процессорные атомарные примитивы (check and set = cas), позволяющие вообще отказаться от блокировок, но это воистинну очень сложная техника (гораздо легче использовать immutable).
Кроссплатформенность
Она предоставляется из коробки. Любой платформозависимый код можно обернуть в специальные блоки version, в каждом из которых находится код для определенной платформы:
version(Windows)
{
...
}
version(linux)
{
...
}
version(MacOS)
{
...
}
// Меток версий ос просто уйма, можно посмотреть на офф. сайте
// также блоки версий можно использовать со своими константами для поддержки различных версий своего же софта
Стандартная библиотека кроссплатформенна, большинство существующих библиотек под D кроссплатформенные. Писать платформонезависимый код в D намного удобнее и быстрее чем в других языках.
Однако есть проблема с gui библиотекой, единственной вменяемой является порт gtk.
Все эти возможности открывают невиданные просторы для экспериментов и улучшения своего кода, при этом сохраняется читаемость и эффективность. В D есть еще несколько более хитрых фишечек вроде встроенная статическая диспетчиризация (при вызове несуществующего метода класса, компилятор вызывает специальны оператор в классе и передает ему строку-название метода). Тех, кто дочитал до этого момента, я искренне благодарю. Подробнее о языке можно узнать на оффициальном сайте(dlang.org) и из книги А.Александреску «The D Programming Language» (не знаю, появился ли перевод на русский). Далее пойдет речь о недостатках и проблемах языка.
Помимо достоинств, стоит также обратить внимание на недостатки:
Недостатки языка
- Первое, что замечаешь при переходе с C++, что D имеет довольно ограниченный инструментарий для взаимодействия с C++, а точнее D бинарно совместим только с C и тонны собственных библиотек приходится портировать на D вручную, однако я заметил тенденцию, что после портирования код на 50% компактнее и прекрастно читается.
- Отсутствие нормальных IDE, это удар ниже пояса. Язык еще очень молод и среды разработки все еще в альфа-бета версиях. Есть плагин для Visual Studio называемый VisualD, но автодополнение и анализ кода там очень хромают, есть D-IDE на .net — очень многообещающая среда, но сырая и только под Windows, есть DDT плагин для Eclipse с тяжеловесной проверкой синтаксиса, но у него довольно топорные настройки, которые мне не подошли. Однако вскоре понимаешь, что этот язык страдает не так сильно от отсутсвия IDE, сейчас пользуюсь Sublime Text 2 и я удовлетворена полностью, все сложности с поиском функций, классов и т.д. отошли к организации структуры сорцов и хорошей документации.
- Под виндой есть просто подводная гора, компилятор dmd гененрирует obj файлы в формате OMF, несовместимый с майкрософтовским COFF форматом. Поэтому у меня не получилось подключить, например, CUDA к проекту, потому что компилятор от Nvidia генерирует как раз COFF объектные файлы. Никакие ухищрения и конверторы не помогли. Под другими платформами такой проблемы вообще нет, так как линкует все файлы gcc. Из-за этого же различия форматов могут быть странные проблемы под виндой при передаче функций в сишные библиотеки, вплоть до падений приложения.
- Отсутствие огромного количества библиотек, однако все pure С и предоставляющие C интерфес библиотеки подходят для использования. На D все еще не существует хоть какого-нибудь трехмерного графического движка (что я пытаюсь исправить).
- Недавно я столкнулась с багом компилятора (что очень плохо) при генерирования shared library (.so) под 64 битную архитектуру, хоть все исходники компилятора и стандартной библиотеки открыты, существующие компиляторы C++, С, С# и т.д. гораздо стабильнее.
- Высокий порог вхождения. Язык полон различных фич и инструментов, не навязывает определенную парадигму программирования (я не рассказала о функционально программировании в D), поэтому требует от программиста четкого понимания, чего он хочет. Поэтому я бы не советовала язык новичкам.
Подведя итоги, мое мнение: D отличный язык как для системного, так и для прикладного программирования, но как технология еще довольно молодая и страдающая детскими проблемами. Сообщество вокруг языка маленькое, но активное. Я буду очень рада, если мой пост хоть как-то поможет развитию языка.
Автор: nugrome