Alexander 2 лет назад
Родитель
Сommit
5063e81e4a
1 измененных файлов с 1438 добавлено и 0 удалено
  1. 1438 0
      12-перегрузка-операторов/README.md

+ 1438 - 0
12-перегрузка-операторов/README.md

@@ -0,0 +1,1438 @@
+# 12. Перегрузка операторов
+
+Мы, программисты, не очень любим слишком уж отделять встроенные
+типы от пользовательских. Магические свойства встроенных типов ме
+шают открытости и расширяемости любого языка, поскольку при этом
+пользовательские типы обречены оставаться второсортными. Тем не
+менее у проектировщиков языка есть все законные основания отно
+ситься к встроенным типам с большим почтением. Одно из таких осно
+ваний: более настраиваемый язык сложнее выучить, а также сложнее
+выполнять его синтаксический анализ как человеку, так и машине. Ка
+ждый язык по-своему определяет приемлемое соотношение между
+встроенным и настраиваемым, что для некоторых языков означает впа
+дение в одну из этих двух крайностей.
+
+Язык D подходит к этому вопросу прагматично: он не умаляет важ
+ность настраиваемости, но при этом осознает практичность встроенных
+типов – D использует преимущества встроенных типов ровно тремя пу
+тями:
+
+1. *Синтаксис названий типов*. Массивы и ассоциативные массивы ис
+пользуются повсеместно, и, согласитесь, синтаксис `int[]` и `int[string]`
+гораздо нагляднее, чем `Array!int` и `AssotiativeArray!(string, int)`. В поль
+зовательском коде нельзя определять новые формы записи названий
+типов, например `int[[]]`.
+
+*Литералы*. Числовые и строковые литералы, как и литералы масси
+вов и ассоциативных массивов, – «особые», и их набор нельзя расши
+рить. «Сборные» объекты-структуры, такие как `Point(5, 3)`, – тоже
+литералы, но тип не может определить новый синтаксис литерала,
+например `(3, 5)pt`.
+
+*Семантика*. Зная семантику определенных типов и их операций,
+компилятор оптимизирует код. Например, встретив выражение
+`"Hello" ~ ", " ~ "world"`, компилятор не откладывает конкатенацию до
+времени исполнения: он знает, что делает операция конкатенации
+строк, и склеивает строки уже во время компиляции. Аналогично
+компилятор упрощает и оптимизирует арифметические выражения,
+используя знание арифметики.
+
+Некоторые языки добавляют к этому списку операторы. Они делают
+операторы особенными; чтобы выполнить какую-либо операцию приме
+нительно к пользовательским типам, приходится использовать стан
+дартные средства языка, такие как вызов функций или макросов. Не
+смотря на то что это совершено законное решение, оно на самом деле
+создает проблемы при большом объеме кода, ориентированного на ариф
+метические вычисления. Многие программы, ориентированные на вы
+числения, определяют собственные типы с алгебрами[^1] (числа неограни
+ченной точности, специализированные числа с плавающей запятой,
+кватернионы, октавы, матрицы всевозможных форм, тензоры, ... оче
+видно, что язык не может сделать встроенными их все). При использова
+нии таких типов выразительность кода резко снижается. По сравнению
+с эквивалентным функциональным синтаксисом, операторы обычно
+требуют меньше места и круглых скобок, а получаемый с их участием
+код зачастую легок для восприятия. Рассмотрим для примера вычисле
+ние среднего гармонического трех ненулевых чисел `x`, `y` и `z`. Выражение
+на основе операторов очень близко к математическому определению:
+
+```d
+m = 3 / (1/x + 1/y + 1/z);
+```
+
+В языке, требующем использовать вызовы функций вместо операторов,
+соответствующее выражение выглядит вовсе не так хорошо:
+
+```d
+m = divide(3, add(add(divide(1, x), divide(1, y)), divide(1, z)));
+```
+
+Читать и изменять код с множеством арифметических функций гораз
+до сложнее, чем код с обычной записью операторов.
+
+Язык D очень привлекателен для численного программирования. Он
+предоставляет надежную арифметику с плавающей запятой и превос
+ходную библиотеку трансцендентных функций, которые иногда возвра
+щают результат с большей точностью, чем «родные» системные библио
+теки, и предлагает широкие возможности для моделирования. Мощное
+средство перегрузки операторов добавляет ему привлекательности. Пе
+регрузка операторов позволяет вам определять собственные числовые
+типы (такие как числа с фиксированной запятой, десятичные числа для
+финансовых и бухгалтерских программ, неограниченные целые числа
+или действительные числа неограниченной точности), максимально
+близкие к встроенным числовым типам. Перегрузка операторов также
+позволяет определять типы с «числоподобными» алгебрами, такие как
+векторы и матрицы. Давайте посмотрим, как можно определять типы
+с помощью этого средства.
+
+## 12.1. Перегрузка операторов в D
+
+Подход D к перегрузке операторов прост: если хотя бы один участник
+выражения с оператором имеет пользовательский тип, компилятор *заменяет* это выражение на обычный вызов метода с регламентирован
+ным именем. Затем применяются обычные правила языка. Таким обра
+зом, перегруженные операторы – лишь синтаксический сахар для вызо
+ва методов, а значит, нет нужды вникать в причуды самостоятельного
+средства языка. Например, если `a` относится к некоторому определенно
+му пользователем типу, выражение `a + 5` заменяется на `a.opBinary!"+"(5)`.
+К методу `opBinary` применяются обычные правила и проверки, и тип `a`
+должен определять этот метод, если желает обеспечить поддержку пе
+регрузки операторов.
+
+Замена (точнее, *снижение*, т. к. этот процесс преобразует конструкции
+более высокого уровня в низкоуровневый код) – очень эффективный ин
+струмент, позволяющий реализовать новые средства на основе имею
+щихся, и D обычно его применяет. Мы уже видели снижение в действии
+применительно к конструкции `scope` (см. раздел 3.13). По сути, `scope` –
+лишь синтаксический сахар, которым засыпаны особым образом сцеп
+ленные конструкции `try`, но вам точно не придет в голову самостоятель
+но писать сниженный код, так как `scope` значительно поднимает уро
+вень высказываний. Перегрузка операторов действует в том же духе,
+определяя все вызовы операторов через замену на вызовы функций, тем
+самым придавая мощь обычным определениям функций и используя
+их как средство достижения своей цели. Без лишних слов посмотрим,
+как компилятор осуществляет снижение операторов разных категорий.
+
+## 12.2. Перегрузка унарных операторов
+
+В случае унарных операторов `+` (плюс), `-` (отрицание), `~` (поразрядное от
+рицание), `*` (разыменование указателя), `++` (увеличение на единицу) и `--`
+(уменьшение на единицу) компилятор заменяет выражение
+
+```d
+‹оп› a
+```
+
+на
+
+```d
+a.opUnary!"‹оп›"()
+```
+
+для всех значений пользовательских типов. В качестве замены высту
+пает вызов метода opUnary с одним аргументом времени компиляции
+`"‹оп›"` и без каких-либо аргументов времени исполнения. Например `++a`
+перезаписывается как `a.opUnary! "++" ()`.
+
+Чтобы перегрузить один или несколько унарных операторов для типа T,
+определите метод T.opUnary так:
+
+```d
+struct T
+{
+    SomeType opUnary(string op)();
+}
+```
+
+В таком виде, как он здесь определен, этот метод будет вызываться для
+всех унарных операторов. А если вы хотите для некоторых операторов
+определить отдельные методы, вам помогут ограничения сигнатуры
+(см. раздел 5.4). Рассмотрим определение типа `CheckedInt`, который слу
+жит оберткой базовых числовых типов и гарантирует, что значения, по
+лучаемые в результате применения операций к оборачиваемым типам,
+не выйдут за границы, установленные для этих типов. Тип `CheckedInt`
+должен быть параметризирован оборачиваемым типом (например, `CheckedInt!int`, `CheckedInt!long` и т. д.). Вот неполное определение `CheckedInt`
+с операторами префиксного увеличения и уменьшения на единицу:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    private N value;
+
+    ref CheckedInt opUnary(string op)() if (op == "++")
+    {
+        enforce(value != value.max);
+        ++value;
+        return this;
+    }
+
+    ref CheckedInt opUnary(string op)() if (op == "--")
+    {
+        enforce(value != value.min);
+        --value;
+        return this;
+    }
+    ...
+}
+```
+
+### 12.2.1. Объединение определений операторов с помощью выражения mixin
+
+Есть очень мощная техника, позволяющая определить не один, а сразу
+группу операторов. Например, все унарные операторы `+`, `-` и `~` для типа
+`CheckedInt` делают одно и то же – всего лишь проталкивают соответст
+вующую операцию по направлению к `value`, внутреннему элементу `CheckedInt`. Хоть эти операторы и неидентичны, они несомненно придержи
+ваются одного и того же шаблона поведения. Можно просто определить
+специальный метод для каждого оператора, но это вылилось бы в неин
+тересное дублирование шаблонного кода. Лучший подход – использо
+вать работающие со строками выражения `mixin` (см. раздел 2.3.4.2), по
+зволяющие напрямую монтировать операции из имен операндов и иден
+тификаторов операторов. Следующий код реализует все унарные опера
+ции, применимые к `CheckedInt`.[^2]
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    private N value;
+
+    this(N value)
+    {
+        this.value = value;
+    }
+
+    CheckedInt opUnary(string op)()
+        if (op == "+" || op == "-" || op == "~")
+    {
+        return CheckedInt(mixin(op ~ "value"));
+    }
+
+    ref CheckedInt opUnary(string op)() if (op == "++" || op == "--")
+    {
+        enum limit = op == "++" ? N.max : N.min;
+        enforce(value != limit);
+        mixin(op ~ "value;");
+        return this;
+    }
+    ...
+}
+```
+
+Это уже заметная экономия на длине кода, и она лишь возрастет, как
+только мы доберемся до бинарных операторов и выражений индекса
+ции. Главное действующее лицо такого подхода – выражение `mixin`, ко
+торое позволяет вам взять строку и попросить компилятор скомпили
+ровать ее. Строка получается буквальным соединением операнда и опе
+ратора вручную. Способность трансформироваться в код, по счастливой
+случайности присущая строке op, фактически воплощает в жизнь эту
+идиому; на самом деле, весь этот механизм перегрузки проектировался
+с прицелом на `mixin`. Раньше D использовал отдельное имя для каждого
+оператора (`opAdd`, `opSub`, `opMul`, ...), что требовало механического запоми
+нания соответствия имен операторам и написания группы функций
+с практически идентичными телами.
+
+### 12.2.2. Постфиксный вариант операторов увеличения и уменьшения на единицу
+
+Постфиксные операторы увеличения (`a++`) и уменьшения (`a--`) на едини
+цу – необычные: они выглядят так же, как и их префиксные «коллеги»,
+так что различать их по идентификатору не получится. Дополнитель
+ная проблема в том, что вызывающему коду, которому нужен результат
+применения оператора, также должно быть доступно и старое значение
+сущности, увеличенной на единицу. Наконец, постфиксные и префикс
+ные варианты операторов увеличения и уменьшения на единицу долж
+ны согласовываться друг с другом.
+
+Постфиксное увеличение и уменьшение на единицу можно целиком
+сгенерировать из префиксного увеличения и уменьшения на единицу
+соответственно – нужно лишь немного шаблонного кода. Но вместо то
+го чтобы заставлять вас писать этот шаблонный код, D делает это сам.
+Замена `a++` выполняется так (постфиксное уменьшение на единицу об
+рабатывается аналогично):
+
+- если результат `a++` не используется, осуществляется замена на `++a`,
+что затем перезаписывается на `a.opUnary! "++"()`;
+- если результат `a++` используется (например, `arr[a++]`), заменой послу
+жит выражение (тяжкий вздох) `((ref x) {auto t=x; ++x; return t;})(a)`.
+
+В первом случае попросту обыгрывается тот факт, что постфиксный
+оператор увеличения на единицу без применения результата делает то
+же самое, что и префиксный вариант соответствующего оператора. Во
+втором случае определяется лямбда-функция (см. раздел 5.6), выполня
+ющая нужный шаблонный код: она создает новую копию входных дан
+ных, прибавляет единицу к входным данным и возвращает созданную
+ранее копию. Лямбда-функция применяется непосредственно к увели
+чиваемому значению.
+
+### 12.2.3. Перегрузка оператора cast
+
+Явное приведение типов осуществляется с помощью унарного операто
+ра, применение которого выглядит как `cast(T) a`. Он немного отличает
+ся от всех остальных операторов тем, что использует тип в качестве па
+раметра, а потому для него выделена особая форма снижения. Для лю
+бого `значения` пользовательского типа и некоторого другого типа T при
+ведение
+
+```d
+cast(T) значение
+```
+
+переписывается как
+
+```d
+значение.opCast!T()
+```
+
+Реализация метода `opCast`, разумеется, должна возвращать значение
+типа `T` – деталь, на которой настаивает компилятор. Несмотря на то что
+перегрузка функций по значению аргумента не обеспечивается на уров
+не средства языка, множественные определения `opCast` можно реализо
+вать с помощью шаблонов с ограничениями сигнатуры. Например, ме
+тоды приведения к типам `string` и `int` для некоторого типа `T` можно
+определить так:
+
+```d
+struct T
+{
+    string opCast(T)() if (is(T == string))
+    {
+        ...
+    }
+
+    int opCast(T)() if (is(T == int))
+    {
+        ...
+    }
+}
+```
+
+Можно определить приведение к целому классу типов. «Надстроим»
+пример с `CheckedInt`, определив приведение ко всем встроенным число
+вым типам. Загвоздка в том, что некоторые из них могут обладать более
+ограничивающим диапазоном значений, а нам бы хотелось гарантиро
+вать, что преобразование не будет сопровождаться никакими потерями
+информации. Дополнительная задача: хотелось бы избежать проверок
+там, где они не требуются (например, нет нужды проверять границы
+при преобразовании из `CheckedInt!int` в `long`). Поскольку информация
+о границах доступна во время компиляции, вставка проверок лишь
+там, где это необходимо, задается с помощью конструкции `static if` (см.
+раздел 3.4):
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    private N value;
+    // Преобразования ко всевозможным целым типам
+    N1 opCast(N1)() if (isIntegral!N1)
+    {
+        static if (N.min < N1.min)
+        {
+            enforce(N1.min <= value);
+        }
+
+        static if (N.max > N1.max)
+        {
+            enforce(N1.max >= value);
+        }
+        // Теперь можно без опаски делать "сырые" преобразования
+        return cast(N1) value;
+    }
+    ...
+}
+```
+
+### 12.2.4. Перегрузка тернарной условной операции и ветвления
+
+Встретив значение пользовательского типа, компилятор заменяет код
+вида
+
+```d
+a ? ‹выражение1› : ‹выражение2›
+```
+
+на
+
+```d
+cast(bool) a ? ‹выражение1› : ‹выражение2›
+```
+
+Сходным образом компилятор переписывает проверку внутри конструк
+ции `if` с
+
+```d
+if (a) ‹инструкция› // С блоком else или без него
+```
+
+на
+
+```d
+if (cast(bool) a) ‹инструкция›
+```
+
+Оператор отрицания `!` также переписывается в виде отрицания выра
+жения с `cast`.
+
+Чтобы обеспечить выполнение таких проверок, определите метод при
+ведения к типу `bool`, как это сделано здесь:
+
+```d
+struct MyArray(T)
+{
+    private T[] data;
+
+    bool opCast(T)() if (is(T == bool))
+    {
+        return !data.empty;
+    }
+    ...
+}
+```
+
+## 12.3. Перегрузка бинарных операторов
+
+В случае бинарных операторов `+` (сложение), `-` (вычитание), `*` (умноже
+ние), `/` (деление), `%` (получение остатка от деления), `&` (поразрядное И),
+`|` (поразрядное ИЛИ), `<<` (сдвиг влево), `>>` (сдвиг вправо), `~` (конкатенация)
+и `in` (проверка на принадлежность множеству) выражение
+
+```d
+a ‹оп› b
+```
+
+где по крайней мере один из операндов `a` и `b` имеет пользовательский
+тип, переписывается в виде
+
+```d
+a.opBinary!"‹оп›"(b)
+```
+
+или
+
+```d
+b.opBinaryRight!"‹оп›"(a)
+```
+
+Если разрешение имен и проверки перегрузки успешны лишь для одно
+го из этих вызовов, он выбирается для замены. Если оба вызова допус
+тимы, возникает ошибка в связи с двусмысленностью. Если же не под
+ходит ни один из вызовов, очевидно, что перед нами ошибка «иденти
+фикатор не найден».
+
+Продолжим наш пример с `CheckedInt` из раздела 12.2. Определим для
+этого типа все бинарные операторы:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    private N value;
+    // Сложение
+    CheckedInt opBinary(string op)(CheckedInt rhs) if (op == "+")
+    {
+        auto result = value + rhs.value;
+        enforce(rhs.value >= 0 ? result >= value : result < value);
+        return CheckedInt(result);
+    }
+    // Вычитание
+    CheckedInt opBinary(string op)(CheckedInt rhs) if (op == "-")
+    {
+        auto result = value - rhs.value;
+        enforce(rhs.value >= 0 ? result <= value : result > value);
+        return CheckedInt(result);
+    }
+    // Умножение
+    CheckedInt opBinary(string op)(CheckedInt rhs) if (op == "*")
+    {
+        auto result = value * rhs.value;
+        enforce(value && result / value == rhs.value || rhs.value && result / rhs.value == value || result == 0);
+        return CheckedInt(result);
+    }
+    // Деление и остаток от деления
+    CheckedInt opBinary(string op)(CheckedInt rhs)
+        if (op == "/" || op == "%")
+    {
+        enforce(rhs.value != 0);
+        return CheckedInt(mixin("value" ~ op ~ "rhs.value"));
+    }
+    // Сдвиг
+    CheckedInt opBinary(string op)(CheckedInt rhs)
+        if (op == "<<" || op == ">>" || op == ">>>")
+    {
+        enforce(rhs.value >= 0 && rhs.value <= N.sizeof * 8);
+        return CheckedInt(mixin("value" ~ op ~ "rhs.value"));
+    }
+    // Поразрядные операции (без проверок, переполнение невозможно)
+    CheckedInt opBinary(string op)(CheckedInt rhs)
+        if (op == "&" || op == "|" || op == "^")
+    {
+        return CheckedInt(mixin("value" ~ op ~ "rhs.value"));
+    }
+    ...
+}
+```
+
+(Многие из этих проверок можно осуществить дешевле – с помощью би
+та переполнения, имеющегося у процессоров Intel, который при выпол
+нении арифметических операций или устанавливается, или сбрасыва
+ется. Но это аппаратно-зависимый способ.) Данный код определяет по
+одному отдельному оператору для каждой уникальной проверки. Если
+у двух и более операторов одинаковый код, они всегда объединяются
+в один метод. Это сделано в случае операторов `/` и `%` (поскольку оба они
+выполняют одну и ту же проверку), всех операторов сдвига и трех по
+разрядных операторов, не требующих проверок. Здесь снова применен
+подход, смысл которого – собрать операцию в виде строки, а потом с по
+мощью `mixin` скомпилировать ее в выражение.
+
+### 12.3.1. Перегрузка операторов в квадрате
+
+Если перегрузка операторов означает разрешение типам определять
+собственную реализацию операторов, то перегрузка перегрузки опера
+торов, то есть перегрузка операторов в квадрате, означает разрешение
+типам определять некоторое количество перегруженных версий пере
+груженных операторов.
+
+Рассмотрим выражение `a * 5`, где операнд `a` имеет тип `CheckedInt!int`. Оно
+не скомпилируется, поскольку до сих пор тип `CheckedInt` определял ме
+тод `opBinary` с правым операндом типа `CheckedInt`. Так что для выполне
+ния вычисления в клиентском коде нужно писать `a * CheckedInt!int(5)`,
+что довольно неприятно.
+
+Верный способ решить эту проблему – определить еще одну или не
+сколько дополнительных реализаций метода `opBinary` для типа `CheckedInt!N`, так чтобы на этот раз тип `N` ожидался справа от оператора. Может
+показаться, что определение нового метода `opBinary` потребует изрядного
+объема монотонной работы, но на самом деле достаточно добавить всего
+одну строчку:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    ... // То же, что и раньше
+    // Операции с "сырыми" числами
+    CheckedInt opBinary(string op)(N rhs)
+    {
+        return opBinary!op(CheckedInt(rhs));
+    }
+}
+```
+
+Красота этого подхода в простоте: оператор преобразуется в обычный
+идентификатор, который затем можно передать другой реализации опе
+ратора.
+
+### 12.3.2. Коммутативность
+
+Присутствие `opBinaryRight` требуется в тех случаях, когда тип, опреде
+ляющий оператор, является правым операндом, например, как в выра
+жении `5 * a`. В этом случае тип операнда a имеет шанс «поймать» опера
+тор, лишь определив метод `opBinaryRight!"*"(int)`. Здесь есть некоторая
+избыточность – если, скажем, нужно организовать поддержку опера
+ций, для которых не важно, с какой стороны подставлен целочислен
+ный операнд (например, все равно, `5 * a` или `a * 5`), вам потребуется опре
+делить как `opBinary!"*"(int)`, так и `opBinaryRight!"*"(int)`, а это расточи
+тельство, т. к. умножение коммутативно. При этом, предоставив языку
+принимать решение о коммутативности, можно столкнуться с излиш
+ними ограничениями: свойство коммутативности зависит от алгебры;
+например, умножение матриц некоммутативно. Поэтому компилятор
+оставляет за пользователем право определить отдельные операторы для
+правого и левого операндов, отказываясь брать на себя какую-либо от
+ветственность за коммутативность операторов.
+
+Чтобы организовать поддержку `a ‹оп› b` и `b ‹оп› a`, когда один операнд
+легко преобразуется к типу другого операнда, достаточно добавить од
+ну строку:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    ... // То же, что и раньше
+    // Реализовать правосторонние операторы
+    CheckedInt opBinaryRight(string op)(N lhs)
+    {
+        return CheckedInt(lhs).opBinary!op(this);
+    }
+}
+```
+
+Все, что нужно, – получить соответствующее выражение с `CheckedInt`
+слева. А затем вступают в права уже определенные операторы.
+
+Но иногда для преобразования требуется ряд дополнительных шагов,
+без которых можно было бы обойтись. Например, представьте выра
+жение `5 * c`, где `c` имеет тип `Complex!double`. Применив приведенное вы
+ше решение, мы бы протолкнули умножение в выражение `Complex!double(5) * c`, при вычислении которого пришлось бы преобразовать `5` в ком
+плексное число с нулевой мнимой частью, а затем зачем-то умножать
+комплексные числа, когда достаточно было бы всего лишь двух умноже
+ний действительных чисел. Результат, конечно, будет верным, но для
+его получения пришлось бы гораздо больше попотеть. В таких случаях
+лучше всего разделить правосторонние операции на две группы – ком
+мутативные и некоммутативные операции – и обрабатывать их по от
+дельности. Коммутативные операции можно обрабатывать просто с по
+мощью перестановки аргументов. Некоммутативные операции можно
+реализовывать так, чтобы каждый случай обрабатывался отдельно –
+или каждый раз заново, или извлекая пользу из уже реализованных
+примитивов.
+
+```d
+struct Complex(N) if (isFloatingPoint!N)
+{
+    N re, im;
+    // Реализовать коммутативные операторы
+    Complex opBinaryRight(string op)(N lhs)
+        if (op == "+" || op == "*")
+    {
+        // Предполагается, что левосторонний оператор уже реализован
+        return opBinary!op(lhs);
+    }
+    // Реализовать некоммутативные операторы вручную
+    Complex opBinaryRight(string op)(N lhs) if (op == "-")
+    {
+        return Complex(lhs - re, -im);
+    }
+
+    Complex opBinaryRight(string op)(N lhs) if (op == "/")
+    {
+        auto norm2 = re * re + im * im;
+        enforce(norm2 != 0);
+        auto t = lhs / norm2;
+        return Complex(re * t, -im * t);
+    }
+}
+```
+
+Для других типов можно выбрать другой способ группировки некото
+рых групп операций, в таком случае могут пригодиться уже описанные
+техники наложения ограничений на `op`.
+
+## 12.4. Перегрузка операторов сравнения
+
+В случае операторов сравнения (равенство и упорядочивание) D следует
+той же схеме, с которой мы познакомились, обсуждая классы (см. раз
+делы 6.8.3 и 6.8.4). Может показаться, что так сложилось исторически,
+но есть и веские причины обрабатывать сравнения не в общем методе
+`opBinary`, а иным способом. Во-первых, между операторами `==` и `!=` есть
+тесные взаимоотношения, как и у всей четверки `<`, `<=`, `>` и `>=`. Эти взаимо
+отношения подразумевают, что лучше использовать две отдельные
+функции со специфическими именами, чем код, определяющий каж
+дый оператор отдельно в зависимости от идентификаторов. Кроме того,
+многие типы, скорее всего, будут определять лишь равенство и упоря
+дочивание, а не все возможные операторы. С учетом этого факта для оп
+ределения сравнений язык предоставляет простое и компактное сред
+ство, не заставляя использовать мощный инструмент `opBinary`.
+
+Замена `a == b`, где хотя бы один из операндов `a` и `b` имеет пользователь
+ский тип, производится по следующему алгоритму:
+
+- Если как `a`, так и `b` – экземпляры классов, заменой служит выраже
+ние `object.opEquals(a, b)`. Как говорилось в разделе 6.8.3, сравнения
+между классами подчиняются небольшому протоколу, реализован
+ному в модуле `object` из ядра стандартной библиотеки.
+- Иначе если при разрешении имен `a.opEquals(b)` и `b.opEquals(a)` выясня
+ется, что это обращения к одной и той же функции, заменой служит
+выражение `a.opEquals(b)`. Такое может произойти, если `a` и `b` имеют
+один и тот же тип, с одинаковыми или разными квалификаторами.
+- Иначе компилируется только одно из выражений `a.opEquals(b)` и `b`.
+`opEquals(a)`, которое и становится заменой.
+
+Выражения с каким-либо из четырех операторов упорядочивающего
+сравнения `<`, `<=`, `>` и `>=` переписываются по следующему алгоритму:
+
+- Если при разрешении имен `a.opCmp(b)` и `b.opCmp(a)` выясняется, что
+это обращения к одной и той же функции, заменой служит выраже
+ние `a.opCmp(b) ‹оп› 0`.
+- Иначе компилируется только одно из выражений `a.opCmp(b)` и `b.opCmp(a)`. Если первое, то заменой служит выражение `a.opCmp(b) ‹оп› 0`. Иначе заменой служит выражение `0 ‹оп› b.opCmp(a)`.
+
+Здесь также стоит упомянуть о разумном обосновании одновременного
+существования как `opEquals`, так и `opCmp`. На первый взгляд может пока
+заться, что достаточно и одного метода `opCmp` (равенство было бы реали
+зовано как `a.opCmp(b) == 0`). Но хотя большинство типов могут определить
+равенство, многим типам нелегко реализовать отношение неравенства.
+Например, матрицы и комплексные числа определяют равенство, одна
+ко канонического определения отношения порядка им недостает.
+
+## 12.5. Перегрузка операторов присваивания
+
+К операторам присваивания относится не только простое присваивание
+вида `a = b`, но и присваивания с выполнением «на ходу» бинарных опе
+раторов, например `a += b` или `a *= b`. В разделе 7.1.5.1 уже было показа
+но, что выражение
+
+```d
+a = b
+``
+
+переписывается как
+
+```d
+a.opAssign(b)
+```
+
+При выполнении бинарных операторов «на месте» заменой
+
+```d
+a ‹оп›= b
+```
+
+послужит
+
+```d
+a.opOpAssign!"‹оп›"(b)
+```
+
+Замена позволяет типу операнда a реализовать операции «на месте» по
+описанным выше техникам. Рассмотрим пример реализации оператора
+`+=` для типа `CheckedInt`:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    private N value;
+    ref CheckedInt opOpAssign(string op)(CheckedInt rhs)
+        if (op == "+")
+    {
+        auto result = value + rhs.value;
+        enforce(rhs.value >= 0 ? result >= value : result <= value);
+        value = result;
+        return this;
+    }
+    ...
+}
+```
+
+В этом определении примечательны три детали. Во-первых, метод `opAssign` возвращает ссылку на текущий объект, благодаря чему поведение
+`CheckedInt` становится сравнимым с поведением встроенных типов. Во-
+вторых, истинное вычисление делается не «на месте», а напротив, «в сто
+ронке». Собственно, состояние объекта изменяется лишь после удачного
+выполнения проверки. В противном случае, если при вычислении выра
+жения с `enforce` будет порождено исключение, мы рискуем испортить те
+кущий объект. В-третьих, тело оператора фактически дублирует тело
+метода `opBinary!"+"`, рассмотренного выше. Воспользуемся последним на
+блюдением, чтобы задействовать имеющиеся реализации всех бинар
+ных операторов в определении операторов присваивания, одновременно
+выполняющих и бинарные операции. Вот новое определение:
+
+```d
+struct CheckedInt(N) if (isIntegral!N)
+{
+    ... // То же, что и раньше
+    // Определить все операторы присваивания
+    ref CheckedInt opOpAssign(string op)(CheckedInt rhs)
+    {
+        value = opBinary!op(rhs).value;
+        return this;
+    }
+}
+```
+
+Можно было бы поступить и по-другому: определять бинарные операто
+ры через операторы присваивания, определяемые с нуля. К этому выбо
+ру можно прийти из соображений эффективности; для многих типов
+изменение объекта «на месте» требует меньше памяти и выполняется
+быстрее, чем создание нового объекта.
+
+## 12.6. Перегрузка операторов индексации
+
+Язык D позволяет определять полностью абстрактный массив – массив,
+который поддерживает все операции, обычно ожидаемые от массива,
+но никогда не предоставляет адреса своих элементов клиентскому коду.
+Перегрузка операторов индексации – необходимое условие реализации
+этого средства. Чтобы обеспечить должный доступ по индексу, компи
+лятор различает чтение и запись элементов. В последнем случае эле
+мент массива находится слева от оператора присваивания, простой ли
+это оператор `=` или выполняющийся «на месте» бинарный оператор, та
+кой как `+=`.
+
+Если никакого присваивания не выполняется, компилятор заменяет
+выражение
+
+```d
+a[b1, b2, ..., bk]
+```
+
+на
+
+```d
+a.opIndex(b1, b2, ..., bk)
+```
+
+для любого числа аргументов *k*. Сколько принимается аргументов, ка
+кими должны быть их типы и каков тип результата, решает реализа
+ция метода `opIndex`.
+
+Если результат применения оператора индексации участвует в при
+сваивании слева, при снижении выражение
+
+```d
+a[b1, b2, ..., bk] = c
+```
+
+преобразуется в
+
+```d
+a.opIndexAssign(c, b1, b2, ..., bk)
+```
+
+Если к результату выражения с индексом применятся оператор увели
+чения или уменьшения на единицу, выражение
+
+```d
+‹оп› a[b1, b2, ..., bk]
+```
+
+где в качестве `‹оп›` выступает или `++`, `--`, или унарный `-`, `+`, `~`, `*`, переписы
+вается как
+
+```d
+a.opIndexUnary!"‹оп›"(b1, b2, ..., bk)
+```
+
+Постфиксные увеличение и уменьшение на единицу генерируются ав
+томатически из соответствующих префиксных вариантов, как описано
+в разделе 12.2.2.
+
+Наконец, если полученный по индексу элемент изменяется «на месте»,
+при снижении выражение
+
+```d
+a[b1, b2, ..., bk] ‹оп›= c
+```
+
+преобразуется в
+
+```d
+a.opIndexOpAssign!"‹оп›"(c, b1, b2, ..., bk)
+```
+
+Эти замены позволяют типу операнда `a` полностью определить, каким
+образом выполняется доступ к элементам, получаемым по индексу,
+и как они обрабатываются. Для чего индексируемому типу брать на себя
+ответственность за операторы присваивания? Казалось бы, более удач
+ное решение – просто предоставить методу `opIndex` возвращать ссылку
+на хранимый элемент, например:
+
+```
+struct MyArray(T)
+{
+    ref T opIndex(uint i) { ... }
+}
+```
+
+Тогда какие бы операции присваивания и изменения-с-присваиванием
+ни поддерживал тип `T`, они будут выполняться правильно. Предполо
+жим, дан массив типа `MyArray!int` с именем `a`, тогда при вычислении вы
+ражения `a[7] *= 2` сначала с помощью метода `opIndex` будет получено зна
+чение типа `ref int`, а затем эта ссылка будет использована для умноже
+ния «на месте» на 2. На самом деле, именно так и работают встроенные
+массивы.
+
+К сожалению, это решение не без изъяна. Одна из проблем заключается
+в том, что немалое число коллекций, построенных по принципу масси
+ва, не пожелают предоставить доступ к своим элементам по ссылке. Они,
+насколько это возможно, инкапсулируют расположение своих элемен
+тов, обернув их в абстракцию. Преимущества такого подхода – обычные
+плюсы сокрытия информации: у контейнера появляется свобода выбора
+наилучшей стратегии хранения его элементов. Простой пример – опре
+деление контейнера, содержащего объекты типа `bool`. Если бы контей
+нер был обязан предоставлять доступ к `ref bool`, ему пришлось бы хра
+нить каждое значение по отдельному адресу. Если же контейнер вправе
+скрывать адреса, то он может сохранить восемь логических значений
+в одном байте.
+
+Другой пример: для некоторых контейнеров доступ к данным неотде
+лим от их изменения. Представим разреженный массив. Разреженные
+массивы могут фиктивно содержать миллионы элементов, из которых
+лишь горстка ненулевые, что позволяет разреженным массивам приме
+нять стратегии хранения, экономичные в плане занимаемого места.
+А теперь рассмотрим следующий код:
+
+```d
+SparseArray!double a;
+...
+a[8] += 2;
+```
+
+Что должен предпринять массив, зависит как от его текущего содержи
+мого, так и от новых данных: если ячейка `a[8]` не была ранее заполнена,
+то создать ячейку со значением `2`; если ячейка была заполнена значени
+ем `-2`, удалить эту ячейку, поскольку ее новым значением будет ноль,
+а такие значения явно не сохраняются; если же ячейка содержала не
+что помимо `-2`, выполнить сложение и записать полученное значение
+обратно в ячейку. Реализовать эти действия или хотя бы большинство
+из них невозможно, если требуется, чтобы метод `opIndex` возвращал
+ссылку.
+
+## 12.7. Перегрузка операторов среза
+
+Массивы D предоставляют операторы среза `a[]` и `a[b1 .. b2]` (см. раз-
+дел 4.1.3). Оба эти оператора могут быть перегружены пользователь
+скими типами. Компилятор выполняет снижение, примерно как в слу
+чае оператора индексации.
+
+Если нет никакого присваивания, компилятор переписывает `a[]` в виде
+`a.opSlice()`, а `a[b1 .. b2]` – в виде `a.opSlice(b1, b2)`.
+
+Снижения для операций со срезами делаются по образцу снижений для
+соответствующих операций, определенных для массивов. Во всех име
+нах методов Index заменяется на `Slice`: `‹оп› a[]` снижается до `a.opSliceUnary!"‹оп›"()`, `‹оп› a[b1 .. b2]` превращается в `a.opSliceUnary!"‹оп›"(b1, b2)`, `a[] = c` – в `a.opSliceAssign(c)`, `a[b1 .. b2] = c` – в `a.opSliceAssign(c, b1, b2)`, `a[] ‹оп›= c` – в `a.opSliceOpAssign!"‹оп›"(c)`, и наконец, `a[b1 .. b2] ‹оп›= c` – в `a.opSliceOpAssign!"‹оп›"(c, b1, b2)`.
+
+## 12.8. Оператор $
+
+В случае встроенных массивов язык D позволяет внутри индексных вы
+ражений и среза обозначить длину массива идентификатором `$`. Напри
+мер, выражение `a[0 .. $ - 1`] выбирает все элементы встроенного масси
+ва a кроме последнего.
+
+Хотя этот оператор с виду довольно скромен, оказалось, что `$` сильно
+повышает и без того хорошее настроение программиста на D. С другой
+стороны, если бы оператор $ был «волшебным» и не допускал перегруз
+ку, это бы неизменно раздражало, еще раз подтверждая, что встроен
+ные типы должны лишь изредка обладать возможностями, недоступ
+ными пользовательским типам.
+
+Для пользовательских типов оператор `$` может быть перегружен так:
+
+• для выражения `a[‹выраж›]`, где `a` имеет пользовательский тип: если
+в `‹выраж›` встречается `$`, оно переписывается как `a.opDollar()`. Замена
+одна и та же независимо от присваивания этого выражения;
+• для выражения `a[‹выраж1›, ..., ‹выражk›]`: если в `‹выражi›` встречается `$`,
+оно переписывается как `a.opDollar!(i)()`;
+• для выражения `a[‹выраж1› .. ‹выраж2›]`: если в `‹выраж1›` или `‹выраж2›` встре
+чается `$`, оно переписывается как `a.opDollar()`.
+
+Если `a` – результат некоторого выражения, это выражение вычисляется
+только один раз.
+
+## 12.9. Перегрузка foreach
+
+Пользовательские типы могут существенным образом определять, как
+цикл просмотра будет с ними работать. Это огромное благо для типов,
+моделирующих коллекции, диапазоны, потоки и другие сущности, эле
+менты которых можно перебирать. Более того, дела обстоят еще лучше:
+есть целых два независимых способа организовать перегрузку, со свои
+ми плюсами и минусами.
+
+### 12.9.1. foreach с примитивами перебора
+
+Первый способ определить, как цикл `foreach` должен работать с вашим
+типом (структурой или классом), заключается в определении трех при
+митивов перебора: свойства `empty` типа `bool`, сообщающего, остались ли
+еще непросмотренные элементы, свойства `front`, возвращающего теку
+щий просматриваемый элемент, и метода `popFront()`[^3], осуществляющего
+переход к следующему элементу. Вот типичная реализация этих трех
+примитивов:
+
+```d
+struct SimpleList(T)
+{
+private:
+    struct Node
+    {
+        T _payload;
+        Node * _next;
+    }
+    Node * _root;
+public:
+    @property bool empty() { return !_root; }
+    @property ref T front() { return _root._payload; }
+    void popFront() { _root = _root._next; }
+    ...
+}
+```
+
+Имея такое определение, организовать перебор элементов списка про
+ще простого:
+
+```d
+void process(SimpleList!int lst)
+{
+    foreach (value; lst)
+    {
+        ... // Использовать значение типа int
+    }
+}
+```
+
+Компилятор заменяет управляющий код `foreach` соответствующим цик
+лом `for`, более неповоротливым, но мелкоструктурным аналогом, кото
+рый и использует три рассмотренные примитива:
+
+```d
+void process(SimpleList!int lst)
+{
+    for (auto __c = lst; !__c.empty; __c.popFront())
+    {
+        auto value = __c.front;
+        ... // Использовать значение типа int
+    }
+}
+```
+
+Если вы снабдите аргумент `value` ключевым словом `ref`, компилятор
+заменит все обращения к `value` в теле цикла обращениями к свойству
+`__c.front`. Таким образом, вы получаете возможность изменять элемен
+ты списка напрямую. Конечно, и само ваше свойство `front` должно воз
+вращать ссылку, иначе попытки использовать его как l-значение поро
+дят ошибки.
+
+Последнее, но не менее важное: если просматриваемый объект предос
+тавляет оператор среза без аргументов `lst[]`, `__c` инициализируется вы
+ражением `lst[]`, а не `lst`. Это делается для того, чтобы разрешить «из
+влечь» из контейнера средства перебора, не требуя определения трех
+примитивов перебора.
+
+### 12.9.2. foreach с внутренним перебором
+
+Примитивы из предыдущего раздела образуют интерфейс перебора, ко
+торый клиентский код может использовать, как заблагорассудится. Но
+иногда лучше использовать *внутренний перебор*, когда просматривае
+мая сущность полностью управляет процессом перебора и выполняет те
+ло цикла самостоятельно. Такое перекладывание ответственности часто
+может быть полезно, например, если полный просмотр коллекции пред
+почтительнее выполнять рекурсивно (как в случае с деревьями).
+
+Чтобы организовать цикл `foreach` с внутренним перебором, для вашей
+структуры или класса нужно определить метод `opApply`[^4]. Например:
+
+```d
+import std.stdio;
+
+class SimpleTree(T)
+{
+private:
+    T _payload;
+    SimpleTree _left, _right;
+public:
+    this(T payload)
+    {
+        _payload = payload;
+    }
+
+    // Обход дерева в глубину
+    int opApply(int delegate(ref T) dg)
+    {
+        auto result = dg(_payload);
+        if (result) return result;
+        if (_left)
+        {
+            result = _left.opApply(dg);
+            if (result) return result;
+        }
+        if (_right)
+        {
+            result = _right.opApply(dg);
+            if (result) return result;
+        }
+        return 0;
+    }
+}
+
+void main()
+{
+    auto obj = new SimpleTree!int(1);
+    obj._left = new SimpleTree!int(5);
+    obj._right = new SimpleTree!int(42);
+    obj._right._left = new SimpleTree!int(50);
+    obj._right._right = new SimpleTree!int(100);
+    foreach (i; obj)
+    {
+        writeln(i);
+    }
+}
+```
+
+Эта программа выполняет обход дерева в глубину и выводит:
+
+```
+1
+5
+42
+50
+100
+```
+
+Компилятор упаковывает тело цикла (в данном случае `{ writeln(i); }`)
+в делегат и передает его методу `opApply`. Компилятор организует испол
+нение программы так, что код, выполняющий выход из цикла с помо
+щью инструкции `break`, преждевременно возвращает `1` в качестве ре
+зультата делегата, отсюда и манипуляции с `result` внутри `opApply`.
+
+Зная все это, читать код метода `opApply` действительно легко: сначала
+тело цикла применяется к корневому узлу, а затем рекурсивно к левому
+и правому узлам. Простота реализации действительно имеет значение.
+Если вы попробуете реализовать просмотр узлов дерева с помощью при
+митивов `empty`, `front` и `popFront`, задача сильно усложнится. Так происхо
+дит потому, что в методе `opApply` состояние итерации формируется неяв
+но благодаря стеку вызовов. А при использовании трех примитивов пе
+ребора вам придется управлять этим состоянием явно.
+
+Упомянем еще одну достойную внимания деталь во взаимодействии
+`foreach` и `opApply`. Переменная `i`, используемая в цикле, становится ча
+стью типа делегата. К счастью, на тип этой переменной и даже на число
+привязываемых к делегату переменных, задействованных в `foreach`,
+ограничения не налагаются – все поддается настройке. Если вы опреде
+лите метод `opApply` так, что он будет принимать делегат с двумя аргумен
+тами, то сможете использовать цикл `foreach` следующего вида:
+
+```d
+// Вызывает метод object.opApply(delegate int(ref K k, ref V v){...})
+foreach (k, v; object)
+{
+    ...
+}
+```
+
+На самом деле, просмотр ключей и значений встроенных ассоциатив
+ных массивов реализован именно с помощью `opApply`. Для любого ассо
+циативного массива типа `V[K]` справедливо, что делегат, принимаемый
+методом `opApply`, ожидает в качестве параметров значения типов `V` и `K`.
+
+## 12.10. Определение перегруженных операторов в классах
+
+Большинство рассмотренных замен включали вызовы методов с пара
+метрами времени компиляции, таких как `opBinary(string)(T)`. Такие ме
+тоды очень хорошо работают как внутри классов, так и внутри струк
+тур. Единственная проблема в том, что методы с параметрами времени
+компиляции неявно неизменяемы, и их нельзя переопределить, так что
+для определения класса или интерфейса с переопределяемыми элемен
+тами может потребоваться ряд дополнительных шагов. Простейшее ре
+шение – написать, к примеру, метод `opBinary`, так чтобы он проталкивал
+выполнение операции далее в обычный метод, который можно пере
+определить:
+
+```d
+class A
+{
+    // Метод, не допускающий переопределение
+    A opBinary(string op)(A rhs)
+    {
+        // Протолкнуть в функцию, допускающую переопределение
+        return opBinary(op, rhs);
+    }
+    // Переопределяемый метод, управляется строкой во время исполнения
+    A opBinary(string op, A rhs)
+    {
+        switch (op)
+        {
+            case "+":
+                ... // Реализовать сложение
+                break;
+            case "-":
+                ... // Реализовать вычитание
+                break;
+            ...
+        }
+    }
+}
+```
+
+Такой подход позволяет решить поставленную задачу, но не оптималь
+но, ведь оператор проверяется во время исполнения – действие, которое
+может быть выполнено во время компиляции. Следующее решение по
+зволяет исключить излишние затраты по времени за счет переноса про
+верки внутрь обобщенной версии метода `opBinary`:
+
+```d
+class A
+{
+    // Метод, не допускающий переопределение
+    A opBinary(string op)(A rhs)
+    {
+        // Протолкнуть в функцию, допускающую переопределение
+        static if (op == "+")
+        {
+            return opAdd(rhs);
+        }
+        else static if (op == "-")
+        {
+            return opSubtract(rhs);
+        } ...
+    }
+    // Переопределяемые методы
+    A opAdd(A rhs)
+    {
+        ... // Реализовать сложение
+    }
+
+    A opSubtract(A rhs)
+    {
+        ... // Реализовать вычитание
+    }
+    ...
+}
+```
+
+На этот раз каждому оператору соответствует свой метод. Вы, разумеет
+ся, вправе выбрать операторы для перегрузки и способы их группирова
+ния, соответствующие вашему случаю.
+
+## 12.11. Кое-что из другой оперы: opDispatch
+
+Пожалуй, самая интересная из замен, открывающая максимум воз
+можностей, – это замена с участием метода `opDispatch`. Именно она по
+зволяет D встать в один ряд с гораздо более динамическими языками.
+
+Если некоторый тип `T` определяет метод `opDispatch`, компилятор пере
+писывает выражение
+
+```d
+a.fun(‹арг1›, ..., ‹аргk›)
+```
+
+как
+
+```d
+a.opDispatch!"fun"(‹арг1›, ..., ‹аргk›)
+```
+
+для всех методов `fun`, которые должны были бы присутствовать, но не
+определены, то есть для всех вызовов, которые бы иначе вызвали ошиб
+ку «метод не определен».
+
+Определение `opDispatch` может реализовывать много очень интерес
+ных задумок разной степени динамичности. Рассмотрим пример мето
+да `opDispatch`, реализующего подчинение альтернативному соглашению
+именования методов класса. Для начала объявим простую функцию,
+преобразующую идентификатор `такого_вида` в его альтернативу «в сти
+ле верблюда» (camel-case) `такогоВида`:
+
+```d
+import std.ctype;
+
+string underscoresToCamelCase(string sym)
+{
+    string result;
+    bool makeUpper;
+    foreach (c; sym)
+    {
+        if (c == '_')
+        {
+            makeUpper = true;
+        }
+        else
+        {
+            if (makeUpper)
+            {
+                result ~= toupper(c);
+                makeUpper = false;
+            }
+            else
+            {
+                result ~= c;
+            }
+        }
+    }
+    return result;
+}
+
+unittest
+{
+    assert(underscoresToCamelCase("здравствуй_мир") == "здравствуйМир");
+    assert(underscoresToCamelCase("_a") == "A");
+    assert(underscoresToCamelCase("abc") == "abc");
+    assert(underscoresToCamelCase("a_bc_d_") == "aBcD");
+}
+```
+
+Вооружившись функцией `underscoresToCamelCase`, можно легко опреде
+лить для некоторого класса метод `opDispatch`, заставляющий этот класс
+принимать вызовы `a.метод_такого_вида()` и автоматически перенаправ
+лять эти обращения к методам `a.методТакогоВида()` – и все это во время
+компиляции.
+
+```d
+class A
+{
+    auto opDispatch(string m, Args...)(Args args)
+    {
+        return mixin("this."~underscoresToCamelCase(m)~"(args)");
+    }
+
+    int doSomethingCool(int x, int y)
+    {
+        ...
+        return 0;
+    }
+}
+
+unittest
+{
+    auto a = new A;
+    a.doSomethingCool(5, 6); // Вызов напрямую
+    a.do_something_cool(5, 6); // Тот же вызов, но через посредника opDispatch
+}
+```
+
+Второй вызов не относится ни к одному из методов класса `A`, так что он
+перенаправляется в метод `opDispatch` через вызов `a.opDispatch!"do_something_cool"(5, 6)`. `opDispatch`, в свою очередь, генерирует строку `"this.doSomethingCool(args)"`, а затем компилирует ее с помощью выражения `mixin`.
+Учитывая, что с переменной `args` связана пара аргументов `5`, `6`, вызов
+`mixin` в итоге сменяется вызовом `a.doSomethingCool(5, 6)` – старое доброе
+перенаправление в своем лучшем проявлении. Миссия выполнена!
+
+### 12.11.1. Динамическое диспетчирование с opDispatch
+
+Хотя, конечно, интересно использовать `opDispatch` в разнообразных про
+делках времени компиляции, реально интересные приложения требуют
+динамичности. Динамические языки, такие как JavaScript или Small
+talk, позволяют присоединять к объектам методы во время исполне
+ния. Попробуем сделать нечто подобное на D: определим класс `Dynamic`,
+позволяющий динамически добавлять, удалять и вызывать методы.
+
+Во-первых, для таких динамических методов придется определить сиг
+натуру времени исполнения. Здесь нам поможет тип `Variant` из модуля
+`std.variant`. Это мастер на все руки: объект типа `Variant` может содер
+жать практически любое значение. Такое свойство делает `Variant` иде
+альным кандидатом на роль типа параметра и возвращаемого значения
+динамического метода. Итак, определим сигнатуру такого динамиче
+ского метода в виде делегата, который в качестве первого аргумента (иг
+рающего роль `this`) принимает `Dynamic`, а вместо остальных аргументов –
+массив элементов типа `Variant`, и возвращает результат типа `Variant`.
+
+```d
+import std.variant;
+
+alias Variant delegate(Dynamic self, Variant[] args...) DynMethod;
+```
+
+Благодаря ... можно вызывать `DynMethod` с любым количеством аргумен
+тов с уверенностью, что компилятор упакует их в массив. А теперь
+определим класс `Dynamic`, который, как и обещано, позволит манипули
+ровать методами во время исполнения. Чтобы обеспечить такие воз
+можности, `Dynamic` определяет ассоциативный массив, отображающий
+строки на элементы типа `DynMethod`:
+
+```d
+class Dynamic
+{
+    private DynMethod[string] methods;
+
+    void addMethod(string name, DynMethod m)
+    {
+        methods[name] = m;
+    }
+
+    void removeMethod(string name)
+    {
+        methods.remove(name);
+    }
+
+    // Динамическое диспетчирование вызова метода
+    Variant call(string methodName, Variant[] args...)
+    {
+        return methods[methodName](this, args);
+    }
+
+    // Предоставить синтаксический сахар с помощью opDispatch
+    Variant opDispatch(string m, Args...)(Args args)
+    {
+        Variant[] packedArgs = new Variant[args.length];
+        foreach (i, arg; args)
+        {
+            packedArgs[i] = Variant(arg);
+        }
+        return call(m, args);
+    }
+}
+```
+
+Посмотрим на `Dynamic` в действии:
+
+```d
+unittest
+{
+    auto obj = new Dynamic;
+    obj.addMethod("sayHello",
+        delegate Variant(Dynamic, Variant[]...)
+        {
+            writeln("Здравствуй, мир!");
+            return Variant();
+        }
+    );
+    obj.sayHello(); // Печатает "Здравствуй, мир!"
+}
+```
+
+Поскольку все методы должны соответствовать одной и той же сигнату
+ре, добавление метода не обходится без некоторого синтаксического
+шума. В этом примере довольно много незадействованных элементов:
+добавляемый делегат не использует ни один из своих параметров и воз
+вращает результат, не представляющий никакого интереса. Зато син
+таксис вызова очень прозрачен. Это важно, так как обычно методы до
+бавляются редко, а вызываются часто. Усовершенствовать класс `Dynamic`
+можно разными путями. Например, можно определить информацион
+ную функцию `getMethodInfo(string)`, возвращающую для заданного ме
+тода число его параметров и их типы.
+
+Заметим, что в данном случае приходится идти на уступки, обычные
+для решения о статическом или динамическом выполнении действий.
+Чем больше вы делаете во время исполнения, тем чаще требуется соот
+ветствовать общим форматам данных (`Variant` в нашем примере) и идти
+на компромисс, жертвуя быстродействием (например, из-за поиска имен
+методов во время исполнения). Взамен вы получаете возросшую гиб
+кость: можно как угодно манипулировать определениями классов во
+время исполнения, определять отношения динамического наследова
+ния, взаимодействовать со скриптовыми языками, определять скрип
+ты для собственных объектов и еще много чего.
+
+## 12.12. Итоги и справочник
+
+Пользовательские типы могут перегружать большинство операторов.
+Есть несколько исключений, таких как «запятая» `,`, логическая конъ
+юнкция `&&`, логическая дизъюнкция `||`, проверка на идентичность `is`,
+тернарный оператор `?:`, а также унарные операторы получения адреса `&`
+и `typeid`. Было решено, что перегрузка этих операторов добавит скорее
+путаницы, чем гибкости.
+
+Кстати, о путанице. Заметим, что перегрузка операторов – это мощный
+инструмент, к которому прилагается инструкция с предупреждением
+той же мощности. В языке D лучший совет для вас: не используйте опе
+раторы в экзотических целях, вроде определения целых встроенных
+предметно-ориентированных языков (Domain-Specific Embedded Lan
+guage, DSEL). Если желаете определять встроенные предметно-ориен
+тированные языки, то для этой цели лучше всего подойдут строки
+и выражение `mixin` (см. раздел 2.3.4.2) с вычислением функций на этапе
+компиляции (см. раздел 5.12). Эти средства позволяют выполнить син
+таксический разбор входных конструкций на DSEL, представленных
+в виде строки времени компиляции, а затем сгенерировать соответству
+ющий код на D. Такой подход требует больше труда, но пользователи
+вашей библиотеки это оценят.
+
+Определение `opDispatch` открывает новые горизонты, но это средство
+также нужно использовать с умом. Чрезмерная динамичность может
+снизить быстродействие программы за счет лишних манипуляций и ос
+лабить проверку типов (например, не стоит забывать, что если в преды
+дущем фрагменте кода вместо `a.helloWorld()` написать `a.heloWorld()`, код
+все равно скомпилируется, а ошибка проявится лишь во время испол
+нения).
+
+В табл. 12.1 в сжатой форме представлена информация из этой главы.
+Используйте эту таблицу как шпаргалку, когда будете перегружать
+операторы для собственных типов.
+
+*Таблица 12.1. Перегруженные операторы*
+
+|Выражение|Переписывается как...|
+|-|-|
+|`‹оп›a`, где `‹оп›` ∈ {`+`, `-`, `~`, `*`, `++`, `--`}|`a.opUnary!"‹оп›"()`|
+|`a++`|`((ref x) {auto t=x; ++x; return t;})(a)`|
+|`a--`|`((ref x) {auto t=x; --x; return t;})(a)`|
+|`cast(T) a`|`a.opCast!(T)()`|
+|`a ? ‹выраж1› : ‹выраж2›`|`cast(bool) a ? ‹выраж1› : ‹выраж2›`|
+|`if (a) ‹инстр›`|`if (cast(bool) a) ‹инстр›`|
+|`a ‹оп› b`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, `|`, `^`, `<<`, `>>`, `>>>`, `~`, `in`}|`a.opBinary!"‹оп›"(b)` или `b.opBinaryRight!"‹оп›"(a)`|
+|`a == b`|Если `a` и `b` – экземпляры классов: `object.opEquals(a, b)` (см. раздел 6.8.3). Иначе если `a` и `b` имеют один тип: `a.opEquals(b)`. Иначе единственное выражение из `a.opEquals(b)` и `b.opEquals(a)`, которое компилируется|
+|`a != b`|`!(a == b)`, затем действовать по предыдущему алгоритму|
+|`a < b`|`a.opCmp(b) < 0` или `b.opCmp(a) > 0`|
+|`a <= b`|`a.opCmp(b) <= 0` или `b.opCmp(a) >= 0`|
+|`a > b`|`a.opCmp(b) > 0` или `b.opCmp(a) < 0`|
+|`a >= b`|`a.opCmp(b) >= 0` или `b.opCmp(a) <= 0`|
+|`a = b`|`a.opAssign(b)`|
+|`a ‹оп›= b`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, `|||`, `^`, `<<`, `>>`, `>>>`, `~`}|`a.opOpAssign!"‹оп›"(b)`|
+|`a[b1, b2, ..., bk]`|`a.opIndex(b1, b2, ..., bk)`|
+|`a[b1, b2, ..., bk] = c`|`a.opIndexAssign(c, b1, b2, ..., bk)`|
+|`‹оп›a[b1, b2, ..., bk]`, где `‹оп›` ∈ {`++`, `--`}|`a.opIndexUnary(b1, b2, ..., bk)`|
+|`a[b1, b2, ..., bk] ‹оп›= c`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, `|||`, `^`, `<<`, `>>`, `>>>`, `~`}|`a.opIndexOpAssign!"‹оп›"(c, b1, b2, ..., bk)`|
+|`a[b1 .. b2]`|`a.opSlice(b1 .. b2)`|
+|`‹оп› a[b1 .. b2]`|`a.opSliceUnary!"‹оп›"(b1, b2)`|
+|`a[] = c`|`a.opSliceAssign(c)`|
+|`a[b1 .. b2] = c`|`a.opSliceAssign(c, b1, b2)`|
+|`a[] ‹оп›= c`|`a.opSliceOpAssign!"‹оп›"(c)`|
+|`a[b1 .. b2] ‹оп›= c`|`a.opSliceOpAssign!"‹оп›"(c, b1, b2)`|
+
+[^1]: Автор использует понятия «тип» и «алгебра» не совсем точно. Тип определяет множество значений и множество операций, производимых над ними. Алгебра – это набор операций над определенным множеством. То есть уточнение «с алгебрами» – избыточно. – *Прим. науч. ред.*
+[^2]: В данном коде отсутствует проверка перехода за границы для оператора отрицания. – *Прим. науч. ред.*
+[^3]: Для перегрузки `foreach_reverse` служат примитивы `popBack` и `back` аналогичного назначения. – *Прим. науч. ред.*
+[^4]: Существует также оператор `opApplyReverse`, предназначенный для перегрузки `foreach_reverse` и действующий аналогично `opApply` для `foreach`. – *Прим. науч. ред.*