28 KiB
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
. Пользовательские типы могут определять собственное значение по умолчанию.)
О функциях можно еще долго рассказывать. Можно передавать функции другим функциям, встраивать одну в другую, разрешать функции сохранять свою локальную среду (полнофункциональная синтаксическая клауза), создавать анонимные функции (лямбда-функции), с удобством манипулировать ими и еще множество дополнительных «вкусностей». Со временем мы доберемся до каждой из них.
-
«Shebang» (от shell bang: shell – консоль, bang – восклицательный знак), или «shabang» (# – sharp) – обозначение пути к компилятору или интерпретатору в виде
#!/путь/к/программе
. – Прим. пер. ↩︎ -
В этой книге под «параметром» понимается значение, используемое внутри функции, а под «аргументом» – значение, передаваемое в функцию извне. ↩︎