Compare commits

..

3 Commits

Author SHA1 Message Date
Alexander Zhirov c83ed6cf42 Глава 1 закончена 2023-01-22 14:29:42 +03:00
Alexander Zhirov a88d089c2e 1.4 2023-01-22 12:49:07 +03:00
Alexander Zhirov b2bfe5c7a1 1.3 2023-01-22 11:36:37 +03:00
2 changed files with 5180 additions and 11 deletions

View File

@ -1,16 +1,17 @@
# 1. Знакомство с языком D
- [1.1. Числа и выражения](#11-числа-и-выражения)
- [1.2. Инструкции](#12-инструкции)
- [1.3. Основы работы с функциями]()
- 1.4. Массивы и ассоциативные массивы
- [1.4.1. Работа со словарем]()
- [1.4.2. Получение среза массива. Функции с обобщенными типами параметров. Тесты модулей]()
- [1.4.3. Подсчет частот. Лямбда-функции]()
- [1.5. Основные структуры данных]()
- [1.6. Интерфейсы и классы]()
- [1.6.1. Больше статистики. Наследование]()
- [1.7. Значения против ссылок]()
- [1.1. Числа и выражения](#1-1-числа-и-выражения)
- [1.2. Инструкции](#1-2-инструкции)
- [1.3. Основы работы с функциями](#1-3-основы-работы-с-функциями)
- [1.4. Массивы и ассоциативные массивы](#1-4-массивы-и-ассоциативные-массивы)
- [1.4.1. Работа со словарем](#1-4-1-работаем-со-словарем)
- [1.4.2. Получение среза массива. Функции с обобщенными типами параметров. Тесты модулей](#1-4-2-получение-среза-массива-функции-с-обобщенными-типами-параметров-тесты-модулей)
- [1.4.3. Подсчет частот. Лямбда-функции](#1-4-3-подсчет-частот-лямбда-функции)
- [1.5. Основные структуры данных](#1-5-основные-структуры-данных)
- [1.6. Интерфейсы и классы](#1-6-интерфейсы-и-классы)
- [1.6.1. Больше статистики. Наследование](#1-6-1-больше-статистики-наследование)
- [1.7. Значения против ссылок](#1-7-значения-против-ссылок)
- [1.8. Итоги](#1-8-итоги)
Вы ведь знаете, с чего обычно начинают, так что без лишних слов:
@ -178,4 +179,713 @@ if (‹выражение›) инструкция1 else ‹инструк
Чисто теоретический вывод, известный как принцип структурного программирования, гласит, что все алгоритмы можно реализовать с помощью составных инструкций, `if`-проверок и циклов а-ля `for` и `foreach`. Разумеется, любой адекватный язык (как и D) предлагает гораздо больше, но мы пока постановим, что с нас довольно и этих инструкций, и двинемся дальше.
## 1.3. Основы работы с функциями
Оставим пока в стороне обязательное определение функции `main` и посмотрим, как определяются другие функции на D. Определение функции соответствует модели, характерной и для других Алгол-подобных языков: сначала пишется возвращаемый тип, потом имя функции и, наконец, заключенный в круглые скобки список формальных аргументов, разделенных запятыми. Например, определение функции с именем `pow`, которая принимает значения типа `double` и `int`, а возвращает `double`, записывается так:
```d
double pow(double base, int exponent)
{
...
}
```
Каждый параметр функции (`base` и `exponent` в данном примере) кроме типа может иметь необязательный ***класс памяти*** (***storage class***), определяющий способ передачи аргумента в функцию при ее вызове[^2].
По умолчанию аргументы передаются в `pow` по значению. Если перед типом параметра указан класс памяти `ref`, то параметр привязывается напрямую к входному аргументу, так что изменение параметра непосредственно отражается на значении, полученном извне. Например:
```d
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.4. Массивы и ассоциативные массивы
Массивы и ассоциативные массивы (которые обычно называют хеш-таблицами, или хешами) пожалуй, наиболее часто используемые сложные структуры данных за всю историю машинных вычислений, завистливо преследуемые списками языка Лисп. Множество полезных программ не требуют ничего, кроме массива или ассоциативного массива. Так что пришло время посмотреть, как D их реализует.
### 1.4.1. Работаем со словарем
Для примера напишем простенькую программку, следуя такой спецификации:
> Читать текст, состоящий из слов, разделенных пробелами, и сопоставлять каждому не встречавшемуся до сих пор при чтении слову уникальное число. Вывод организовать в виде строк формата:
```sh
идентификатор слово
```
Такой маленький скрипт вполне может пригодиться, когда вы захотите обработать какой-нибудь текст. Построив словарь, вы получите возможность манипулировать только числами (что дешевле), а не полновесными словами. Один из вариантов построения такого словаря накапливать уже прочитанные слова в ассоциативном массиве, отображающем слова на целые числа. При добавлении нового соответствия достаточно убедиться, что число, связываемое со словом, уникально («железная» гарантия просто использовать текущую длину массива, в результате чего получится последовательность идентификаторов 0, 1, 2, ...). Посмотрим, как это можно реализовать на D.
```d
import std.stdio, std.string;
import std.algorithm;
void main()
{
size_t [string] dictionary;
foreach (line; stdin.byLine())
{
// Разбить строку на слова
// Добавить каждое слово строки в словарь
foreach (word; line.strip.splitter)
{
if (word in dictionary) continue; // Ничего не делать
auto newID = dictionary.length;
dictionary[word.idup] = newID;
writeln(newID, '\t', word);
}
}
}
```
В языке D ассоциативный массив (хеш-таблица), который значениям типа `K` ставит в соответствие значения типа `V`, обозначается как `V[K]`. Итак, переменная `dictionary` типа `size_t[string]` сопоставляет строкам целые числа без знака как раз то, что нам нужно для хранения соответствий слов идентификаторам. Выражение `word in dictionary` истинно, если ключевое слово `word` можно найти в ассоциативном массиве `dictionary`. Наконец, вставка в словарь выполняется так: `dictionary[word.idup] = newID`[^3].
Хотя в рассмотренном сценарии не отражается явно тот факт, что тип `string` на самом деле массив знаков, это так. В общем виде динамический массив элементов типа `T` обозначается как `T[]` и может определяться различными способами:
```d
int[] a = new int[20]; // 20 целых чисел, инициализированных нулями
int[] b = [ 1, 2, 3 ]; // Массив, содержащий 1, 2, и 3
```
В отличие от массивов C, массивы D «знают» собственную длину. Для любого массива `arr` это значение доступно как `arr.length`. Присваивание значения `arr.length` перераспределяет память, выделенную под массив. При попытке обращения к элементам массива проверяется, не выходит ли запрашиваемый индекс за границу массива. Любители рискнуть переполнением буфера могут «выдрать» указатель из массива (используя `arr.ptr`) и затем выполнять непроверенные арифметические операции над ним. Кроме того, если вам действительно нужно все, что может дать кремниевая пластина, есть опция компилятора, отменяющая проверку границ. Можно сказать, что к безопасности ведет путь наименьшего сопротивления. Код безопасен по умолчанию, а если поработать, можно сделать его чуть более быстрым.
Вот как можно проходить по массиву с помощью новой формы уже знакомой инструкции `foreach`:
```d
int[] arr = new int[20];
foreach (elem; arr)
{
/* ... использовать elem... */
}
```
Этот цикл по очереди связывает переменную `elem` с каждым элементом массива `arr`. Присваивание `elem` не влияет на элементы `arr`. Чтобы изменить массив таким способом, просто используйте ключевое слово `ref`:
```d
// Обнулить все элементы arr
foreach (ref elem; arr)
{
elem = 0;
}
```
Теперь, когда мы знаем, как `foreach` работает с массивами, рассмотрим еще один полезный прием. Если в теле цикла вам потребуется индекс элемента массива, `foreach` может рассчитать его для вас:
```d
int[] months = new int[12];
foreach (i, ref e; months)
{
e = i + 1;
}
```
Этот код заполняет массив числами от 1 до 12. Такой цикл эквивалентен чуть более многословному определению (см. ниже), использующему `foreach` для просмотра диапазона чисел:
```d
foreach (i; 0 .. months.length)
{
months[i] = i + 1;
}
```
D также предлагает массивы фиксированного размера, обозначаемые, например, как `int[5]`. За исключением отдельных специализированных приложений, предпочтительнее использовать динамические массивы, поскольку обычно размер массива вам заранее неизвестен.
Семантика копирования массивов неочевидна: копирование одной переменной типа массив в другую не копирует весь массив; эта операция порождает лишь новую ссылку на ту же область памяти. Если вам действительно хочется заполучить копию, просто используйте свойство массива `.dup`:
```d
int[] a = new int[100];
int[] b = a;
// ++x увеличивает на 1 значение x
++b[10]; // В b[10] теперь 1, в a[10] то же
b = a.dup; // Полностью скопировать a в b
++b[10]; // В b[10] теперь 2, а в a[10] остается 1
```
### 1.4.2. Получение среза массива. Функции с обобщенными типами параметров. Тесты модулей
Получение среза массива это мощное средство, позволяющее ссылаться на часть массива, в действительности не копируя данные массива. В качестве иллюстрации напишем функцию двоичного поиска, реализующую одноименный алгоритм: получив упорядоченный массив и значение, двоичный поиск быстро возвращает логический результат, сообщающий, есть ли заданное значение в массиве. Функция из стандартной библиотеки D возвращает более информативный ответ, чем просто булево значение, но знакомство с ней придется отложить, так как для этого необходимо более глубокое знание языка. Позволим себе, однако, приподнять планку, задавшись целью написать функцию, которая будет работать не только с массивами целых чисел, но с массивами элементов любого типа, допускающего сравнение с помощью операции `<`. Оказывается, реализовать эту задумку можно без особого труда. Вот как выглядит функция обобщенного двоичного поиска `binarySearch`:
```d
import std.array;
bool binarySearch(T)(T[] input, T value)
{
while (!input.empty)
{
auto i = input.length / 2;
auto mid = input[i];
if (mid > value) input = input[0 .. i];
else if (mid < value) input = input[i + 1 .. $];
else return true;
}
return false;
}
unittest
{
assert(binarySearch([ 1, 3, 6, 7, 9, 15 ], 6));
assert(!binarySearch([ 1, 3, 6, 7, 9, 15 ], 5));
}
```
Знаки `(T)` в сигнатуре функции `binarySearch` обозначают *параметр типа* с именем `T`. `T` становится псевдонимом переданного типа в теле этой функции. Затем параметр типа можно использовать в обычном списке параметров функции. При вызове `binarySearch` компилятор определит значение `T` по фактическим аргументам. Если вы хотите указать `T` явно (например, для надежности), то можете написать:
```d
assert(binarySearch!(int)([ 1, 3, 6, 7, 9, 15 ], 6));
```
что обнаруживает возможность вызова обобщенной функции с двумя заключенными в круглые скобки последовательностями аргументов. Сначала следуют заданные во время компиляции аргументы, заключенные в `!(...)`, а за ними получаемые во время исполнения программы аргументы в `(...)`. Объединение этих двух «владений» в одно обдумывалось, но эксперименты показали, что такая унификация создает больше проблем, чем решает.
Если вы знакомы с аналогичными средствами Java, C# и C++, то, вероятно, вам сразу бросилось в глаза то, что D сделал шаг в сторону от этих языков, отказавшись применять угловые скобки `<` и `>` для обозначения аргументов, заданных во время компиляции. Это осознанное решение. Его цель избежать горького опыта C++ (возросшее количество трудностей при синтаксическом разборе, гекатомба в виде множества специальных правил и тай-брейков плюс ко всему неясный синтаксис, осложняющий жизнь пользователя своей двусмысленностью[^4]). Проблема в том, что знаки `<` и `>` являются операторами сравнения[^5]]. Это делает их использование в качестве разделителей очень двусмысленным, учитывая тот факт, что *внутри* этих разделителей разрешены выражения. Таким «кандидатам в разделители» очень сложно подыскать замену. Языкам Java и C# живется легче: они запрещают писать выражения внутри `<` и `>`. Однако этим они ограничивают свою расширяемость ради сомнительного преимущества. D разрешает использовать выражения в качестве аргументов, заданных во время компиляции. Было решено упростить жизнь как человеку, так и компьютеру, наделив дополнительным смыслом традиционный унарный оператор `!` (используемый в логических операциях) и задействовав классические круглые скобки (которые, уверен, вы всегда сможете верно сопоставить друг другу).
Другая любопытная деталь нашей реализации бинарного поиска употребление `auto` для запуска алгоритма, строящего предположения о типах по контексту программы: типы переменных `i` и `mid` определены из выражений, которыми они инициализируются.
В стремлении придерживаться хорошего тона при написании программ к `binarySearch` был добавлен тест модуля. Тесты модулей вводятся в виде блоков, озаглавленных ключевым словом `unittest` (файл может содержать сколько угодно конструкций `unittest`, поскольку, как известно, проверок много не бывает). Чтобы перед входом в функцию `main` запустить тест, передайте компилятору флаг `-unittest`. Хотя `unittest` кажется незначительной деталью, такая конструкция помогает соблюдать хороший стиль программирования: с ее помощью вставлять тесты так легко, что было бы странно не делать этого. Кроме того, если вы привыкли создавать программы «сверху вниз» и предпочитаете видеть сначала тест модуля, а реализацию потом, смело вставляйте `unittest` до `binarySearch`; в D семантика символов на уровне модуля никогда не зависит от их расположения относительно других символов на том же уровне.
Операция получения среза `input[a .. b]` возвращает срез массива `input` от `a` до `b`, исключая индекс `b`. Если `a == b`, будет возвращен пустой срез, а если `a > b`, генерируется исключительная ситуация. Операция получения среза не влечет за собой динамическое выделение памяти; это всего лишь создание новой ссылки на часть массива. Символ `$` внутри выражения индексации или получения среза обозначает длину массива, к которому осуществляется доступ; например, `input[0 .. $]` это то же самое, что и просто `input`.
Повторимся: несмотря на кажущееся обилие перемещений, производимых функцией `binarySearch`, память под новые массивы не была выделена ни разу. Предложенную реализацию алгоритма ни в коей мере нельзя назвать менее эффективной по сравнению с традиционной реализацией, которую характеризует постоянное вычисление индексов. Однако, без сомнения, новая реализация проще для понимания, поскольку она оперирует меньшим количеством состояний. В свете разговора о состояниях напишем рекурсивную реализацию `binarySearch`, которая вообще не переопределяет `input`:
```d
import std.array;
bool binarySearchR(T)(T[] input, T value)
{
if (input.empty) return false;
auto i = input.length / 2;
auto mid = input[i];
if (mid > value) return binarySearch(input[0 .. i], value);
if (mid < value) return binarySearch(input[i + 1 .. $], value);
return true;
}
```
Рекурсивная реализация явно проще и концентрированнее по сравнению со своим итеративным собратом. Кроме того, она ничуть не менее эффективна, так как рекурсивные вызовы оптимизируются благодаря популярной среди компиляторов технике, известной как *оптимизация хвостовой рекурсии*. В двух словах: если функция возвращает просто вызов самой себя (но с другими аргументами), компилятор модифицирует аргументы и инициирует переход к началу функции.
### 1.4.3. Подсчет частот. Лямбда-функции
Поставим себе задачу написать еще одну полезную программу, которая будет подсчитывать частоту употребления слов в заданном тексте. Хотите знать, какие слова употребляются в «Гамлете» чаще всего? Тогда вы как раз там, где надо.
Следующая программа использует ассоциативный массив, сопоставляющий строки переменным типа `uint`, и имеет структуру, напоминающую структуру программы построения словаря, рассмотренной в предыдущем примере. Чтобы сделать программу подсчета частот полностью полезной, в нее добавлен простой цикл печати:
```d
import std.algorithm, std.stdio, std.string;
void main()
{
// Рассчитать таблицу частот
uint[string] freqs;
foreach (line; stdin.byLine())
{
foreach (word; line.strip.splitter)
{
++freqs[word.idup];
}
}
// Напечатать таблицу частот
foreach (key, value; freqs)
{
writefln("%6u\t%s", value, key);
}
// Напечатать таблицу частот с сортировкой
string[] words = freqs.keys;
sort!((a, b) { return freqs[a] > freqs[b]; })(words);
foreach (word; words)
{
writefln("%6u\t%s", freqs[word], word);
}
}
```
А теперь, скачав из Сети файл `hamlet.txt`[^6] (который вы найдете по прямой [ссылке](hamlet.txt)) и запустив нашу маленькую программу с шекспировским шедевром в качестве аргумента, вы получите:
```sh
1 outface
1 come?
1 blanket,
1 operant
1 reckon
2 liest
1 Unhand
1 dear,
1 parley.
1 share.
...
```
И, к сожалению, обнаружите, что вывод неупорядочен: слова, которые напечатаны в первых строках, далеко не самые часто встречающиеся. Что неудивительно для ускорения реализации примитивов ассоциативных массивов элементы в них могут храниться в любом порядке.
Для того чтобы отсортировать вывод по убыванию частоты употребления слов, вы можете просто передать вывод программы утилите `sort` с флажком `-nr` (от numerically отсортировать по числам и reversed в обратном порядке), но это своего рода хитрость. Чтобы добавить сортировку непосредственно в нашу программу, заменим последний цикл следующим кодом:
```d
// Напечатать таблицу частот
string[] words = freqs.keys;
sort!((a, b) { return freqs[a] > freqs[b]; })(words);
foreach (word; words)
{
writefln("%6u\t%s", freqs[word], word);
}
```
Свойство `.keys` позволяет получить только ключи ассоциативного массива `freqs` в виде массива строк, под который выделяется новая область памяти. Этого не избежать, ведь нам требуется делать перестановки. Мы получили код
```d
sort!((a, b) { return freqs[a] > freqs[b]; })(words);
```
который соответствует недавно рассмотренной нотации:
```d
sort!(‹аргументы времени компиляции›)(‹аргументы времени исполнения›);
```
Взяв текст, заключенный в первые круглые скобки `!(...)`, мы получим форму записи, напоминающую незаконченную функцию как будто ее автор забыл о типах параметров, возвращаемом типе и о самом имени функции:
```d
(a, b) { return freqs[a] > freqs[b]; }
```
Это *лямбда-функция* небольшая анонимная функция, которая обычно создается для того, чтобы потом передавать ее другим функциям в качестве аргумента. Лямбда-функции используются постоянно и повсеместно, поэтому разработчики D сделали все возможное, чтобы избавить программиста от синтаксической нагрузки, прежде неизбежной при определении таких функций: типы параметров и возвращаемый тип выясняются из контекста. Это действительно имеет смысл, потому что тело лямбда-функции по определению там, где нужно автору, читателю и компилятору, то есть здесь нет места разночтениям и принципы модульности не нарушаются.
В связи с лямбда-функцией, определенной в этом примере, стоит упомянуть еще одну деталь. Лямбда-функция осуществляет доступ к переменной `freqs`, локальной переменной функции `main`, а значит, лямбда-функция не является ни глобальной, ни статической. Это больше напоминает подход Лиспа, а не C, и позволяет работать с очень мощными лямбда-конструкциями. И хотя обычно за такое преимущество приходится платить неявными вызовами функций во время исполнения программы, D гарантирует отсутствие таких вызовов (и, следовательно, ничем не ограниченные возможности реализации инлайнинга).
Вывод измененной программы:
```sh
929 the
680 and
625 of
608 to
523 I
453 a
444 my
382 in
361 you
358 Ham.
...
```
Что и ожидалось: самые часто употребляемые слова набрали больше всего «очков». Настораживает лишь «Ham»[^7]. Это слово в пьесе вовсе не отражает кулинарные предпочтения героев. «Ham» всего лишь сокращение от «Hamlet» (Гамлет), которым помечена каждая из его реплик. Явно у него был повод высказаться 358 раз больше, чем любой другой герой пьесы. Далее по списку следует король всего 116 реплик, меньше трети сказанного Гамлетом. А Офелия с ее 58 репликами просто молчунья.
## 1.5. Основные структуры данных
Раз уж мы взялись за «Гамлета», проанализируем этот текст чуть глубже. Например, соберем кое-какую информацию о главных героях: сколько всего слов было произнесено каждым персонажем и насколько богат его (ее) словарный запас. Для этого с каждым действующим лицом понадобится связать несколько фактов. Чтобы сосредоточить эту информацию в одном месте, определим такую структуру данных:
```d
struct PersonaData
{
uint totalWordsSpoken;
uint[string] wordCount;
}
```
В языке D понятия структуры (`struct`) и классы (`class`) четко разделены. С точки зрения удобства они во многом схожи, но устанавливают разные правила: структуры это типы значений, а классы были задуманы для реализации динамического полиморфизма, поэтому экземпляры классов могут быть доступны исключительно по ссылке. Упразднены связанное с этим непонимание, ошибки при копировании экземпляров классов потомков в переменные классов-предков и комментарии а-ля `// Нет! НЕ наследуй!`. Разрабатывая тип, вы с самого начала должны решить, будет ли это мономорфный тип-значение или полиморфная ссылка. Общеизвестно, что C++ разрешает определять типы, принадлежность которых к тому или иному разряду неочевидна, но эти типы редко используются и чреваты ошибками. В целом достаточно оснований сознательно отказаться от них.
В нашем случае требуется просто собрать немного данных, и мы не планируем использовать полиморфные типы, поэтому тип `struct` хороший выбор. Теперь определим ассоциативный массив, отображающий имена персонажей на дополнительную информацию о них (значения типа `PersonaData`):
```d
PersonaData[string] info;
```
Все, что нам требуется, это правильно заполнить `info` данными из `hamlet.txt`. Придется немного потрудиться: реплика героя может простираться на несколько строк, и нам понадобится простая обработка, сцепляющая эти физические строки в одну логическую. Чтобы понять, как это сделать, обратимся к небольшому фрагменту файла `hamlet.txt`, познаково представленному ниже (предшествующие тексту пробелы для наглядности отображаются видимыми знаками):
```sh
˽˽Pol. Marry, I will teach you! Think yourself a baby
˽˽˽˽That you have ta'en these tenders for true pay,
˽˽˽˽Which are not sterling. Tender yourself more dearly,
˽˽˽˽Or (not to crack the wind of the poor phrase,
˽˽˽˽Running it thus) you'll tender me a fool.
˽˽Oph. My lord, he hath importun'd me with love
˽˽˽˽In honourable fashion.
˽˽Pol. Ay, fashion you may call it. Go to, go to!
```
До сих пор гадают, не было ли истинной причиной гибели Полония злоупотребление инструкцией `goto`. Но нас больше интересует другое: заметим, что реплике каждого персонажа предшествуют ровно два пробела, за ними следует имя, после которого стоит точка, потом пробел, за которым наконец-то начинается само высказывание. Если логическая строка занимает несколько физических строк, то каждая следующая строка всегда начинается с четырех пробелов. Можно осуществить простое сопоставление шаблону, воспользовавшись регулярными выражениями (для работы с которыми предназначен модуль `std.regex`), но мы хотим научиться работать с массивами, поэтому выполним сопоставление «вручную». Призовем на помощь лишь логическую функцию `a.startsWith(b)`, определенную в модуле `std.algorithm`, которая сообщает, начинается ли `a` с `b`.
Управляющая функция `main` читает входную последовательность физических строк, сцепляет их в логические строки (игнорируя все, что не подходит под наш шаблон), передает полученные полные реплики в функцию-накопитель и в конце печатает требуемую информацию:
```d
import std.algorithm, std.conv, std.regex, std.range, std.stdio, std.string, std.ascii;
struct PersonaData
{
uint totalWordsSpoken;
uint[string] wordCount;
}
void main()
{
// Накапливает информацию о главных героях
PersonaData[string] info;
// Заполнить info
string currentParagraph;
foreach (line; stdin.byLine())
{
// 4 символа отступа
if (line.startsWith(" ") && line.length > 4 && isAlpha(line[4]))
{
// Персонаж продолжает высказывание
currentParagraph ~= line[3 .. $];
}
// 2 символа отступа
else if (line.startsWith(" ") && line.length > 2 && isAlpha(line[2]))
{
// Персонаж только что начал говорить
addParagraph(currentParagraph, info);
currentParagraph = to!string(line[2 .. $]);
}
}
// Закончили, теперь напечатаем собранную информацию
printResults(info);
}
```
Зная, как работают массивы, мы без труда читаем этот код, за исключением конструкции `to!string(line[2 .. $])`. Зачем она нужна и что будет, если о ней забыть?
Цикл `foreach`, последовательно считывая из стандартного потока ввода строки текста, размещает их в переменной `line`. Поскольку не имеет смысла выделять память под новый буфер при чтении следующей строки, в каждой итерации `byLine` заново использует место, выделенное для `line`. Тип самой переменной `line char[]`, массив знаков.
Если вы всего лишь, считав, «обследуете» каждую строчку, а потом забываете о ней, в любом случае (как с `to!string(line[2 .. $])`, так и без нее) все будет работать гладко. Но если вы желаете создать код, который будет где-то накапливать содержание читаемых строк, лучше позаботиться о том, чтобы он их действительно копировал. Очевидно, было задумано реально хранить текст в переменной `currentParagraph`, а не использовать ее как временное пристанище, так что необходимо получать дубликаты; отсюда и присутствие конструкции `to!string`, которая преобразует любое выражение в строку. Переменные типа `string` неизменяемы, а `to` гарантирует приведение к этому типу созданием дубликата.
Если забыть написать `to!string` и впоследствии код все же скомпилируется, в результате получится бессмыслица, и ошибку будет довольно-таки сложно обнаружить. Очень неприятно отлаживать программу, одна часть которой изменяет данные, находящиеся в другой части программы, потому что это уже не локальные изменения (трудно представить, сколько вызовов `to` можно забыть при написании большой программы). К счастью, это не причина для беспокойства: типы переменных `line` и `currentParagraph` соответствуют роли этих переменных в программе. Переменная `line` имеет тип `char[]`, представляющий собой массив знаков, которые можно перезаписывать в любой момент; переменная `currentParagraph` имеет тип `string` массив знаков, которые нельзя изменять по отдельности. (Для самых любопытных: полное имя типа `string` `immutable(char)[]`, что дословно означает «непрерывный диапазон неизменяемых знаков». Мы вернемся к разговору о строках в главе 4.) Эти переменные не могут ссылаться на одну и ту же область памяти, поскольку `line` нарушает обязательство `currentParagraph` не изменять знаки по отдельности. Поэтому компилятор отказывает в компиляции ошибочного кода и требует копию, которую вы и предоставляете благодаря преобразованию в строку с помощью конструкции `to!string`. И все счастливы.
С другой стороны, если постоянно копировать строковые значения, то нет необходимости дублировать данные на нижнем уровне их представления переменные просто могут ссылаться на одну и ту же область памяти, которая наверняка не будет перезаписана. Это делает копирование переменных типа `string` безопасным и эффективным одновременно. Но это еще не все плюсы. Строки можно без проблем разделять между потоками, потому что данные типа `string` неизменяемы, так что возможность конфликта при обращении к памяти попросту отсутствует. Неизменяемость это действительно здорово. С другой стороны, если вам потребуется интенсивно изменять знаки по отдельности, возможно, вы предпочтете использовать тип `char[]`, хотя бы временно.
Структура `PersonData` в том виде, в каком она задана выше, очень проста. Однако в общем случае структуры могут определять не только данные, но и другие сущности, такие как частные (приватные, закрытые) разделы (обозначаются ключевым словом `private`), функции-члены, тесты модулей, операторы, конструкторы и деструкторы. По умолчанию любой элемент структуры инициализируется значением по умолчанию (ноль для целых чисел, NaN для чисел с плавающей запятой[^8] и `null` для массивов и других типов, доступ к которым не осуществляется напрямую. А теперь реализуем функцию `addParagraph`, которая разбивает строку текста на слова и распределяет их по ассоциативному массиву.
Строка, которую обрабатывает `main`, имеет вид: `"Ham. To be, or not to be, that is the question."`. Для того чтобы отделить имя персонажа от слов, которые он произносит, нам требуется найти первый разделитель `". "`. Для этого используем функцию `find`. Выражение `haystack.find(needle)` возвращает правую часть `haystack`, начинающуюся с первого вхождения `needle`. (Если `needle` в `haystack` отсутствует, то вызов `find` с такими аргументами вернет пустую строку.) Пока мы формируем словарь, не мешает немного прибраться. Во-первых, нужно преобразовать фразу к нижнему регистру, чтобы слово с заглавной и со строчной буквы воспринималось как одна и та же словарная единица. Об этом легко позаботиться с помощью вызова функции `tolower`. Второе, что необходимо сделать, удалить мощный источник шума знаки пунктуации, которые превращают, к примеру, `«him.`» и «`him`» в разные слова. Для того чтобы очистить словарь, достаточно передать функции `split` единственный дополнительный параметр. Имеется в виду регулярное выражение, которое уничтожит всю «шелуху»: `regex("[ \t,.;:?]+")`. Получив такой аргумент, функция `split` сочтет любую последовательность знаков, упомянутых между `[` и `]`, одним из разделителей слов. Теперь мы готовы, как говорится, приносить большую пользу с помощью всего лишь маленького кусочка кода:
```d
void addParagraph(string line, ref PersonaData[string] info)
{
// Выделить имя персонажа и его реплику
line = strip(line);
// auto sentence = std.algorithm.find(line, ". ");
auto sentence = line.find(". ");
if (sentence.empty)
{
return;
}
auto persona = line[0 .. $ - sentence.length];
sentence = toLower(strip(sentence[2 .. $]));
// Выделить произнесенные слова
auto words = split(sentence, regex("[ \t,.;:?]+"));
// Вставка или обновление информации
if (!(persona in info))
{
// Первая реплика персонажа
info[persona] = PersonaData();
}
info[persona].totalWordsSpoken += words.length;
foreach (word; words)
{
++info[persona].wordCount[word];
}
}
```
Функция `addParagraph` отвечает за обновление ассоциативного массива. В случае если персонаж еще не высказывался, код вставляет «пустой» объект типа `PersonaData`, инициализированный значениями по умолчанию. Поскольку значение по умолчанию для типа `uint` ноль, а созданный в соответствии с правилами по умолчанию ассоциативный массив пуст, только что вставленный слот готов к приему осмысленной информации.
Наконец, для того чтобы напечатать краткую сводку по каждому персонажу, реализуем функцию `printResults`:
```d
void printResults(PersonaData[string] info)
{
foreach (persona, data; info)
{
writefln("%20s %6u %6u", persona, data.totalWordsSpoken, data.wordCount.length);
}
}
```
Готовы к тест-драйву? Тогда сохраните и запустите!
```sh
Queen 1104 500
Ros 738 338
For 55 45
Fort 74 61
Gentlemen 4 3
Other 105 75
Guil 349 176
Mar 423 231
Capt 92 66
Lord 70 49
Both 44 24
Oph 998 401
Ghost 683 350
All 20 17
Player 16 14
Laer 1507 606
Pol 2626 870
Priest 92 66
Hor 2129 763
King 4153 1251
Cor., Volt 11 11
Both [Mar 8 8
Osr 379 179
Mess 110 79
Sailor 42 36
Servant 11 10
Ambassador 41 34
Fran 64 47
Clown 665 298
Gent 101 77
Ham 11901 2822
Ber 220 135
Volt 150 112
Rey 80 37
```
Тут есть чем позабавиться. Как и ожидалось, наш дружок «Ham» с большим отрывом выигрывает у всех остальных, получив львиную долю слов. Довольно интересна роль Вольтиманда («Volt»): он немногословен, но при скромном количестве реплик виртуозно демонстрирует солидный словарный запас. Еще любопытнее в этом плане роль матроса («Sailor»), который вообще почти не повторяется. Также сравните красноречивую королеву («Queen») с Офелией («Oph»): королева произносит всего на 10% слов больше, чем Офелия, но ее лексикон богаче как минимум на 25%.
В выводе есть немного шума (например, `"Both [Mar"`), который прилежный программист легко устранит и который вряд ли статистически влияет на то, что действительно представляет для нас интерес. Тем не менее исправление последних огрехов поучительное (и рекомендуемое) упражнение.
## 1.6. Интерфейсы и классы
Объектно-ориентированные средства важны для больших проектов; так что, знакомя вас с ними на примере маленьких программ, я рискую выставить себя недоумком. Прибавьте к этому большое желание избежать заезженных примеров с животными и работниками, и сложится довольно неприятная картина. Да, забыл еще кое-что: в маленьких примерах обычно не видны проблемы создания полиморфных объектов, а это очень важно. Что делать бедному автору! К счастью, реальный мир снабдил меня полезным примером в виде относительно небольшой задачи, которая в то же время не имеет удовлетворительного процедурного решения. Обсуждаемый ниже код это переработка небольшого полезного скрипта на языке awk, который вышел далеко за рамки задуманного. Мы вместе пройдем путь до объектно-ориентированного решения одновременно компактного, полного и изящного.
Как насчет небольшой программы, собирающей статистику (в связи с этим назовем ее `stats`)? Пускай ее интерфейс будет простым: имена статистических функций, используемых для вычислений, передаются в `stats` как параметры командной строки, последовательность чисел для анализа поступает в стандартный поток ввода в виде списка (разделитель пробел), статистические результаты печатаются один за другим по одному на строке. Вот пример работы программы:
```sh
$ echo 3 5 1.3 4 10 4.5 1 5 | stats Min Max Average
1
10
4.225
$ _
```
Написанный на скорую руку «непричесанный» скрипт без проблем решит эту задачу. Но в данном случае при увеличении количества статистических функций «лохматость» кода уничтожит преимущества от быстроты его создания. Так что поищем решение получше. Для начала остановимся на простейших статистических функциях: получение минимума, максимума и среднего арифметического. Нащупав легко расширяемый вариант кода, мы получим простор для неограниченной реализации более сложных статистических функций.
Простейший подход к решению задачи в цикле пройтись по входным данным и вычислить всю необходимую статистику. Но выбрать такой путь значит отказаться от идеи масштабируемости программы. Ведь всякий раз, когда нам потребуется добавить новую статистическую функцию, придется подвергать готовый код хирургическому вмешательству. Если мы хотим выполнять только те вычисления, о которых попросили в командной строке, необходимы серьезные изменения. В идеале мы должны заключить все статистические функции в последовательные куски кода. Таким образом, мы расширяем функциональность программы, просто добавляя новый код; это принцип открытости/закрытости во всей красе.
При таком подходе необходимо выяснить, что общего у всех (или хотя бы у большинства) статистических функций. Ведь наша цель обращаться ко всем функциям из одной точки программы, причем унифицированно. Для начала отметим, что `Min` и `Max` отбирают аргументы из входной последовательности по одному, а результат будет готов, как только закончится ввод. Конечный результат одно-единственное число. Также функция `Average` по окончании чтения всех своих аргументов должна выполнить завершающий шаг (разделить накопившуюся сумму на число слагаемых). Кроме того, у каждого алгоритма есть собственное состояние. Если разные вычисления должны предоставлять одинаковый интерфейс для работы с ними и при этом «запоминать» свое состояние, разумный шаг сделать их объектами и определить формальный интерфейс для управления всеми этими объектами и каждым из них в отдельности.
```d
interface Stat
{
void accumulate(double x);
void postprocess();
double result();
}
```
Интерфейс определяет требуемое поведение в виде набора функций. Разумеется, тот, кто замахнется на реализацию интерфейса, должен будет определить все функции в том виде, в каком они заявлены. Раз уж мы заговорили о реализации, давайте посмотрим, как можно определить класс `Min`, так чтобы он повиновался указаниям железной руки интерфейса `Stat`.
```d
class Min : Stat
{
private double min = double.max;
void accumulate(double x)
{
if (x < min)
{
min = x;
}
}
void postprocess() {} // Ничего не делать
double result()
{
return min;
}
}
```
`Min` это *класс*, пользовательский тип, привносящий в D преимущества ООП. С помощью синтаксиса `class Min: Stat` класс `Min` во всеуслышание объявляет, что он реализует интерфейс `Stat`. И `Min` действительно определяет все три функции, продиктованные волей `Stat`, в точности с теми же аргументами и возвращаемыми типами (иначе компилятор не дал бы `Min` просто так проскочить). `Min` содержит всего лишь один закрытый элемент (тот, что помечен директивой `private`) переменную `min` (наименьшее из прочитанных значений) и обновляет ее внутри функции `accumulate`. Начальное значение `Min` *самое большое* число (которое можно представить типом `double`), так что первое же число из входной последовательности заместит его.
Перед тем как определить другие статистические функции, реализуем основной алгоритм нашей программы `stats`, предусматривающий чтение параметров командной строки, создание соответствующих объектов, производящих вычисления (таких как экземпляр класса `Min`, когда через консоль передан аргумент *`Min`*), и манипулирование ими с помощью интерфейса `Stat`.
```d
import std.stdio : writeln, stdin;
import std.exception : enforce;
import stats;
void main(string[] args)
{
Stat[] stats;
foreach (arg; args[1 .. $])
{
auto newStat = cast(Stat) Object.factory("stats." ~ arg);
enforce(newStat, "Invalid statistics function: " ~ arg);
stats ~= newStat;
}
for (double x; stdin.readf(" %s ", &x) == 1;)
{
foreach (s; stats)
{
s.accumulate(x);
}
}
foreach (s; stats)
{
s.postprocess();
writeln(s.result());
}
}
```
Эта небольшая программа творит чудеса. Для начала список параметров `main` отличается от того, что мы видели до сих пор: на этот раз в функцию передается массив строк. Средства библиотеки времени исполнения D инициализируют этот массив параметрами, переданными компилятору из командной строки вместе с именем скрипта для запуска. Первый цикл инициализирует массив `stats` исходя из значений массива `args`. Учитывая, что в D (как и в других языках) первый аргумент это имя самой программы, мы пропускаем первую позицию: нас интересует срез `args[1 .. $]`. Теперь разберемся с командой
```d
auto newStat = cast(Stat) Object.factory("stats." ~ arg);
```
Тут много непонятного, но, как говорят в ситкомах, я все могу объяснить. Во-первых, здесь знак `~` служит бинарным оператором, то есть осуществляет конкатенацию строк. Поэтому если аргумент командной строки `Min`, то результат конкатенации строка `"stats.Min"`, которая и будет передана функции `Object.factory`. `Object` предок всех классов, создаваемых в программах на D. Он определяет статический метод `factory`, который принимает строку, ищет соответствующий тип в небольшой базе данных (которая строится во время компиляции), магическим образом создает объект типа, указанного в переданной строке, и возвращает его. Если запрошенный класс отсутствует в упомянутой базе данных, `Object.factory` возвращает `null`. Чтобы этого не произошло, достаточно определить класс `Min` где-нибудь в том же файле, что и вызов `Object.factory`. Возможность создавать объект по имени его типа это важное средство, востребованное во множестве полезных приложений. На самом деле, оно настолько важно, что является «сердцем» некоторых языков с динамической типизацией. Языки со статической типизацией (такие как D и Java) вынуждены полагаться на средства своих библиотек времени исполнения или предоставлять программисту самостоятельно изобретать механизмы регистрации и распознавания типов.
Почему `stats.Min`, а не просто `Min`? D серьезно относится к принципу модульности, поэтому в этом языке отсутствует глобальное пространство имен, где кто угодно может складировать что угодно. Каждый символ обитает в рамках модуля со своим именем, и по умолчанию имя модуля совпадает с именем его исходного файла без расширения. Таким образом, при условии что наш файл назван `stats.d`, D полагает, что всякое имя, определенное в этом файле, принадлежит модулю `stats`.
Осталась последняя загвоздка. Статический тип только что полученного объекта типа `Min` на самом деле не `Min`. Это звучит странно, но легко объясняется тем, что, вызвав `Object.factory("что угодно")`, вы можете создать *любой* объект, поэтому возвращаемый тип должен быть неким общим знаменателем для всех возможных объектных типов и это `Object`. Для того чтобы получить ссылку, соответствующую типу объекта, который вы задумали, необходимо преобразовать объект, возвращенный `Object.factory`, в объект типа `State`. Эта операция называется *приведением типов (type casting)*. В языке D выражение `cast(T) expr` приводит выражение `expr` к типу `T`. Операции приведения типов, в которых участвуют классы или интерфейсы, всегда проверяются, поэтому код надежно защищен от дураков.
Оглянувшись назад, мы заметим, что львиная доля того, что делает скрипт, выполняется в первых пяти его строках. Эта самая сложная часть, которая полностью определяет весь остальной код. Второй цикл читает по одному числу за раз (об этом заботится функция `readf`) и вызывает `accumulate` для всех объектов, собирающих статистику. Функция `readf` возвращает число объектов, успешно прочитанных согласно заданной строке формата. В нашем случае формат задан в виде строки `" %s "`, что означает «один элемент, окруженный любым количеством пробелов». (Тип элемента определяется типом считанного элемента, в нашем случае `x` принимает значение типа `double`.) Последнее, что делает программа, выводит результаты вычислений на печать.
### 1.6.1. Больше статистики. Наследование
Реализация `Max` так же тривиальна, как и реализация `Min`; за исключением небольших изменений в `accumulate`, эти классы ничем не отличаются друг от друга[^9]. Даже если новое задание до боли напоминает предыдущее, в голову должна приходить мысль «интересно», а не «о боже, какая скука». Рутинные задачи это возможность для повторного использования, и «правильные» языки, способные лучше эксплуатировать различные преимущества подобия, по некоторой абстрактной шкале качества должны оцениваться выше. Нам придется выяснить, что именно общего у функций `Min` и `Max` (и, в идеале, у прочих статистических функций). Присмотревшись к ним, можно заметить, что обе принадлежат к разряду статистических функций, результат которых вычисляется шаг за шагом и может быть вычислен всего по одному числу. Назовем такую категорию статистических функций *пошаговыми функциями*.
```d
class IncrementalStat : Stat
{
protected double _result;
abstract void accumulate(double x);
void postprocess() {}
double result()
{
return _result;
}
}
```
Абстрактный класс можно воспринимать как частичное обязательство: он реализует некоторые методы, но не все, так что «самостоятельно» такой код работать не может. Материализуется абстрактный класс тогда, когда от него наследуют и в теле потомков завершают реализацию. Класс `IncrementalStat` обслуживает повторяющийся код классов, реализующих интерфейс `Stat`, но оставляет реализацию метода `accumulate` своим потомкам. Вот как выглядит новая версия класса `Min`:
```d
class Min : IncrementalStat
{
this()
{
_result = double.max;
}
override void accumulate(double x)
{
if (x < _result)
{
_result = x;
}
}
}
```
Кроме того, в классе `Min` определен конструктор в виде специальной функции `this()`, необходимый для корректной инициализации результата. Даже несмотря на добавление конструктора, полученный код значительно улучшил ситуацию относительно исходного положения дел, особенно с учетом того факта, что множество других статистических функций также соответствуют этому шаблону (например, сумма, дисперсия, среднее арифметическое, стандартное отклонение). Посмотрим на реализацию функции получения среднего арифметического, поскольку это прекрасный повод представить еще пару концепций:
```d
class Average : IncrementalStat
{
private uint items = 0;
this()
{
_result = 0;
}
override void accumulate(double x)
{
_result += x;
++items;
}
override void postprocess()
{
if (items)
{
_result /= items;
}
}
}
```
Начнем с того, что в `Average` вводится еще одно поле, `items`, которое инициализируется нулем с помощью синтаксиса `items = 0` (только для того, чтобы показать, как надо инициализировать переменные, но, как отмечалось выше, целые числа и так инициализируются нулем по умолчанию). Второе, что необходимо отметить: `Average` определяет конструктор, который присваивает переменной `_result` ноль. Так сделано, потому что, в отличие от минимума или максимума, при отсутствии аргументов среднее арифметическое считается равным нулю. И хотя может показаться, что инициализировать `_result` значением NaN только для того, чтобы тут же записать в эту переменную ноль, бессмысленное действие, уход от так называемого «мертвого присваивания» представляет собой легкую добычу для любого оптимизатора. Наконец, `Average` переопределяет метод `postprocess`, несмотря на то что в классе `IncrementalStat` он уже определен. В языке D по умолчанию можно переопределить (унаследовать и заново определить) методы любого класса, но надо обязательно добавлять директиву `override`, чтобы избежать всевозможных несчастных случаев (таких как неудача переопределения в связи с какой-нибудь опечаткой или изменением в базовом типе, либо переопределение чего-нибудь по ошибке). Если вы поставите перед методом класса ключевое слово `final`, то запретите классам-потомкам переопределять эту функцию (что эффективно останавливает механизм динамического поиска методов по дереву классов).
## 1.7. Значения против ссылок
Проведем небольшой эксперимент:
```d
import std.stdio;
struct MyStruct
{
int data;
}
class MyClass
{
int data;
}
void main()
{
// Играем с объектом типа MyStruct
MyStruct s1;
MyStruct s2 = s1;
++s2.data;
writeln(s1.data); // Печатает 0
// Играем с объектом типа MyClass
MyClass c1 = new MyClass;
MyClass c2 = c1;
++c2.data;
writeln(c1.data); // Печатает 1
}
```
Похоже, игры с объектом типа `MyStruct` сильно отличаются от игр с объектом типа `MyObject`. И в том и в другом случае мы создаем переменную, которую затем копируем в другую переменную, после чего изменяем копию (вспомните, что `++` это унарный оператор, прибавляющий единицу к своему аргументу). Этот эксперимент показывает, что после копирования `c1` и `c2` ссылаются на одну и ту же область памяти с информацией, а `s1` и `s2`, напротив, «живут врозь».
Поведение `MyStruct` свидетельствует о том, что этот объект подчиняется *семантике значений*: каждая переменная ссылается на собственное единственное значение, и присваивание одной переменной другой означает, что значение одной переменной реально копируется в значение другой переменной. Исходное значение, по образу и подобию которого изменяли вторую переменную, остается нетронутым, и обе переменные далее продолжают развиваться независимо друг от друга. Поведение `MyClass` говорит, что объект этого типа подчиняется *ссылочной семантике*: значения создаются явно (в нашем случае с помощью вызова `new MyClass`), и присваивание одного экземпляра класса другому означает лишь то, что обе переменные будут ссылаться на одно и то же значение в памяти.
Со значениями легко работать, о них просто рассуждать, и они позволяют производить эффективные вычисления с переменными небольшого размера. С другой стороны, нетривиальные программы сложно реализовать, не обладая средствами доступа к переменным без их копирования. Отсутствие возможности работать со ссылками препятствует, например, работе с типами, ссылающимися на себя же (списки или деревья), или структурами, ссылающимися друг на друга (такими как дочернее окно, знающее о своем родительском окне). Любой уважающий себя язык реализует работу со ссылками в том или ином виде; спорят только о необходимых умолчаниях. В C в общем случае переменные трактуются как значения, но если пользователь захочет, он будет работать со ссылками с помощью указателей. В дополнение к указателям, C++ определяет ссылочные типы. Любопытно, что чисто функциональные языки могут использовать ссылки или значения, когда сочтут нужным, потому что при написании кода между ними нет разницы. Ведь чисто функциональные языки запрещают изменения, поэтому невозможно сказать, когда они порождают копию значения, а когда просто используют ссылку на него значения «заморожены», поэтому вы не сможете проверить, разделяется ли значение между несколькими переменными, изменив одну из них. Чисто объектно-ориентированные языки, напротив, традиционно поощряют изменения. Для них общий случай ссылочная семантика. Некоторые такие языки достигают умопомрачительной гибкости, допуская, например, динамическое изменение системных переменных. Наконец, некоторые языки избрали гибридный подход, включая как типы-значения, так и ссылочные типы, с разной долей предпочтения тем или другим.
Язык D систематически реализует гибридный подход. Для определения ссылочных типов используйте классы. Для определения типов-значений или гибридных типов используйте структуры. В главах 6 и 7 соответственно описаны конструкторы этих типов, снабженные средствами для реализации соответствующего подхода. Например, структуры не поддерживают динамическое наследование и полиморфизм (такой как в рассмотренной нами программе `stats`), поскольку такое поведение не согласуется с семантикой значений. Динамический полиморфизм объектов это характеристика ссылочной семантики, и любая попытка смешать эти два подхода приведет лишь к жутким последствиям. (Например, классическая опасность, подстерегающая программистов на C++, slicing, неожиданное лишение объекта его полиморфных способностей в результате невнимательного использования этого объекта в качестве значения. В языке D slicing невозможен.)
В завершение хочется сказать, что структуры пожалуй, наиболее гибкое проектное решение. Определив структуру, вы можете вдохнуть в нее любую семантику. Вы можете сделать так, что значение будет копироваться постоянно, реализовать ленивое копирование, а-ля копирование при записи, или подсчитывать ссылки, или выбрать что-то среднее между этими способами. Вы даже можете определить ссылочную семантику, используя классы или указатели *внутри* своей структуры. С другой стороны, некоторые из этих альтернатив требуют подкованности в техническом плане; использование классов, напротив, подразумевает простоту и унифицированность.
## 1.8. Итоги
Эта глава вводная, поэтому какие-то детали отдельных примеров и концепций остались за кадром или были рассмотрены вскользь. При этом опытный программист легко поймет, как можно завершить и усовершенствовать код примеров.
Надеюсь, что-то интересное нашлось для каждого. Кодера-практика, противника любых излишеств, могла порадовать чистота синтаксиса массивов и ассоциативных массивов. Уже эти две концепции сильно упрощают ежедневное кодирование и полезны как для малых, так и для больших проектов. Поклонник объектно-ориентированного программирования, хорошо знакомый с интерфейсами и классами, мог отметить хорошую масштабируемость языка для крупных проектов. А те, кто хочет писать на D короткие скрипты, увидели, как легко пишутся и запускаются сценарии, манипулирующие файлами.
Как водится, полный рассказ гораздо длиннее. И все же полезно время от времени вернуться к основам и удостовериться, что простые вещи остаются простыми.
[^1]: «Shebang» (от shell bang: shell консоль, bang восклицательный знак), или «shabang» (# sharp) обозначение пути к компилятору или интерпретатору в виде `#!/путь/к/программе`. *Прим. пер.*
[^2]: В этой книге под «параметром» понимается значение, используемое внутри функции, а под «аргументом» значение, передаваемое в функцию извне.
[^3]: `.idup` свойство любого массива, возвращающее неизменяемую (immutable) копию массива. Про неизменяемость будет рассказано позже, пока же следует знать, что ключ ассоциативного массива должен быть неизменяемым. *Прим. науч. ред.*
[^4]: Если кто-то из ваших коллег прокачал самоуверенность до уровня Супермена, спросите его, что делает код `object.template fun<arg>()`, и вы увидите криптонит в действии.
[^5]: Усугубляет ситуацию с угловыми скобками то, что `<<` и `>>` тоже операторы.
[^6]: Этот файл содержит текст пьесы «Гамлет». *Прим. пер.*
[^7]: Ham (англ.) ветчина. *Прим. пер.*
[^8]: NaN (Not a Number, нечисло) хорошее начальное значение по умолчанию для чисел с плавающей запятой. К сожалению, для целых чисел не существует эквивалентного начального значения.
[^9]: Это не совсем так. Переменная-`аккумулятор` должна быть инициализирована значением `double.max` и соответственно переименована. *Прим. науч. ред.*

File diff suppressed because it is too large Load Diff