From 5063e81e4ad2edb0df3e2f4afe104f21427f5450 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 4 Mar 2023 18:17:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 12-перегрузка-операторов/README.md | 1438 ++++++++++++++++++++++++++++ 1 file changed, 1438 insertions(+) diff --git a/12-перегрузка-операторов/README.md b/12-перегрузка-операторов/README.md index e69de29..4ad9558 100644 --- a/12-перегрузка-операторов/README.md +++ b/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`. – *Прим. науч. ред.*