Заметки из мира IT

В стиле минимализм

Язык программирования D является хорошей альтернативой C++. В особенности в тех случаях, когда тратить время на написание кода, а не на его отладку. В...
http://dynamic.versusit.ru/Files/2013/56017bdf-5bdb-4627-9f84-b21e27c42547.png

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

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

Зачем переходить с С++ на D? Очевидно, что тонны C++ кода никто переписывать просто так не будет. Но для новых проектов D является весьма хорошим вариантом. Обычно библиотеки это часть того что делает язык популярным. Этим обязан своей популярности Python.

Хотя в прошлом были некоторые проблемы, когда Phobos, стандартная библиотека D, значительно отставала в своем развитии, то сейчас это не так. Если библиотека написана на С, то вы сможете ее использовать в D лишь немного поправив ее заголовки. Если есть потребность использовать в D проекте С++ библиотеку, то тут уже сложнее, хотя теоретически это может сделать обернув С++ враппером С, но тут много подводных камней.

Вы можете задаться вопросом, кто использует D в реальном мире. Поможет ли знание D вам найти работу? Пока спрос намного меньше, чем на специалистов знающих C++, C # или Java, но предложений на рынке хватает. С D сейчас можно получить больше выгоды пиша код для себя. И тогда вы смело можете начинать использовать Dlang т.к. он даст вам много интересных возможностей.

Что хорошего в D? Выразительный код, отсутствие заголовочных файлов, и предварительного объявления класса, безопасный код по умолчанию (в том числе потоков), можно писать код, используя несколько парадигм, юникодные строки, быстрое время компиляции и, наверное, самое главное: шаблоны и правильно сделанные алгоритмы.

Безопасность и отладка




Начнем с самой простой программы, где ничто не может пойти не так (принятие желаемого за действительное). Вот типичный «hello world» программа, которая должна быть вызвана с именем из аргумента:

import std.stdio;

void main(string[] args) {

    writeln("Hello ", args[1]);

}

Обратите внимание, код является ошибочным. Тут мы должны были проверить есть ли 2 элемента в массиве аргументов, так что вместо args[1] мы должны бы написать args.length > 1 ? args[1] : «unknown». Но мы этого не сделали, мы скомпилировали код так dmd -g hello.d, а затем один сумасшедший выполнил программу без аргументов. Что произошло потом? Программа выходит с такой информацией:

core.exception.RangeError@hello(3): Range violation

----------------

0x0040E48C in char[][] core.sys.windows.stacktrace.StackTrace.trace()

0x0040E317 in core.sys.windows.stacktrace.StackTrace core.sys.windows.stacktrace.StackTrace.__ctor()

0x0040466C in onRangeError

0x0040202F in _Dmain at C:\data\dmd2\test\hello.d(3)

0x00402A48 in extern (C) int rt.dmain2.main(int, char**).void runMain()

0x00402A82 in extern (C) int rt.dmain2.main(int, char**).void runAll()

0x004026A4 in main

0x004187AD in mainCRTStartup

0x75853677 in BaseThreadInitThunk

0x77709F42 in RtlInitializeExceptionChain

0x77709F15 in RtlInitializeExceptionChain

----------------

Это вполне информативно. С другой стороны, точная копия этой программы в C++ напечатала бы «Hello «, как будто ничего плохого не случилось, давая программисту ложное ощущение, что все хорошо. Но когда индекс массива будет увеличен с 1 до 1000, мы, наконец, получим ошибку «Segmentation fault (core dumped)» . Кроме того, информация об ошибке будет весьма не интуитивна.

Программирование в D гораздо лучше чем С++, потому что вы лучше информированы об ошибках, они не игнорируются, и отлавливаются с помощью встроенных механизмов. Вы также получаете лучшие инструменты, встроенные непосредственно в язык, чтобы помочь убедиться, что код делает то, что вы намеревались сделать. Такие инструменты как: assert’ы, static assert’ы, юнит-тесты, ввод-вывод контрактов (input-output contracts).

Основные типы, массивы, срезы и строки

Сила D заключается в его системе типов. Прежде всего, соблюдение принципа RAII(получение ресурса есть его инициализация), не инициализированные переменные не будут создаваться с мусором. Целые типы всегда будут инициализироваться в 0, числа с плавающей запятой в NaN, и самое главное, указатели будут инициализированы в null. Это очень удобно и полезно.

