dlang-book/01-знакомство-с-языком-d
Alexander Zhirov b2bfe5c7a1 1.3 2023-01-22 11:36:37 +03:00
..
README.md 1.3 2023-01-22 11:36:37 +03:00

README.md

1. Знакомство с языком D

  • 1.1. Числа и выражения
  • 1.2. Инструкции
  • 1.3. Основы работы с функциями
  • 1.4. Массивы и ассоциативные массивы
  • 1.4.1. Работа со словарем
  • 1.4.2. Получение среза массива. Функции с обобщенными типами параметров. Тесты модулей
  • 1.4.3. Подсчет частот. Лямбда-функции
  • 1.5. Основные структуры данных
  • 1.6. Интерфейсы и классы
  • 1.6.1. Больше статистики. Наследование
  • 1.7. Значения против ссылок

Вы ведь знаете, с чего обычно начинают, так что без лишних слов:

import std.stdio;

void main()
{
    writeln("Hello, world!");
}

В зависимости от того, какие еще языки вы знаете, у вас может возникнуть ощущение дежавю, чувство легкой благодарности за простоту, а может, и легкого разочарования из-за того, что D не пошел по стопам скриптовых языков, разрешающих использовать «корневые» (top-level) инструкции. (Такие инструкции побуждают вводить глобальные переменные, которые по мере роста программы превращаются в головную боль; на самом деле, D позволяет исполнять код не только внутри, но и вне функции main, хотя и более организованно.) Самые въедливые будут рады узнать, что void main это эквивалент функции int main, возвращающей операционной системе «успех» (код 0) при успешном окончании ее выполнения.

Но не будем забегать вперед. Традиционная программа типа «Hello, world!» («Здравствуй, мир!») вовсе не повод для обсуждения возможностей языка. Она здесь для того, чтобы помочь вам начать писать и запускать программы на этом языке. Если у вас нет никакой IDE, которая выполнит за вас сборку программы, то самый простой способ это командная строка. Напечатав приведенный код и сохранив его в файле с именем, скажем, hello.d, запустите консоль и введите следующие команды:

$ dmd hello.d
$ ./hello
Hello, world!
$ _

Знаком $ обозначено приглашение консоли вашей ОС (это может быть c:\Путь\К\Папке> в Windows или /путь/к/каталогу% в системах семейства UNIX, таких как OSX, Linux, Cygwin). Применив пару известных вам приемов систем-фу, вы сможете добиться автоматической компиляции программы при ее запуске. Пользователи Windows, вероятно, захотят привязать программу rdmd.exe (которая устанавливается вместе с компилятором D) к команде Выполнить. UNIX-подобные системы поддерживают запуск скриптов в нотации «shebang»1. D понимает такой синтаксис: добавление строки

#!/usr/bin/rdmd

в самое начало программы в файле hello.d позволяет компилировать ее автоматически перед исполнением. Внеся это изменение, просто введите в командной строке:

$ chmod u+x hello.d
$ ./hello.d
Hello, world!
$ _

(chmod нужно ввести только один раз).

Для всех операционных систем справедливо следующее: программа rdmd достаточно «умна», для того чтобы кэшировать сгенерированное приложение. Так что фактически компиляция выполняется только после изменения исходного кода программы, а не при каждом запуске. Эта особенность в сочетании с высокой скоростью самого компилятора позволяет экономить время на запусках программы между внесением в нее изменений, что одинаково полезно как при разработке больших систем, так и при написании маленьких скриптов.

Программа hello.d начинается с инструкции

import std.stdio;

которая предписывает компилятору найти модуль с именем std.stdio и сделать его символы доступными для использования. Инструкция import напоминает препроцессорную директиву #include, которую можно встретить в синтаксисе C и С++, но семантически она ближе команде import языка Python: никакой вставки текста подключаемого модуля в текст основной программы не происходит выполняется только простое расширение таблицы символов. Если повторно применить инструкцию import к тому же файлу, ничего не произойдет.

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

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

1.1. Числа и выражения

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

/*
    Рассчитать значения роста в сантиметрах для заданного диапазона значений в футах и дюймах
*/
import std.stdio;

void main()
{
    // Значения, которые никогда не изменятся
    immutable inchesPerFoot = 12;
    immutable cmPerInch = 2.54;
    // Перебираем и пишем
    foreach (feet; 5 .. 7)
    {
        foreach (inches; 0 .. inchesPerFoot)
        {
            writeln(feet, "'", inches, "''\t", (feet * inchesPerFoot + inches) * cmPerInch);
        }
    }
}

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

5'0''   152.4
5'1''   154.94
5'2''   157.48
...
6'10''  208.28
6'11''  210.82

Инструкция foreach (feet; 5..7) {...} это цикл, где определена целочисленная переменная feet, с которой последовательно связываются значения 5 и 6 (значение 7 она не принимает, так как интервал открыт справа). Как и Java, C++ и C#, D поддерживает /* многострочные комментарии */ и // однострочные комментарии (и, кроме того, документирующие комментарии, о которых позже). Еще одна интересная деталь нашей маленькой программы способ объявления данных. Во-первых, введены две константы:

