This commit is contained in:
Alexander Zhirov 2023-01-22 12:49:07 +03:00
parent b2bfe5c7a1
commit a88d089c2e
1 changed files with 427 additions and 8 deletions

View File

@ -3,14 +3,15 @@
- [1.1. Числа и выражения](#1-1-числа-и-выражения)
- [1.2. Инструкции](#1-2-инструкции)
- [1.3. Основы работы с функциями](#1-3-основы-работы-с-функциями)
- 1.4. Массивы и ассоциативные массивы
- [1.4.1. Работа со словарем]()
- [1.4.2. Получение среза массива. Функции с обобщенными типами параметров. Тесты модулей]()
- [1.4.3. Подсчет частот. Лямбда-функции]()
- [1.5. Основные структуры данных]()
- [1.6. Интерфейсы и классы]()
- [1.6.1. Больше статистики. Наследование]()
- [1.7. Значения против ссылок]()
- [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-итоги)
Вы ведь знаете, с чего обычно начинают, так что без лишних слов:
@ -218,5 +219,423 @@ void main()
О функциях можно еще долго рассказывать. Можно передавать функции другим функциям, встраивать одну в другую, разрешать функции сохранять свою локальную среду (полнофункциональная синтаксическая клауза), создавать анонимные функции (лямбда-функции), с удобством манипулировать ими и еще множество дополнительных «вкусностей». Со временем мы доберемся до каждой из них.
## 1.4. Массивы и ассоциативные массивы
Массивы и ассоциативные массивы (которые обычно называют хеш-таблицами, или хешами) пожалуй, наиболее часто используемые сложные структуры данных за всю историю машинных вычислений, завистливо преследуемые списками языка Лисп. Множество полезных программ не требуют ничего, кроме массива или ассоциативного массива. Так что пришло время посмотреть, как D их реализует.
### 1.4.1. Работаем со словарем
Для примера напишем простенькую программку, следуя такой спецификации:
> Читать текст, состоящий из слов, разделенных пробелами, и сопоставлять каждому не встречавшемуся до сих пор при чтении слову уникальное число. Вывод организовать в виде строк формата:
```sh
идентификатор слово
```
Такой маленький скрипт вполне может пригодиться, когда вы захотите обработать какой-нибудь текст. Построив словарь, вы получите возможность манипулировать только числами (что дешевле), а не полновесными словами. Один из вариантов построения такого словаря накапливать уже прочитанные слова в ассоциативном массиве, отображающем слова на целые числа. При добавлении нового соответствия достаточно убедиться, что число, связываемое со словом, уникально («железная» гарантия просто использовать текущую длину массива, в результате чего получится последовательность идентификаторов 0, 1, 2, ...). Посмотрим, как это можно реализовать на D.
```d
import std.stdio, std.string;
void main() {
size_t [string] dictionary;
foreach (line; stdin.byLine()) {
// Разбить строку на слова
// Добавить каждое слово строки в словарь
foreach (word; splitter(strip(line))) {
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 binarySearch(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; splitter(strip(line))) {
++freqs[word.idup];
}
}
// Напечатать таблицу частот
foreach (key, value; freqs) {
writefln("%6u\t%s", value, key);
}
}
```
А теперь, скачав из Сети файл hamlet.txt[^6] (который вы найдете по прямой ссылке http://erdani.com/tdpl/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.ctype, std.regex,
std.range, std.stdio, std.string;
struct PersonaData {
uint totalWordsSpoken;
uint[string] wordCount;
}
void main() {
// Накапливает информацию о главных героях
PersonaData[string] info;
// Заполнить info
string currentParagraph;
foreach (line; stdin.byLine()) {
if (line.startsWith(" ")
&& line.length > 4
&& isalpha(line[4])) {
// Персонаж продолжает высказывание
currentParagraph ~= line[3 .. $];
} 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, ". ");
if (sentence.empty) {
return;
}
auto persona = line[0 .. $ - sentence.length];
sentence = tolower(strip(sentence[2 .. $]));
// Выделить произнесенные слова
auto words = split(sentence, regex("[ \t,.;:?]+"));
// Insert or update information
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. Интерфейсы и классы
### 1.6.1. Больше статистики. Наследование
## 1.7. Значения против ссылок
## 1.8. Итоги
[^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, нечисло) хорошее начальное значение по умолчанию для чисел с плавающей запятой. К сожалению, для целых чисел не существует эквивалентного начального значения.