Глава 1 закончена
This commit is contained in:
parent
a88d089c2e
commit
c83ed6cf42
|
@ -237,12 +237,17 @@ void main()
|
||||||
|
|
||||||
```d
|
```d
|
||||||
import std.stdio, std.string;
|
import std.stdio, std.string;
|
||||||
void main() {
|
import std.algorithm;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
size_t [string] dictionary;
|
size_t [string] dictionary;
|
||||||
foreach (line; stdin.byLine()) {
|
foreach (line; stdin.byLine())
|
||||||
|
{
|
||||||
// Разбить строку на слова
|
// Разбить строку на слова
|
||||||
// Добавить каждое слово строки в словарь
|
// Добавить каждое слово строки в словарь
|
||||||
foreach (word; splitter(strip(line))) {
|
foreach (word; line.strip.splitter)
|
||||||
|
{
|
||||||
if (word in dictionary) continue; // Ничего не делать
|
if (word in dictionary) continue; // Ничего не делать
|
||||||
auto newID = dictionary.length;
|
auto newID = dictionary.length;
|
||||||
dictionary[word.idup] = newID;
|
dictionary[word.idup] = newID;
|
||||||
|
@ -250,6 +255,7 @@ writeln(newID, '\t', word);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
В языке D ассоциативный массив (хеш-таблица), который значениям типа `K` ставит в соответствие значения типа `V`, обозначается как `V[K]`. Итак, переменная `dictionary` типа `size_t[string]` сопоставляет строкам целые числа без знака – как раз то, что нам нужно для хранения соответствий слов идентификаторам. Выражение `word in dictionary` истинно, если ключевое слово `word` можно найти в ассоциативном массиве `dictionary`. Наконец, вставка в словарь выполняется так: `dictionary[word.idup] = newID`[^3].
|
В языке D ассоциативный массив (хеш-таблица), который значениям типа `K` ставит в соответствие значения типа `V`, обозначается как `V[K]`. Итак, переменная `dictionary` типа `size_t[string]` сопоставляет строкам целые числа без знака – как раз то, что нам нужно для хранения соответствий слов идентификаторам. Выражение `word in dictionary` истинно, если ключевое слово `word` можно найти в ассоциативном массиве `dictionary`. Наконец, вставка в словарь выполняется так: `dictionary[word.idup] = newID`[^3].
|
||||||
|
@ -321,8 +327,11 @@ b = a.dup; // Полностью скопировать a в b
|
||||||
|
|
||||||
```d
|
```d
|
||||||
import std.array;
|
import std.array;
|
||||||
bool binarySearch(T)(T[] input, T value) {
|
|
||||||
while (!input.empty) {
|
bool binarySearch(T)(T[] input, T value)
|
||||||
|
{
|
||||||
|
while (!input.empty)
|
||||||
|
{
|
||||||
auto i = input.length / 2;
|
auto i = input.length / 2;
|
||||||
auto mid = input[i];
|
auto mid = input[i];
|
||||||
if (mid > value) input = input[0 .. i];
|
if (mid > value) input = input[0 .. i];
|
||||||
|
@ -331,7 +340,9 @@ else return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
unittest {
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
assert(binarySearch([ 1, 3, 6, 7, 9, 15 ], 6));
|
assert(binarySearch([ 1, 3, 6, 7, 9, 15 ], 6));
|
||||||
assert(!binarySearch([ 1, 3, 6, 7, 9, 15 ], 5));
|
assert(!binarySearch([ 1, 3, 6, 7, 9, 15 ], 5));
|
||||||
}
|
}
|
||||||
|
@ -357,12 +368,14 @@ assert(binarySearch!(int)([ 1, 3, 6, 7, 9, 15 ], 6));
|
||||||
|
|
||||||
```d
|
```d
|
||||||
import std.array;
|
import std.array;
|
||||||
bool binarySearch(T)(T[] input, T value) {
|
|
||||||
|
bool binarySearchR(T)(T[] input, T value)
|
||||||
|
{
|
||||||
if (input.empty) return false;
|
if (input.empty) return false;
|
||||||
auto i = input.length / 2;
|
auto i = input.length / 2;
|
||||||
auto mid = input[i];
|
auto mid = input[i];
|
||||||
if (mid > value) return binarySearch(input[0 .. i], value);
|
if (mid > value) return binarySearch(input[0 .. i], value);
|
||||||
if (mid < value) return binarySearch(input[i + 1 .. $]], value);
|
if (mid < value) return binarySearch(input[i + 1 .. $], value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -377,22 +390,36 @@ return true;
|
||||||
|
|
||||||
```d
|
```d
|
||||||
import std.algorithm, std.stdio, std.string;
|
import std.algorithm, std.stdio, std.string;
|
||||||
void main() {
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
// Рассчитать таблицу частот
|
// Рассчитать таблицу частот
|
||||||
uint[string] freqs;
|
uint[string] freqs;
|
||||||
foreach (line; stdin.byLine()) {
|
foreach (line; stdin.byLine())
|
||||||
foreach (word; splitter(strip(line))) {
|
{
|
||||||
|
foreach (word; line.strip.splitter)
|
||||||
|
{
|
||||||
++freqs[word.idup];
|
++freqs[word.idup];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Напечатать таблицу частот
|
// Напечатать таблицу частот
|
||||||
foreach (key, value; freqs) {
|
foreach (key, value; freqs)
|
||||||
|
{
|
||||||
writefln("%6u\t%s", value, key);
|
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] (который вы найдете по прямой ссылке http://erdani.com/tdpl/hamlet.txt) и запустив нашу маленькую программу с шекспировским шедевром в качестве аргумента, вы получите:
|
А теперь, скачав из Сети файл `hamlet.txt`[^6] (который вы найдете по прямой [ссылке](hamlet.txt)) и запустив нашу маленькую программу с шекспировским шедевром в качестве аргумента, вы получите:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
1 outface
|
1 outface
|
||||||
|
@ -500,26 +527,31 @@ PersonaData[string] info;
|
||||||
Управляющая функция `main` читает входную последовательность физических строк, сцепляет их в логические строки (игнорируя все, что не подходит под наш шаблон), передает полученные полные реплики в функцию-накопитель и в конце печатает требуемую информацию:
|
Управляющая функция `main` читает входную последовательность физических строк, сцепляет их в логические строки (игнорируя все, что не подходит под наш шаблон), передает полученные полные реплики в функцию-накопитель и в конце печатает требуемую информацию:
|
||||||
|
|
||||||
```d
|
```d
|
||||||
import std.algorithm, std.conv, std.ctype, std.regex,
|
import std.algorithm, std.conv, std.regex, std.range, std.stdio, std.string, std.ascii;
|
||||||
std.range, std.stdio, std.string;
|
|
||||||
struct PersonaData {
|
struct PersonaData
|
||||||
|
{
|
||||||
uint totalWordsSpoken;
|
uint totalWordsSpoken;
|
||||||
uint[string] wordCount;
|
uint[string] wordCount;
|
||||||
}
|
}
|
||||||
void main() {
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
// Накапливает информацию о главных героях
|
// Накапливает информацию о главных героях
|
||||||
PersonaData[string] info;
|
PersonaData[string] info;
|
||||||
// Заполнить info
|
// Заполнить info
|
||||||
string currentParagraph;
|
string currentParagraph;
|
||||||
foreach (line; stdin.byLine()) {
|
foreach (line; stdin.byLine())
|
||||||
if (line.startsWith(" ")
|
{
|
||||||
&& line.length > 4
|
// 4 символа отступа
|
||||||
&& isalpha(line[4])) {
|
if (line.startsWith(" ") && line.length > 4 && isAlpha(line[4]))
|
||||||
|
{
|
||||||
// Персонаж продолжает высказывание
|
// Персонаж продолжает высказывание
|
||||||
currentParagraph ~= line[3 .. $];
|
currentParagraph ~= line[3 .. $];
|
||||||
} else if (line.startsWith(" ")
|
}
|
||||||
&& line.length > 2
|
// 2 символа отступа
|
||||||
&& isalpha(line[2])) {
|
else if (line.startsWith(" ") && line.length > 2 && isAlpha(line[2]))
|
||||||
|
{
|
||||||
// Персонаж только что начал говорить
|
// Персонаж только что начал говорить
|
||||||
addParagraph(currentParagraph, info);
|
addParagraph(currentParagraph, info);
|
||||||
currentParagraph = to!string(line[2 .. $]);
|
currentParagraph = to!string(line[2 .. $]);
|
||||||
|
@ -545,24 +577,31 @@ printResults(info);
|
||||||
Строка, которую обрабатывает `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` сочтет любую последовательность знаков, упомянутых между `[` и `]`, одним из разделителей слов. Теперь мы готовы, как говорится, приносить большую пользу с помощью всего лишь маленького кусочка кода:
|
Строка, которую обрабатывает `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
|
```d
|
||||||
void addParagraph(string line, ref PersonaData[string] info) {
|
void addParagraph(string line, ref PersonaData[string] info)
|
||||||
|
{
|
||||||
// Выделить имя персонажа и его реплику
|
// Выделить имя персонажа и его реплику
|
||||||
line = strip(line);
|
line = strip(line);
|
||||||
auto sentence = std.algorithm.find(line, ". ");
|
// auto sentence = std.algorithm.find(line, ". ");
|
||||||
if (sentence.empty) {
|
auto sentence = line.find(". ");
|
||||||
|
if (sentence.empty)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto persona = line[0 .. $ - sentence.length];
|
auto persona = line[0 .. $ - sentence.length];
|
||||||
sentence = tolower(strip(sentence[2 .. $]));
|
sentence = toLower(strip(sentence[2 .. $]));
|
||||||
// Выделить произнесенные слова
|
// Выделить произнесенные слова
|
||||||
auto words = split(sentence, regex("[ \t,.;:?]+"));
|
auto words = split(sentence, regex("[ \t,.;:?]+"));
|
||||||
// Insert or update information
|
// Вставка или обновление информации
|
||||||
if (!(persona in info)) {
|
if (!(persona in info))
|
||||||
|
{
|
||||||
// Первая реплика персонажа
|
// Первая реплика персонажа
|
||||||
info[persona] = PersonaData();
|
info[persona] = PersonaData();
|
||||||
}
|
}
|
||||||
info[persona].totalWordsSpoken += words.length;
|
info[persona].totalWordsSpoken += words.length;
|
||||||
foreach (word; words) ++info[persona].wordCount[word];
|
foreach (word; words)
|
||||||
|
{
|
||||||
|
++info[persona].wordCount[word];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -625,12 +664,222 @@ Ambassador 41 34
|
||||||
|
|
||||||
## 1.6. Интерфейсы и классы
|
## 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. Больше статистики. Наследование
|
### 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. Значения против ссылок
|
## 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. Итоги
|
## 1.8. Итоги
|
||||||
|
|
||||||
|
Эта глава – вводная, поэтому какие-то детали отдельных примеров и концепций остались за кадром или были рассмотрены вскользь. При этом опытный программист легко поймет, как можно завершить и усовершенствовать код примеров.
|
||||||
|
|
||||||
|
Надеюсь, что-то интересное нашлось для каждого. Кодера-практика, противника любых излишеств, могла порадовать чистота синтаксиса массивов и ассоциативных массивов. Уже эти две концепции сильно упрощают ежедневное кодирование и полезны как для малых, так и для больших проектов. Поклонник объектно-ориентированного программирования, хорошо знакомый с интерфейсами и классами, мог отметить хорошую масштабируемость языка для крупных проектов. А те, кто хочет писать на D короткие скрипты, увидели, как легко пишутся и запускаются сценарии, манипулирующие файлами.
|
||||||
|
|
||||||
|
Как водится, полный рассказ гораздо длиннее. И все же полезно время от времени вернуться к основам и удостовериться, что простые вещи остаются простыми.
|
||||||
|
|
||||||
[^1]: «Shebang» (от shell bang: shell – консоль, bang – восклицательный знак), или «shabang» (# – sharp) – обозначение пути к компилятору или интерпретатору в виде `#!/путь/к/программе`. – *Прим. пер.*
|
[^1]: «Shebang» (от shell bang: shell – консоль, bang – восклицательный знак), или «shabang» (# – sharp) – обозначение пути к компилятору или интерпретатору в виде `#!/путь/к/программе`. – *Прим. пер.*
|
||||||
[^2]: В этой книге под «параметром» понимается значение, используемое внутри функции, а под «аргументом» – значение, передаваемое в функцию извне.
|
[^2]: В этой книге под «параметром» понимается значение, используемое внутри функции, а под «аргументом» – значение, передаваемое в функцию извне.
|
||||||
[^3]: `.idup` – свойство любого массива, возвращающее неизменяемую (immutable) копию массива. Про неизменяемость будет рассказано позже, пока же следует знать, что ключ ассоциативного массива должен быть неизменяемым. – *Прим. науч. ред.*
|
[^3]: `.idup` – свойство любого массива, возвращающее неизменяемую (immutable) копию массива. Про неизменяемость будет рассказано позже, пока же следует знать, что ключ ассоциативного массива должен быть неизменяемым. – *Прим. науч. ред.*
|
||||||
|
@ -639,3 +888,4 @@ Ambassador 41 34
|
||||||
[^6]: Этот файл содержит текст пьесы «Гамлет». – *Прим. пер.*
|
[^6]: Этот файл содержит текст пьесы «Гамлет». – *Прим. пер.*
|
||||||
[^7]: Ham (англ.) – ветчина. – *Прим. пер.*
|
[^7]: Ham (англ.) – ветчина. – *Прим. пер.*
|
||||||
[^8]: NaN (Not a Number, нечисло) – хорошее начальное значение по умолчанию для чисел с плавающей запятой. К сожалению, для целых чисел не существует эквивалентного начального значения.
|
[^8]: NaN (Not a Number, нечисло) – хорошее начальное значение по умолчанию для чисел с плавающей запятой. К сожалению, для целых чисел не существует эквивалентного начального значения.
|
||||||
|
[^9]: Это не совсем так. Переменная-`аккумулятор` должна быть инициализирована значением `double.max` и соответственно переименована. – *Прим. науч. ред.*
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue