1439 lines
83 KiB
Markdown
1439 lines
83 KiB
Markdown
# 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`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, <code>|</code>, `^`, `<<`, `>>`, `>>>`, `~`, `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`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, <code>|</code>, `^`, `<<`, `>>`, `>>>`, `~`}|`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`, где `‹оп›` ∈ {`+`, `-`, `*`, `/`, `%`, `&`, <code>|</code>, `^`, `<<`, `>>`, `>>>`, `~`}|`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`. – *Прим. науч. ред.*
|