Статические массивы фиксированного размера, такие как тип double[3], передаются по значению, бывают очень полезные в расчетах. Динамические массивы, такие как int[], к примеру знают свой текущий размер и вместимость. В языке есть даже ассоциативные массивы, такие как int[string], в которых ключ-строка указывает на целое число. Они могут быть полезны при подсчете слов, например:

int[string] words;

// ... цикл или что то еще где мы читаем someWord ...

words[someWord]++;

Обратите внимание, что мы не должны проверять существование words[someWord] и написать:

if(someWord in words) words[someWord]++; 

else 

words[someWord] = 1;

Это все благодаря инициализацией по умолчанию!

Т.к. динамические массивы состоят из указателя на вектор в памяти и число, обозначающее его длину, то можно создать массив, который указывает во внутрь больших массивов. В D это называется слайсингом (slices) и может быть крайне полезно при работе с большими массивами данных т.к. простое их копирование будет через чур ресурсозатратным. В C пришлось бы использовать два указателя или указатель и размер. Слайсинг с одной стороны скрывает факт того, что мы работаем с другим массивом, и единственное что нам потребуется сделать если мы захотим скопировать таким образом данные это потребуется убедиться, что у нас есть достаточно свободной памяти. Отчасти именно по этому в D есть сборщик мусора т.к. иначе могла бы произойти ситуация, когда больший массив при выходе за свои границы не был удален, а слайс от него все еще существует.

Теперь о строках. Строки в Dlang это последовательность char, wchar или dchar (8, 16 и 32 бита соответственно), но остановим внимание на простом char[]. В D char и byte разный типы, а значит нельзя перепутать строку и с массивом цифр. И это хорошо, так же как хорошо то, что D полностью поддерживает Unicode. 8-ми битные строки в UTF-8 обрабатываются без проблем при помощи std.regex. И вам не придется связываться с Boost и другими библиотеками, только для того, чтобы не иметь проблем с кодировками. При работе с диапазонами D так же имеет большие преимущества нежели std::string из С++, которая значительно менее эффективна.

Огромное количество проблем С++ унаследовал от С. Указатели вне границ массива, Нуль-терминированные строки — все это пришло из С. Хотя в С++ предлагает средство работ с динамическими массивами (std::vector), но это является всего лишь надстройкой над std::string, в которой мало хорошего из-за того, что пришлось оставить совместимость с С. Как итог парсинг строк в D работает значительно быстрее чем в С++. Строки в С++ ущербны by design т.к. std::string использует нуль-терминированные строки для хранения, что в результате дает низкую производительность. В С++11 в очередной раз попытались исправить ситуацию через введение Rvalue References (заодно частично поломав совместимость с С++03. прим. переводчика), но все равно осталось много проблем, в то время как в D все работает из коробки.

Говоря о массивах С++ нужно отметить символ +, который отвечает за сцепку строк. По непонятным для меня причинам так же имеется два оператора потока << и >>. Которые работают весьма странным образом. К примеру >> может неожиданно появиться в шаблоне вида: hash_map<int, set<int>> и до недавних пор это вызывало синтаксическую ошибку. В D таких странностей нет и для сцепки массивом используется специальный оператор ~, который позволяет программистам избегать неприятных сюрпризов.

Смотрим на примере работу этих операторов:

cout << 1 << 3 << endl;  // it's "13" and not "8"!

cout << (1 << 3) << endl;  // now it's "8"

cout << string("a ") + "test" << endl;  // prints "a test"

cout << "a " + "test" << endl;  // doesn't compile

cout << string("it's ") + "a " + "test" << endl;  // prints "it's a test"

cout << string() + 8 << endl;  // doesn't compile!

Алгоритмы, диапазоны и кодогенерация
Я считаю, что за всю историю С++ самой полезной вещью было введение STL (Standard Template Library). Это позволило наконец использовать алгоритмы и контейнеры с различными способами доступа к данным без потери информации о их типе. С STL стало возможным писать код и не думать о том, что нужно будет делать n для n-ного количества контейнеров.

В чистом C тип у нас теряется. Когда вы используете быструю сортировку qsort из стандарной библиотеки, вам придется делать преобразования из type* в void* и обратно. В С++ типы не потеряются, но тут есть одна проблема. Для перемещения по коллекциям используется итератор. Иными словами мы используем низкоуровневый указатель из C в более высоком уровне, но это весьма сложно реализовать в новом классе. Довольно часто может потребоваться иметь два указателя, к примеру начальная и конечная позиция. Работа сразу с двумя указателями весьма сложна т.к. при вызове функций вам нужно будет куда-то запоминать позицию указателей. В D проблема была решена через диапазоны (Ranges). Они весьма просты и удобны в использовании.

