|
@@ -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`. – *Прим. науч. ред.*
|