immutable inchesPerFoot = 12;
immutable cmPerInch = 2.54;

Константы, значения которых никогда не изменятся, определяются с помощью ключевого слова immutable. Как и переменные, константы не требуют явного задания типа: тип задается значением, которым инициализируется константа или переменная. В данном случае литерал 12 говорит компилятору о том, что inchesPerFoot это целочисленная константа (обозначается в D с помощью знакомого int); точно так же литерал 2.54 заставляет cmPerInch стать константой с плавающей запятой (типа double). Далее мы обнаруживаем те же магические способности у определений feet и inches: они выглядят как «обычные» переменные, но безо всяких «украшений», свидетельствующих о каком-либо типе. Это не делает программу менее безопасной по сравнению с той, где типы переменных и констант заданы явно:

immutable int inchesPerFoot = 12;
immutable double cmPerInch = 2.54;
...
foreach (int feet; 5 .. 7)
{
    ...
}

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

Целые типы со знаком в порядке возрастания размера: byte, short, int и long, занимающие 8, 16, 32 и 64 бита соответственно. У каждого из этих типов есть «двойник» без знака того же размера, названный в соответствии с простым правилом: ubyte, ushort, uint и ulong. (Здесь нет модификатора unsigned, как в C). Типы с плавающей запятой: float (32-битное число одинарной точности в формате IEEE 754), double (64-битное в формате IEEE 754) и real (занимает столько, сколько позволяют регистры, предназначенные для хранения чисел с плавающей запятой, но не меньше 64 бит; например, на компьютерах фирмы Intel real это так называемое расширенное 79-битное число двойной точности в формате IEEE 754).

Вернемся к нашим целым числам. Литералы, такие как 42, подходят под определение любого числового типа, но заметим, что компилятор проверяет, достаточно ли вместителен «целевой» тип для этого значения. Поэтому определение

immutable byte inchesPerFoot = 12;

ничем не хуже аналогичного без byte, поскольку 12 можно с таким же успехом представить 8 битами, а не 32. По умолчанию, если вывод о «целевом» типе делается по числу (как в программе-примере), целочисленные константы «воспринимаются» как int, а дробные как double.

Вы можете построить множество выражений на D, используя эти типы, арифметические операторы и функции. Операторы и их приоритеты сходны с теми, что можно найти в языках-собратьях D: +, -, *, / и % для базовых арифметических операций, ==, !=, <, >, <=, >= для сравнений, fun(argument1, argument2) для вызовов функций и т.д.

Вернемся к нашей программе перевода дюймов в сантиметры и отметим две достойные внимания детали вызова функции writeln. Первая: во writeln передаются 5 аргументов (а не один, как в той программе, что установила контакт между вами и миром D). Функция writeln очень похожа на средства ввода-вывода, встречающиеся в языках Паскаль (writeln), C (printf) и C++ (cout). Все они (включая writeln из D) принимают переменное число аргументов (так называемые функции с переменным числом аргументов). Однако в D пользователи могут определять собственные функции с переменным числом аргументов (чего нет в Паскале), которые всегда типизированы (в отличие от C), без излишнего переопределения операторов (как это сделано в С++). Вторая деталь: наш вызов writeln неуклюже сваливает в кучу информацию о форматировании и форматируемые данные. Обычно желательно отделять данные от представления. Поэтому давайте используем специальную функцию writefln, осуществляющую форматированный вывод:

writefln("%s'%s''\t%s", feet, inches, (feet * inchesPerFoot + inches) * cmPerInch);

По-новому организованный вызов дает тот же вывод, но первый аргумент функции writefln полностью описывает формат представления. Со знака % начинаются спецификаторы формата (по аналогии с функцией printf из C): например %d для целых чисел, %f для чисел с плавающей запятой и %s для строк.

Если вы использовали printf прежде, то могли бы почувствовать себя как дома, когда б не маленькая особенность: мы ведь выводим значения переменных типа int и double как же получилось, что и те и другие описаны с помощью спецификатора %s, обычно применяемого для вывода строк? Ответ прост. Средства D для работы с переменным количеством аргументов дают writefln доступ к информации об исходных типах переданных аргументов. Благодаря такому подходу программа получает ряд преимуществ: 1) значение %s может быть расширено до «строкового представления по умолчанию для типа переданного аргумента» и 2) если не удалось сопоставить спецификатор формата с типами переданных аргументов, вы получите ошибку в чистом виде, а не загадочное поведение, присущее вызовам printf с неверно заданным форматом (не говоря уже о подрыве безопасности, возможном при вызове printf с непроверяемыми заранее форматирующими строками).

1.2. Инструкции

В языке D, как и в других родственных ему языках, любое выражение, после которого стоит точка с запятой, это инструкция (например в программе «Hello, world!» сразу после вызова writeln есть ;). Действие инструкции сводится к вычислению выражения.

