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 реализовать операции «на месте» по
описанным выше техникам. Рассмотрим пример реализации оператора
Можно было бы поступить и по-другому: определять бинарные операто
ры через операторы присваивания, определяемые с нуля. К этому выбо
ру можно прийти из соображений эффективности; для многих типов
изменение объекта «на месте» требует меньше памяти и выполняется
быстрее, чем создание нового объекта.
## 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`, реализующего подчинение альтернативному соглашению
именования методов класса. Для начала объявим простую функцию,
преобразующую идентификатор `такого_вида` в его альтернативу «в сти
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` определяет ассоциативный массив, отображающий
|`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`|
[^1]: Автор использует понятия «тип» и «алгебра» не совсем точно. Тип определяет множество значений и множество операций, производимых над ними. Алгебра – это набор операций над определенным множеством. То есть уточнение «с алгебрами» – избыточно. –*Прим. науч. ред.*
[^2]: В данном коде отсутствует проверка перехода за границы для оператора отрицания. –*Прим. науч. ред.*
[^3]: Для перегрузки `foreach_reverse` служат примитивы `popBack` и `back` аналогичного назначения. –*Прим. науч. ред.*
[^4]: Существует также оператор `opApplyReverse`, предназначенный для перегрузки `foreach_reverse` и действующий аналогично `opApply` для `foreach`. –*Прим. науч. ред.*