В отличие от итерараторов, с хранящими их структурами диапазоны практически не имеют ограничений. repeat, iota,sequence и recurrence хорошие примеры из стандарной библиотеки Phobos, которые позволяют создавать диапазоны, последовательности практически без ограничений.

Код получается очень компактным и легко читаемым. Смотрим:

// a[0] = 1, a[1] = 1, and compute a[n+1] = a[n-1] + a[n]

auto fib = recurrence!("a[n-1] + a[n-2]")(1, 1);

// print the first 10 Fibonacci numbers

foreach (e; take(fib, 10)) { writeln(e); }


// print the first 10 factorials

foreach (e; take(recurrence!("a[n-1] * n")(1), 10)) { writeln(e); }

Впечатляет? Последнюю строку можно вообще через лямбду записать:

recurrence!((a, n) => a[n-1] * n)(1)

И это не все! Можно еще короче, и даже без скобок после восклицательного знака, если ясно, что вслед за восклицательным знаком начинаются параметры.

Особое внимание заслуживают шаблоны (templates), о которых можно написать целую книгу, но сейчас пару слов о сборщике мусора (garbage collector).

Сборка мусора
Автоматическая сборка мусора в теории очень хороша, но когда дело доходит до практики появляются некоторые сложности. Первой проблемой являются усилиях требующиеся на написание хорошего сборщика мусора. К примеру Mono очень долго использовал сборщик Boehm Conservative GC и только недавно перешел на более продвинутый SGen. Смена таких вещей как GC в процессе разработки не очень хорошая практика т.к. для больших и сложных приложений это может вызвать проблемы обратной совместимости. В D, кстати, сейчас используется свой собственный сборщик мусора.

Второй проблемй является то, что невозможно одновременно написать оптимизированную по памяти быструю, компактную и производительную программу. Многим разработчикам игр не нравится сборщик мусора т.к. он увеличивает латентность игры. Что уж тут говорить, в геймдеве некоторым даже malloc кажется весьма не эффективным и они пишут свои реализации для управления памятью. Хотя и в С++ и в D это возможно, но в D с этим будет больше сложностей. Рассмотрим это на примере malloc и определения _new и _delete:

import std.stdio, std.conv, core.stdc.stdlib;


class C {

    int a;

    this(int a) {

        this.a = a;

        writefln("C created");

    }

    ~this() {

        writefln("C destroyed");

    }

}


T _new(T, Args...) (Args args) {

    size_t objSize = __traits(classInstanceSize, T);

    void* tmp = core.stdc.stdlib.malloc(objSize);

    if (!tmp) throw new Exception("Memory allocation failed");

    void[] mem = tmp[0..objSize];

    T obj = emplace!(T, Args)(mem, args);

    return obj;

}


void _delete(T)(T obj) {

    clear(obj);

    core.stdc.stdlib.free(cast(void*)obj);

}


void main() {

    C x = _new!C(100);

    _delete(x);

}

Вы сможете если потребуется заменить malloc и free чем либо другим, это позволит вам полностью уйти от использования сборщика мусора в D, при этом естественно придется отказатся конструкторов и всего что завязано на сборщик мусора. Правда глядя на код выше невольно начинаешь благодарить сборщик мусора, за то что он берет все эти сложности на себя. Как никак он решает уйму проблем с памятью и объектами которые забыли удалить.

Но тут кроется одна неприятность. Сборщик мусора в Dlang не очень быстр и не всегда может определить является ли та или иная посследовательность бит указателем и поэтому предполагает, что они являются указателями. Это может привести к падению приложения. Если память занятая подобным ненастоящим указателем не будет освобождена мы в конечном счете получим исключение OutOfMemoryError:

import std.stdio, core.memory, core.thread;

class C { 

    byte[] s;

    this() { s = new byte[80 * 1024 * 1024]; }

}

void main(string[] args) {

    for (int i=0, j=0; i < 1000; i++) {

        auto x = new C();

        Thread.sleep(dur!"msecs"(500));

        // delete x.s;

    }

}

Чтобы избежать этого нужно указать сборщику мусора, что данную последовательность бит не нужно рассмаривать как укзатель и явно удалить ее (расскоментируйте закомментированную строку). Однако к сожалению delete было призано устаревшим и остается надеяться что следующая версия сборщика мусора исправит эту ситуацию.

admin

Яндекс.Метрика