D член семейства с фигурными скобками и с блочной областью видимости». Это означает, что вы можете объединять несколько команд в одну, помещая их в { и }, что порой обязательно, например при желании сделать сразу несколько вещей в цикле foreach. В случае единственной команды вы вправе смело опустить фигурные скобки. На самом деле, весь наш двойной цикл, вычисляющий значения роста, можно переписать так:

import std.stdio;

void main()
{
    // Значения, которые никогда не изменятся
    immutable inchesPerFoot = 12;
    immutable cmPerInch = 2.54;
    // Перебираем и пишем
    foreach (feet; 5 .. 7)
        foreach (inches; 0 .. inchesPerFoot)
            writeln(feet, "'", inches, "''\t", (feet * inchesPerFoot + inches) * cmPerInch);
}

У пропуска фигурных скобок для одиночных инструкций есть как преимущество (более короткий код), так и недостаток редактирование кода становится более утомительным (в процессе отладки придется повозиться с инструкциями, то добавляя, то удаляя скобки). Когда речь заходит о правилах расстановки отступов и фигурных скобок, мнения сильно расходятся. На самом деле, пока вы последовательны в своем выборе, все это не так важно, как может показаться. В качестве доказательства: стиль, предлагаемый в этой книге (обязательное заключение в операторные скобки даже одиночных инструкций, открывающая скобка на одной строке с соответствующим оператором, закрывающие скобки на отдельных строках), по типографским причинам отличается от реально применяемого автором. А раз он мог спокойно это пережить, не превратившись в оборотня, то и любой сможет.

Благодаря языку Python стал популярен иной способ отражения блочной структуры программы с помощью отступов (чудесное воплощение принципа «форма соответствует содержанию»). Для программистов на других языках утверждение, что пробел имеет значение, всего лишь нелепая фраза, но для тех, кто пишет на Python, это зарок. D обычно игнорирует пробелы, но он разработан с прицелом на легкость синтаксического разбора (т. е. чтобы при разборе не приходилось выяснять значения символов). А это подразумевает, что в рамках скромного «комнатного» проекта можно реализовать простой препроцессор, позволяющий использовать для выделения блоков инструкций отступы (как в Python) без каких-либо неудобств во время компиляции, исполнения и отладки программ.

Кроме того, вам должна быть хорошо знакома инструкция if:

if (выражение) инструкция1 else инструкция2

Чисто теоретический вывод, известный как принцип структурного программирования, гласит, что все алгоритмы можно реализовать с помощью составных инструкций, if-проверок и циклов а-ля for и foreach. Разумеется, любой адекватный язык (как и D) предлагает гораздо больше, но мы пока постановим, что с нас довольно и этих инструкций, и двинемся дальше.

1.3. Основы работы с функциями

Оставим пока в стороне обязательное определение функции main и посмотрим, как определяются другие функции на D. Определение функции соответствует модели, характерной и для других Алгол-подобных языков: сначала пишется возвращаемый тип, потом имя функции и, наконец, заключенный в круглые скобки список формальных аргументов, разделенных запятыми. Например, определение функции с именем pow, которая принимает значения типа double и int, а возвращает double, записывается так:

double pow(double base, int exponent)
{
    ...
}

Каждый параметр функции (base и exponent в данном примере) кроме типа может иметь необязательный класс памяти (storage class), определяющий способ передачи аргумента в функцию при ее вызове2.

По умолчанию аргументы передаются в pow по значению. Если перед типом параметра указан класс памяти ref, то параметр привязывается напрямую к входному аргументу, так что изменение параметра непосредственно отражается на значении, полученном извне. Например:

import std.stdio;

void fun(ref uint x, double y)
{
    x = 42;
    y = 3.14;
}

void main()
{
    uint a = 1;
    double b = 2;
    fun(a, b);
    writeln(a, " ", b);
}

Эта программа печатает 42 2, потому что x определен как ref uint, то есть когда значение присваивается x, на самом деле операция проводится с a. С другой стороны, присваивание значения переменной y никак не скажется на b, поскольку y это внутренняя копия в распоряжении функции fun.

Последние «украшения», которые мы обсудим в этом кратком введении, это in и out. Попросту говоря, in данное функцией «обещание» только смотреть на параметр, не «трогая» его. Указание out в определении параметра функции действует сходно с ref, с той поправкой, что параметр принудительно инициализируется своим значением по умолчанию при «входе» в функцию. (Для каждого типа T определено начальное значение, обозначаемое как T.init. Пользовательские типы могут определять собственное значение по умолчанию.)

О функциях можно еще долго рассказывать. Можно передавать функции другим функциям, встраивать одну в другую, разрешать функции сохранять свою локальную среду (полнофункциональная синтаксическая клауза), создавать анонимные функции (лямбда-функции), с удобством манипулировать ими и еще множество дополнительных «вкусностей». Со временем мы доберемся до каждой из них.


  1. «Shebang» (от shell bang: shell консоль, bang восклицательный знак), или «shabang» (# sharp) обозначение пути к компилятору или интерпретатору в виде #!/путь/к/программе. Прим. пер. ↩︎

  2. В этой книге под «параметром» понимается значение, используемое внутри функции, а под «аргументом» значение, передаваемое в функцию извне. ↩︎