diff --git a/12-перегрузка-операторов/README.md b/12-перегрузка-операторов/README.md index 888fc07..984b71e 100644 --- a/12-перегрузка-операторов/README.md +++ b/12-перегрузка-операторов/README.md @@ -1,121 +1,66 @@ # 12. Перегрузка операторов -Мы, программисты, не очень любим слишком уж отделять встроенные -типы от пользовательских. Магические свойства встроенных типов ме -шают открытости и расширяемости любого языка, поскольку при этом -пользовательские типы обречены оставаться второсортными. Тем не -менее у проектировщиков языка есть все законные основания отно -ситься к встроенным типам с большим почтением. Одно из таких осно -ваний: более настраиваемый язык сложнее выучить, а также сложнее -выполнять его синтаксический анализ как человеку, так и машине. Ка -ждый язык по-своему определяет приемлемое соотношение между -встроенным и настраиваемым, что для некоторых языков означает впа -дение в одну из этих двух крайностей. +[12.1. Перегрузка операторов в D](#12-1-перегрузка-операторов-в-d) +[12.2. Перегрузка унарных операторов](#12-2-перегрузка-унарных-операторов) + [12.2.1. Объединение определений операторов с помощью выражения mixin](#12-2-1-объединение-определений-операторов-с-помощью-выражения-mixin) + [12.2.2. Постфиксный вариант операторов увеличения и уменьшения на единицу](#12-2-2-постфиксный-вариант-операторов-увеличения-и-уменьшения-на-единицу) + [12.2.3. Перегрузка оператора cast](#12-2-3-перегрузка-оператора-cast) + [12.2.4. Перегрузка тернарной условной операции и ветвления](#12-2-4-перегрузка-тернарной-условной-операции-и-ветвления) +[12.3. Перегрузка бинарных операторов](#12-3-перегрузка-бинарных-операторов) + [12.3.1. Перегрузка операторов в квадрате](#12-3-1-перегрузка-операторов-в-квадрате) + [12.3.2. Коммутативность](#12-3-2-коммутативность) +[12.4. Перегрузка операторов сравнения](#12-4-перегрузка-операторов-сравнения) +[12.5. Перегрузка операторов присваивания](#12-5-перегрузка-операторов-присваивания) +[12.6. Перегрузка операторов индексации](#12-6-перегрузка-операторов-индексации) +[12.7. Перегрузка операторов среза](#12-7-перегрузка-операторов-среза) +[12.8. Оператор $](#12-8-оператор) +[12.9. Перегрузка foreach](#12-9-перегрузка-foreach) + [12.9.1. foreach с примитивами перебора](#12-9-1-foreach-с-примитивами-перебора) + [12.9.2. foreach с внутренним перебором](#12-9-2-foreach-с-внутренним-перебором) +[12.10. Определение перегруженных операторов в классах](#12-10-определение-перегруженных-операторов-в-классах) +[12.11. Кое-что из другой оперы: opDispatch](#12-11-кое-что-из-другой-оперы-opdispatch) + [12.11.1. Динамическое диспетчирование с opDispatch](#12-11-1-динамическое-диспетчирование-с-opdispatch) +[12.12. Итоги и справочник](#12-12-итоги-и-справочник) -Язык D подходит к этому вопросу прагматично: он не умаляет важ -ность настраиваемости, но при этом осознает практичность встроенных -типов – D использует преимущества встроенных типов ровно тремя пу -тями: +Мы, программисты, не очень любим слишком уж отделять встроенные типы от пользовательских. Магические свойства встроенных типов мешают открытости и расширяемости любого языка, поскольку при этом пользовательские типы обречены оставаться второсортными. Тем не менее у проектировщиков языка есть все законные основания относиться к встроенным типам с большим почтением. Одно из таких оснований: более настраиваемый язык сложнее выучить, а также сложнее выполнять его синтаксический анализ как человеку, так и машине. Каждый язык по-своему определяет приемлемое соотношение между встроенным и настраиваемым, что для некоторых языков означает впадение в одну из этих двух крайностей. -1. *Синтаксис названий типов*. Массивы и ассоциативные массивы ис -пользуются повсеместно, и, согласитесь, синтаксис `int[]` и `int[string]` -гораздо нагляднее, чем `Array!int` и `AssotiativeArray!(string, int)`. В поль -зовательском коде нельзя определять новые формы записи названий -типов, например `int[[]]`. +Язык D подходит к этому вопросу прагматично: он не умаляет важность настраиваемости, но при этом осознает практичность встроенных типов – D использует преимущества встроенных типов ровно тремя путями: -*Литералы*. Числовые и строковые литералы, как и литералы масси -вов и ассоциативных массивов, – «особые», и их набор нельзя расши -рить. «Сборные» объекты-структуры, такие как `Point(5, 3)`, – тоже -литералы, но тип не может определить новый синтаксис литерала, -например `(3, 5)pt`. +1. *Синтаксис названий типов*. Массивы и ассоциативные массивы используются повсеместно, и, согласитесь, синтаксис `int[]` и `int[string]` гораздо нагляднее, чем `Array!int` и `AssotiativeArray!(string, int)`. В пользовательском коде нельзя определять новые формы записи названий типов, например `int[[]]`. -*Семантика*. Зная семантику определенных типов и их операций, -компилятор оптимизирует код. Например, встретив выражение -`"Hello" ~ ", " ~ "world"`, компилятор не откладывает конкатенацию до -времени исполнения: он знает, что делает операция конкатенации -строк, и склеивает строки уже во время компиляции. Аналогично -компилятор упрощает и оптимизирует арифметические выражения, -используя знание арифметики. +2. *Литералы*. Числовые и строковые литералы, как и литералы массивов и ассоциативных массивов, – «особые», и их набор нельзя расширить. «Сборные» объекты-структуры, такие как `Point(5, 3)`, – тоже литералы, но тип не может определить новый синтаксис литерала, например `(3, 5)pt`. -Некоторые языки добавляют к этому списку операторы. Они делают -операторы особенными; чтобы выполнить какую-либо операцию приме -нительно к пользовательским типам, приходится использовать стан -дартные средства языка, такие как вызов функций или макросов. Не -смотря на то что это совершено законное решение, оно на самом деле -создает проблемы при большом объеме кода, ориентированного на ариф -метические вычисления. Многие программы, ориентированные на вы -числения, определяют собственные типы с алгебрами[^1] (числа неограни -ченной точности, специализированные числа с плавающей запятой, -кватернионы, октавы, матрицы всевозможных форм, тензоры, ... оче -видно, что язык не может сделать встроенными их все). При использова -нии таких типов выразительность кода резко снижается. По сравнению -с эквивалентным функциональным синтаксисом, операторы обычно -требуют меньше места и круглых скобок, а получаемый с их участием -код зачастую легок для восприятия. Рассмотрим для примера вычисле -ние среднего гармонического трех ненулевых чисел `x`, `y` и `z`. Выражение -на основе операторов очень близко к математическому определению: +3. *Семантика*. Зная семантику определенных типов и их операций, компилятор оптимизирует код. Например, встретив выражение `"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 очень привлекателен для численного программирования. Он -предоставляет надежную арифметику с плавающей запятой и превос -ходную библиотеку трансцендентных функций, которые иногда возвра -щают результат с большей точностью, чем «родные» системные библио -теки, и предлагает широкие возможности для моделирования. Мощное -средство перегрузки операторов добавляет ему привлекательности. Пе -регрузка операторов позволяет вам определять собственные числовые -типы (такие как числа с фиксированной запятой, десятичные числа для -финансовых и бухгалтерских программ, неограниченные целые числа -или действительные числа неограниченной точности), максимально -близкие к встроенным числовым типам. Перегрузка операторов также -позволяет определять типы с «числоподобными» алгебрами, такие как -векторы и матрицы. Давайте посмотрим, как можно определять типы -с помощью этого средства. +Язык D очень привлекателен для численного программирования. Он предоставляет надежную арифметику с плавающей запятой и превосходную библиотеку трансцендентных функций, которые иногда возвращают результат с большей точностью, чем «родные» системные библиотеки, и предлагает широкие возможности для моделирования. Мощное средство перегрузки операторов добавляет ему привлекательности. Перегрузка операторов позволяет вам определять собственные числовые типы (такие как числа с фиксированной запятой, десятичные числа для финансовых и бухгалтерских программ, неограниченные целые числа или действительные числа неограниченной точности), максимально близкие к встроенным числовым типам. Перегрузка операторов также позволяет определять типы с «числоподобными» алгебрами, такие как векторы и матрицы. Давайте посмотрим, как можно определять типы с помощью этого средства. + +[В начало ⮍](#12-перегрузка-операторов) ## 12.1. Перегрузка операторов в D -Подход D к перегрузке операторов прост: если хотя бы один участник -выражения с оператором имеет пользовательский тип, компилятор *заменяет* это выражение на обычный вызов метода с регламентирован -ным именем. Затем применяются обычные правила языка. Таким обра -зом, перегруженные операторы – лишь синтаксический сахар для вызо -ва методов, а значит, нет нужды вникать в причуды самостоятельного -средства языка. Например, если `a` относится к некоторому определенно -му пользователем типу, выражение `a + 5` заменяется на `a.opBinary!"+"(5)`. -К методу `opBinary` применяются обычные правила и проверки, и тип `a` -должен определять этот метод, если желает обеспечить поддержку пе -регрузки операторов. +Подход D к перегрузке операторов прост: если хотя бы один участник выражения с оператором имеет пользовательский тип, компилятор *заменяет* это выражение на обычный вызов метода с регламентированным именем. Затем применяются обычные правила языка. Таким образом, перегруженные операторы – лишь синтаксический сахар для вызова методов, а значит, нет нужды вникать в причуды самостоятельного средства языка. Например, если `a` относится к некоторому определенному пользователем типу, выражение `a + 5` заменяется на `a.opBinary!"+"(5)`. К методу `opBinary` применяются обычные правила и проверки, и тип `a` должен определять этот метод, если желает обеспечить поддержку перегрузки операторов. -Замена (точнее, *снижение*, т. к. этот процесс преобразует конструкции -более высокого уровня в низкоуровневый код) – очень эффективный ин -струмент, позволяющий реализовать новые средства на основе имею -щихся, и D обычно его применяет. Мы уже видели снижение в действии -применительно к конструкции `scope` (см. раздел 3.13). По сути, `scope` – -лишь синтаксический сахар, которым засыпаны особым образом сцеп -ленные конструкции `try`, но вам точно не придет в голову самостоятель -но писать сниженный код, так как `scope` значительно поднимает уро -вень высказываний. Перегрузка операторов действует в том же духе, -определяя все вызовы операторов через замену на вызовы функций, тем -самым придавая мощь обычным определениям функций и используя -их как средство достижения своей цели. Без лишних слов посмотрим, -как компилятор осуществляет снижение операторов разных категорий. +Замена (точнее, *снижение*, т. к. этот процесс преобразует конструкции более высокого уровня в низкоуровневый код) – очень эффективный инструмент, позволяющий реализовать новые средства на основе имеющихся, и D обычно его применяет. Мы уже видели снижение в действии применительно к конструкции `scope` (см. раздел 3.13). По сути, `scope` – лишь синтаксический сахар, которым засыпаны особым образом сцепленные конструкции `try`, но вам точно не придет в голову самостоятельно писать сниженный код, так как `scope` значительно поднимает уровень высказываний. Перегрузка операторов действует в том же духе, определяя все вызовы операторов через замену на вызовы функций, тем самым придавая мощь обычным определениям функций и используя их как средство достижения своей цели. Без лишних слов посмотрим, как компилятор осуществляет снижение операторов разных категорий. + +[В начало ⮍](#12-1-перегрузка-операторов-в-d) [Наверх ⮍](#12-перегрузка-операторов) ## 12.2. Перегрузка унарных операторов -В случае унарных операторов `+` (плюс), `-` (отрицание), `~` (поразрядное от -рицание), `*` (разыменование указателя), `++` (увеличение на единицу) и `--` -(уменьшение на единицу) компилятор заменяет выражение +В случае унарных операторов `+` (плюс), `-` (отрицание), `~` (поразрядное отрицание), `*` (разыменование указателя), `++` (увеличение на единицу) и `--` (уменьшение на единицу) компилятор заменяет выражение ```d ‹оп› a @@ -127,13 +72,9 @@ m = divide(3, add(add(divide(1, x), divide(1, y)), divide(1, z))); a.opUnary!"‹оп›"() ``` -для всех значений пользовательских типов. В качестве замены высту -пает вызов метода opUnary с одним аргументом времени компиляции -`"‹оп›"` и без каких-либо аргументов времени исполнения. Например `++a` -перезаписывается как `a.opUnary! "++" ()`. +для всех значений пользовательских типов. В качестве замены выступает вызов метода opUnary с одним аргументом времени компиляции `"‹оп›"` и без каких-либо аргументов времени исполнения. Например `++a` перезаписывается как `a.opUnary! "++" ()`. -Чтобы перегрузить один или несколько унарных операторов для типа T, -определите метод T.opUnary так: +Чтобы перегрузить один или несколько унарных операторов для типа `T`, определите метод `T.opUnary` так: ```d struct T @@ -142,15 +83,7 @@ struct T } ``` -В таком виде, как он здесь определен, этот метод будет вызываться для -всех унарных операторов. А если вы хотите для некоторых операторов -определить отдельные методы, вам помогут ограничения сигнатуры -(см. раздел 5.4). Рассмотрим определение типа `CheckedInt`, который слу -жит оберткой базовых числовых типов и гарантирует, что значения, по -лучаемые в результате применения операций к оборачиваемым типам, -не выйдут за границы, установленные для этих типов. Тип `CheckedInt` -должен быть параметризирован оборачиваемым типом (например, `CheckedInt!int`, `CheckedInt!long` и т. д.). Вот неполное определение `CheckedInt` -с операторами префиксного увеличения и уменьшения на единицу: +В таком виде, как он здесь определен, этот метод будет вызываться для всех унарных операторов. А если вы хотите для некоторых операторов определить отдельные методы, вам помогут ограничения сигнатуры (см. раздел 5.4). Рассмотрим определение типа `CheckedInt`, который служит оберткой базовых числовых типов и гарантирует, что значения, получаемые в результате применения операций к оборачиваемым типам, не выйдут за границы, установленные для этих типов. Тип `CheckedInt` должен быть параметризирован оборачиваемым типом (например, `CheckedInt!int`, `CheckedInt!long` и т. д.). Вот неполное определение `CheckedInt` с операторами префиксного увеличения и уменьшения на единицу: ```d struct CheckedInt(N) if (isIntegral!N) @@ -174,19 +107,11 @@ struct CheckedInt(N) if (isIntegral!N) } ``` +[В начало ⮍](#12-2-перегрузка-унарных-операторов) [Наверх ⮍](#12-перегрузка-операторов) + ### 12.2.1. Объединение определений операторов с помощью выражения mixin -Есть очень мощная техника, позволяющая определить не один, а сразу -группу операторов. Например, все унарные операторы `+`, `-` и `~` для типа -`CheckedInt` делают одно и то же – всего лишь проталкивают соответст -вующую операцию по направлению к `value`, внутреннему элементу `CheckedInt`. Хоть эти операторы и неидентичны, они несомненно придержи -ваются одного и того же шаблона поведения. Можно просто определить -специальный метод для каждого оператора, но это вылилось бы в неин -тересное дублирование шаблонного кода. Лучший подход – использо -вать работающие со строками выражения `mixin` (см. раздел 2.3.4.2), по -зволяющие напрямую монтировать операции из имен операндов и иден -тификаторов операторов. Следующий код реализует все унарные опера -ции, применимые к `CheckedInt`.[^2] +Есть очень мощная техника, позволяющая определить не один, а сразу группу операторов. Например, все унарные операторы `+`, `-` и `~` для типа `CheckedInt` делают одно и то же – всего лишь проталкивают соответствующую операцию по направлению к `value`, внутреннему элементу `CheckedInt`. Хоть эти операторы и неидентичны, они несомненно придерживаются одного и того же шаблона поведения. Можно просто определить специальный метод для каждого оператора, но это вылилось бы в неинтересное дублирование шаблонного кода. Лучший подход – использовать работающие со строками выражения `mixin` (см. раздел 2.3.4.2), позволяющие напрямую монтировать операции из имен операндов и идентификаторов операторов. Следующий код реализует все унарные операции, применимые к `CheckedInt`.[^2] ```d struct CheckedInt(N) if (isIntegral!N) @@ -215,59 +140,26 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -Это уже заметная экономия на длине кода, и она лишь возрастет, как -только мы доберемся до бинарных операторов и выражений индекса -ции. Главное действующее лицо такого подхода – выражение `mixin`, ко -торое позволяет вам взять строку и попросить компилятор скомпили -ровать ее. Строка получается буквальным соединением операнда и опе -ратора вручную. Способность трансформироваться в код, по счастливой -случайности присущая строке op, фактически воплощает в жизнь эту -идиому; на самом деле, весь этот механизм перегрузки проектировался -с прицелом на `mixin`. Раньше D использовал отдельное имя для каждого -оператора (`opAdd`, `opSub`, `opMul`, ...), что требовало механического запоми -нания соответствия имен операторам и написания группы функций -с практически идентичными телами. +Это уже заметная экономия на длине кода, и она лишь возрастет, как только мы доберемся до бинарных операторов и выражений индексации. Главное действующее лицо такого подхода – выражение `mixin`, которое позволяет вам взять строку и попросить компилятор скомпилировать ее. Строка получается буквальным соединением операнда и оператора вручную. Способность трансформироваться в код, по счастливой случайности присущая строке op, фактически воплощает в жизнь эту идиому; на самом деле, весь этот механизм перегрузки проектировался с прицелом на `mixin`. Раньше D использовал отдельное имя для каждого оператора (`opAdd`, `opSub`, `opMul`, ...), что требовало механического запоминания соответствия имен операторам и написания группы функций с практически идентичными телами. + +[В начало ⮍](#12-2-1-объединение-определений-операторов-с-помощью-выражения-mixin) [Наверх ⮍](#12-перегрузка-операторов) ### 12.2.2. Постфиксный вариант операторов увеличения и уменьшения на единицу -Постфиксные операторы увеличения (`a++`) и уменьшения (`a--`) на едини -цу – необычные: они выглядят так же, как и их префиксные «коллеги», -так что различать их по идентификатору не получится. Дополнитель -ная проблема в том, что вызывающему коду, которому нужен результат -применения оператора, также должно быть доступно и старое значение -сущности, увеличенной на единицу. Наконец, постфиксные и префикс -ные варианты операторов увеличения и уменьшения на единицу долж -ны согласовываться друг с другом. +Постфиксные операторы увеличения (`a++`) и уменьшения (`a--`) на единицу – необычные: они выглядят так же, как и их префиксные «коллеги», так что различать их по идентификатору не получится. Дополнительная проблема в том, что вызывающему коду, которому нужен результат применения оператора, также должно быть доступно и старое значение сущности, увеличенной на единицу. Наконец, постфиксные и префиксные варианты операторов увеличения и уменьшения на единицу должны согласовываться друг с другом. -Постфиксное увеличение и уменьшение на единицу можно целиком -сгенерировать из префиксного увеличения и уменьшения на единицу -соответственно – нужно лишь немного шаблонного кода. Но вместо то -го чтобы заставлять вас писать этот шаблонный код, D делает это сам. -Замена `a++` выполняется так (постфиксное уменьшение на единицу об -рабатывается аналогично): +Постфиксное увеличение и уменьшение на единицу можно целиком сгенерировать из префиксного увеличения и уменьшения на единицу соответственно – нужно лишь немного шаблонного кода. Но вместо того чтобы заставлять вас писать этот шаблонный код, D делает это сам. Замена `a++` выполняется так (постфиксное уменьшение на единицу обрабатывается аналогично): -- если результат `a++` не используется, осуществляется замена на `++a`, -что затем перезаписывается на `a.opUnary! "++"()`; -- если результат `a++` используется (например, `arr[a++]`), заменой послу -жит выражение (тяжкий вздох) `((ref x) {auto t=x; ++x; return t;})(a)`. +- если результат `a++` не используется, осуществляется замена на `++a`, что затем перезаписывается на `a.opUnary! "++"()`; +- если результат `a++` используется (например, `arr[a++]`), заменой послужит выражение (тяжкий вздох) `((ref x) {auto t=x; ++x; return t;})(a)`. -В первом случае попросту обыгрывается тот факт, что постфиксный -оператор увеличения на единицу без применения результата делает то -же самое, что и префиксный вариант соответствующего оператора. Во -втором случае определяется лямбда-функция (см. раздел 5.6), выполня -ющая нужный шаблонный код: она создает новую копию входных дан -ных, прибавляет единицу к входным данным и возвращает созданную -ранее копию. Лямбда-функция применяется непосредственно к увели -чиваемому значению. +В первом случае попросту обыгрывается тот факт, что постфиксный оператор увеличения на единицу без применения результата делает то же самое, что и префиксный вариант соответствующего оператора. Во втором случае определяется лямбда-функция (см. раздел 5.6), выполняющая нужный шаблонный код: она создает новую копию входных данных, прибавляет единицу к входным данным и возвращает созданную ранее копию. Лямбда-функция применяется непосредственно к увеличиваемому значению. + +[В начало ⮍](#12-2-2-постфиксный-вариант-операторов-увеличения-и-уменьшения-на-единицу) [Наверх ⮍](#12-перегрузка-операторов) ### 12.2.3. Перегрузка оператора cast -Явное приведение типов осуществляется с помощью унарного операто -ра, применение которого выглядит как `cast(T) a`. Он немного отличает -ся от всех остальных операторов тем, что использует тип в качестве па -раметра, а потому для него выделена особая форма снижения. Для лю -бого `значения` пользовательского типа и некоторого другого типа T при -ведение +Явное приведение типов осуществляется с помощью унарного оператора, применение которого выглядит как `cast(T) a`. Он немного отличается от всех остальных операторов тем, что использует тип в качестве параметра, а потому для него выделена особая форма снижения. Для любого `значения` пользовательского типа и некоторого другого типа T приведение ```d cast(T) значение @@ -279,13 +171,7 @@ cast(T) значение значение.opCast!T() ``` -Реализация метода `opCast`, разумеется, должна возвращать значение -типа `T` – деталь, на которой настаивает компилятор. Несмотря на то что -перегрузка функций по значению аргумента не обеспечивается на уров -не средства языка, множественные определения `opCast` можно реализо -вать с помощью шаблонов с ограничениями сигнатуры. Например, ме -тоды приведения к типам `string` и `int` для некоторого типа `T` можно -определить так: +Реализация метода `opCast`, разумеется, должна возвращать значение типа `T` – деталь, на которой настаивает компилятор. Несмотря на то что перегрузка функций по значению аргумента не обеспечивается на уровне средства языка, множественные определения `opCast` можно реализовать с помощью шаблонов с ограничениями сигнатуры. Например, методы приведения к типам `string` и `int` для некоторого типа `T` можно определить так: ```d struct T @@ -302,17 +188,7 @@ struct T } ``` -Можно определить приведение к целому классу типов. «Надстроим» -пример с `CheckedInt`, определив приведение ко всем встроенным число -вым типам. Загвоздка в том, что некоторые из них могут обладать более -ограничивающим диапазоном значений, а нам бы хотелось гарантиро -вать, что преобразование не будет сопровождаться никакими потерями -информации. Дополнительная задача: хотелось бы избежать проверок -там, где они не требуются (например, нет нужды проверять границы -при преобразовании из `CheckedInt!int` в `long`). Поскольку информация -о границах доступна во время компиляции, вставка проверок лишь -там, где это необходимо, задается с помощью конструкции `static if` (см. -раздел 3.4): +Можно определить приведение к целому классу типов. «Надстроим» пример с `CheckedInt`, определив приведение ко всем встроенным числовым типам. Загвоздка в том, что некоторые из них могут обладать более ограничивающим диапазоном значений, а нам бы хотелось гарантировать, что преобразование не будет сопровождаться никакими потерями информации. Дополнительная задача: хотелось бы избежать проверок там, где они не требуются (например, нет нужды проверять границы при преобразовании из `CheckedInt!int` в `long`). Поскольку информация о границах доступна во время компиляции, вставка проверок лишь там, где это необходимо, задается с помощью конструкции `static if` (см. раздел 3.4): ```d struct CheckedInt(N) if (isIntegral!N) @@ -337,10 +213,11 @@ struct CheckedInt(N) if (isIntegral!N) } ``` +[В начало ⮍](#12-2-3-перегрузка-оператора-cast) [Наверх ⮍](#12-перегрузка-операторов) + ### 12.2.4. Перегрузка тернарной условной операции и ветвления -Встретив значение пользовательского типа, компилятор заменяет код -вида +Встретив значение пользовательского типа, компилятор заменяет код вида ```d a ? ‹выражение1› : ‹выражение2› @@ -352,8 +229,7 @@ a ? ‹выражение1› : ‹выражение2› cast(bool) a ? ‹выражение1› : ‹выражение2› ``` -Сходным образом компилятор переписывает проверку внутри конструк -ции `if` с +Сходным образом компилятор переписывает проверку внутри конструкции `if` с ```d if (a) ‹инструкция› // С блоком else или без него @@ -365,11 +241,9 @@ if (a) ‹инструкция› // С блоком else или без него if (cast(bool) a) ‹инструкция› ``` -Оператор отрицания `!` также переписывается в виде отрицания выра -жения с `cast`. +Оператор отрицания `!` также переписывается в виде отрицания выражения с `cast`. -Чтобы обеспечить выполнение таких проверок, определите метод при -ведения к типу `bool`, как это сделано здесь: +Чтобы обеспечить выполнение таких проверок, определите метод приведения к типу `bool`, как это сделано здесь: ```d struct MyArray(T) @@ -384,19 +258,17 @@ struct MyArray(T) } ``` +[В начало ⮍](#12-2-4-перегрузка-тернарной-условной-операции-и-ветвления) [Наверх ⮍](#12-перегрузка-операторов) + ## 12.3. Перегрузка бинарных операторов -В случае бинарных операторов `+` (сложение), `-` (вычитание), `*` (умноже -ние), `/` (деление), `%` (получение остатка от деления), `&` (поразрядное И), -`|` (поразрядное ИЛИ), `<<` (сдвиг влево), `>>` (сдвиг вправо), `~` (конкатенация) -и `in` (проверка на принадлежность множеству) выражение +В случае бинарных операторов `+` (сложение), `-` (вычитание), `*` (умножение), `/` (деление), `%` (получение остатка от деления), `&` (поразрядное И), `|` (поразрядное ИЛИ), `<<` (сдвиг влево), `>>` (сдвиг вправо), `~` (конкатенация) и `in` (проверка на принадлежность множеству) выражение ```d a ‹оп› b ``` -где по крайней мере один из операндов `a` и `b` имеет пользовательский -тип, переписывается в виде +где по крайней мере один из операндов `a` и `b` имеет пользовательский тип, переписывается в виде ```d a.opBinary!"‹оп›"(b) @@ -408,14 +280,9 @@ a.opBinary!"‹оп›"(b) b.opBinaryRight!"‹оп›"(a) ``` -Если разрешение имен и проверки перегрузки успешны лишь для одно -го из этих вызовов, он выбирается для замены. Если оба вызова допус -тимы, возникает ошибка в связи с двусмысленностью. Если же не под -ходит ни один из вызовов, очевидно, что перед нами ошибка «иденти -фикатор не найден». +Если разрешение имен и проверки перегрузки успешны лишь для одного из этих вызовов, он выбирается для замены. Если оба вызова допустимы, возникает ошибка в связи с двусмысленностью. Если же не подходит ни один из вызовов, очевидно, что перед нами ошибка «идентификатор не найден». -Продолжим наш пример с `CheckedInt` из раздела 12.2. Определим для -этого типа все бинарные операторы: +Продолжим наш пример с `CheckedInt` из раздела 12.2. Определим для этого типа все бинарные операторы: ```d struct CheckedInt(N) if (isIntegral!N) @@ -466,37 +333,17 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -(Многие из этих проверок можно осуществить дешевле – с помощью би -та переполнения, имеющегося у процессоров Intel, который при выпол -нении арифметических операций или устанавливается, или сбрасыва -ется. Но это аппаратно-зависимый способ.) Данный код определяет по -одному отдельному оператору для каждой уникальной проверки. Если -у двух и более операторов одинаковый код, они всегда объединяются -в один метод. Это сделано в случае операторов `/` и `%` (поскольку оба они -выполняют одну и ту же проверку), всех операторов сдвига и трех по -разрядных операторов, не требующих проверок. Здесь снова применен -подход, смысл которого – собрать операцию в виде строки, а потом с по -мощью `mixin` скомпилировать ее в выражение. +(Многие из этих проверок можно осуществить дешевле – с помощью бита переполнения, имеющегося у процессоров Intel, который при выполнении арифметических операций или устанавливается, или сбрасывается. Но это аппаратно-зависимый способ.) Данный код определяет по одному отдельному оператору для каждой уникальной проверки. Если у двух и более операторов одинаковый код, они всегда объединяются в один метод. Это сделано в случае операторов `/` и `%` (поскольку оба они выполняют одну и ту же проверку), всех операторов сдвига и трех поразрядных операторов, не требующих проверок. Здесь снова применен подход, смысл которого – собрать операцию в виде строки, а потом с помощью `mixin` скомпилировать ее в выражение. + +[В начало ⮍](#12-3-перегрузка-бинарных-операторов) [Наверх ⮍](#12-перегрузка-операторов) ### 12.3.1. Перегрузка операторов в квадрате -Если перегрузка операторов означает разрешение типам определять -собственную реализацию операторов, то перегрузка перегрузки опера -торов, то есть перегрузка операторов в квадрате, означает разрешение -типам определять некоторое количество перегруженных версий пере -груженных операторов. +Если перегрузка операторов означает разрешение типам определять собственную реализацию операторов, то перегрузка перегрузки операторов, то есть перегрузка операторов в квадрате, означает разрешение типам определять некоторое количество перегруженных версий перегруженных операторов. -Рассмотрим выражение `a * 5`, где операнд `a` имеет тип `CheckedInt!int`. Оно -не скомпилируется, поскольку до сих пор тип `CheckedInt` определял ме -тод `opBinary` с правым операндом типа `CheckedInt`. Так что для выполне -ния вычисления в клиентском коде нужно писать `a * CheckedInt!int(5)`, -что довольно неприятно. +Рассмотрим выражение `a * 5`, где операнд `a` имеет тип `CheckedInt!int`. Оно не скомпилируется, поскольку до сих пор тип `CheckedInt` определял метод `opBinary` с правым операндом типа `CheckedInt`. Так что для выполнения вычисления в клиентском коде нужно писать `a * CheckedInt!int(5)`, что довольно неприятно. -Верный способ решить эту проблему – определить еще одну или не -сколько дополнительных реализаций метода `opBinary` для типа `CheckedInt!N`, так чтобы на этот раз тип `N` ожидался справа от оператора. Может -показаться, что определение нового метода `opBinary` потребует изрядного -объема монотонной работы, но на самом деле достаточно добавить всего -одну строчку: +Верный способ решить эту проблему – определить еще одну или несколько дополнительных реализаций метода `opBinary` для типа `CheckedInt!N`, так чтобы на этот раз тип `N` ожидался справа от оператора. Может показаться, что определение нового метода `opBinary` потребует изрядного объема монотонной работы, но на самом деле достаточно добавить всего одну строчку: ```d struct CheckedInt(N) if (isIntegral!N) @@ -510,31 +357,15 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -Красота этого подхода в простоте: оператор преобразуется в обычный -идентификатор, который затем можно передать другой реализации опе -ратора. +Красота этого подхода в простоте: оператор преобразуется в обычный идентификатор, который затем можно передать другой реализации оператора. + +[В начало ⮍](#12-3-1-перегрузка-операторов-в-квадрате) [Наверх ⮍](#12-перегрузка-операторов) ### 12.3.2. Коммутативность -Присутствие `opBinaryRight` требуется в тех случаях, когда тип, опреде -ляющий оператор, является правым операндом, например, как в выра -жении `5 * a`. В этом случае тип операнда a имеет шанс «поймать» опера -тор, лишь определив метод `opBinaryRight!"*"(int)`. Здесь есть некоторая -избыточность – если, скажем, нужно организовать поддержку опера -ций, для которых не важно, с какой стороны подставлен целочислен -ный операнд (например, все равно, `5 * a` или `a * 5`), вам потребуется опре -делить как `opBinary!"*"(int)`, так и `opBinaryRight!"*"(int)`, а это расточи -тельство, т. к. умножение коммутативно. При этом, предоставив языку -принимать решение о коммутативности, можно столкнуться с излиш -ними ограничениями: свойство коммутативности зависит от алгебры; -например, умножение матриц некоммутативно. Поэтому компилятор -оставляет за пользователем право определить отдельные операторы для -правого и левого операндов, отказываясь брать на себя какую-либо от -ветственность за коммутативность операторов. +Присутствие `opBinaryRight` требуется в тех случаях, когда тип, определяющий оператор, является правым операндом, например, как в выражении `5 * a`. В этом случае тип операнда a имеет шанс «поймать» оператор, лишь определив метод `opBinaryRight!"*"(int)`. Здесь есть некоторая избыточность – если, скажем, нужно организовать поддержку операций, для которых не важно, с какой стороны подставлен целочисленный операнд (например, все равно, `5 * a` или `a * 5`), вам потребуется определить как `opBinary!"*"(int)`, так и `opBinaryRight!"*"(int)`, а это расточительство, т. к. умножение коммутативно. При этом, предоставив языку принимать решение о коммутативности, можно столкнуться с излишними ограничениями: свойство коммутативности зависит от алгебры; например, умножение матриц некоммутативно. Поэтому компилятор оставляет за пользователем право определить отдельные операторы для правого и левого операндов, отказываясь брать на себя какую-либо ответственность за коммутативность операторов. -Чтобы организовать поддержку `a ‹оп› b` и `b ‹оп› a`, когда один операнд -легко преобразуется к типу другого операнда, достаточно добавить од -ну строку: +Чтобы организовать поддержку `a ‹оп› b` и `b ‹оп› a`, когда один операндлегко преобразуется к типу другого операнда, достаточно добавить одну строку: ```d struct CheckedInt(N) if (isIntegral!N) @@ -548,24 +379,9 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -Все, что нужно, – получить соответствующее выражение с `CheckedInt` -слева. А затем вступают в права уже определенные операторы. +Все, что нужно, – получить соответствующее выражение с `CheckedInt` слева. А затем вступают в права уже определенные операторы. -Но иногда для преобразования требуется ряд дополнительных шагов, -без которых можно было бы обойтись. Например, представьте выра -жение `5 * c`, где `c` имеет тип `Complex!double`. Применив приведенное вы -ше решение, мы бы протолкнули умножение в выражение `Complex!double(5) * c`, при вычислении которого пришлось бы преобразовать `5` в ком -плексное число с нулевой мнимой частью, а затем зачем-то умножать -комплексные числа, когда достаточно было бы всего лишь двух умноже -ний действительных чисел. Результат, конечно, будет верным, но для -его получения пришлось бы гораздо больше попотеть. В таких случаях -лучше всего разделить правосторонние операции на две группы – ком -мутативные и некоммутативные операции – и обрабатывать их по от -дельности. Коммутативные операции можно обрабатывать просто с по -мощью перестановки аргументов. Некоммутативные операции можно -реализовывать так, чтобы каждый случай обрабатывался отдельно – -или каждый раз заново, или извлекая пользу из уже реализованных -примитивов. +Но иногда для преобразования требуется ряд дополнительных шагов, без которых можно было бы обойтись. Например, представьте выражение `5 * c`, где `c` имеет тип `Complex!double`. Применив приведенное выше решение, мы бы протолкнули умножение в выражение `Complex!double(5) * c`, при вычислении которого пришлось бы преобразовать `5` в комплексное число с нулевой мнимой частью, а затем зачем-то умножать комплексные числа, когда достаточно было бы всего лишь двух умножений действительных чисел. Результат, конечно, будет верным, но для его получения пришлось бы гораздо больше попотеть. В таких случаях лучше всего разделить правосторонние операции на две группы – коммутативные и некоммутативные операции – и обрабатывать их по отдельности. Коммутативные операции можно обрабатывать просто с помощью перестановки аргументов. Некоммутативные операции можно реализовывать так, чтобы каждый случай обрабатывался отдельно – или каждый раз заново, или извлекая пользу из уже реализованных примитивов. ```d struct Complex(N) if (isFloatingPoint!N) @@ -594,62 +410,32 @@ struct Complex(N) if (isFloatingPoint!N) } ``` -Для других типов можно выбрать другой способ группировки некото -рых групп операций, в таком случае могут пригодиться уже описанные -техники наложения ограничений на `op`. +Для других типов можно выбрать другой способ группировки некоторых групп операций, в таком случае могут пригодиться уже описанные техники наложения ограничений на `op`. + +[В начало ⮍](#12-3-2-коммутативность) [Наверх ⮍](#12-перегрузка-операторов) ## 12.4. Перегрузка операторов сравнения -В случае операторов сравнения (равенство и упорядочивание) D следует -той же схеме, с которой мы познакомились, обсуждая классы (см. раз -делы 6.8.3 и 6.8.4). Может показаться, что так сложилось исторически, -но есть и веские причины обрабатывать сравнения не в общем методе -`opBinary`, а иным способом. Во-первых, между операторами `==` и `!=` есть -тесные взаимоотношения, как и у всей четверки `<`, `<=`, `>` и `>=`. Эти взаимо -отношения подразумевают, что лучше использовать две отдельные -функции со специфическими именами, чем код, определяющий каж -дый оператор отдельно в зависимости от идентификаторов. Кроме того, -многие типы, скорее всего, будут определять лишь равенство и упоря -дочивание, а не все возможные операторы. С учетом этого факта для оп -ределения сравнений язык предоставляет простое и компактное сред -ство, не заставляя использовать мощный инструмент `opBinary`. +В случае операторов сравнения (равенство и упорядочивание) D следует той же схеме, с которой мы познакомились, обсуждая классы (см. разделы 6.8.3 и 6.8.4). Может показаться, что так сложилось исторически, но есть и веские причины обрабатывать сравнения не в общем методе `opBinary`, а иным способом. Во-первых, между операторами `==` и `!=` есть тесные взаимоотношения, как и у всей четверки `<`, `<=`, `>` и `>=`. Эти взаимоотношения подразумевают, что лучше использовать две отдельные функции со специфическими именами, чем код, определяющий каждый оператор отдельно в зависимости от идентификаторов. Кроме того, многие типы, скорее всего, будут определять лишь равенство и упорядочивание, а не все возможные операторы. С учетом этого факта для определения сравнений язык предоставляет простое и компактное средство, не заставляя использовать мощный инструмент `opBinary`. -Замена `a == b`, где хотя бы один из операндов `a` и `b` имеет пользователь -ский тип, производится по следующему алгоритму: +Замена `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`, так и `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`. - Иначе компилируется только одно из выражений `a.opCmp(b)` и `b.opCmp(a)`. Если первое, то заменой служит выражение `a.opCmp(b) ‹оп› 0`. Иначе заменой служит выражение `0 ‹оп› b.opCmp(a)`. -Здесь также стоит упомянуть о разумном обосновании одновременного -существования как `opEquals`, так и `opCmp`. На первый взгляд может пока -заться, что достаточно и одного метода `opCmp` (равенство было бы реали -зовано как `a.opCmp(b) == 0`). Но хотя большинство типов могут определить -равенство, многим типам нелегко реализовать отношение неравенства. -Например, матрицы и комплексные числа определяют равенство, одна -ко канонического определения отношения порядка им недостает. +Здесь также стоит упомянуть о разумном обосновании одновременного существования как `opEquals`, так и `opCmp`. На первый взгляд может показаться, что достаточно и одного метода `opCmp` (равенство было бы реализовано как `a.opCmp(b) == 0`). Но хотя большинство типов могут определить равенство, многим типам нелегко реализовать отношение неравенства. Например, матрицы и комплексные числа определяют равенство, однако канонического определения отношения порядка им недостает. + +[В начало ⮍](#12-4-перегрузка-операторов-сравнения) [Наверх ⮍](#12-перегрузка-операторов) ## 12.5. Перегрузка операторов присваивания -К операторам присваивания относится не только простое присваивание -вида `a = b`, но и присваивания с выполнением «на ходу» бинарных опе -раторов, например `a += b` или `a *= b`. В разделе 7.1.5.1 уже было показа -но, что выражение +К операторам присваивания относится не только простое присваивание вида `a = b`, но и присваивания с выполнением «на ходу» бинарных операторов, например `a += b` или `a *= b`. В разделе 7.1.5.1 уже было показано, что выражение ```d a = b @@ -673,9 +459,7 @@ a ‹оп›= b a.opOpAssign!"‹оп›"(b) ``` -Замена позволяет типу операнда a реализовать операции «на месте» по -описанным выше техникам. Рассмотрим пример реализации оператора -`+=` для типа `CheckedInt`: +Замена позволяет типу операнда a реализовать операции «на месте» по описанным выше техникам. Рассмотрим пример реализации оператора `+=` для типа `CheckedInt`: ```d struct CheckedInt(N) if (isIntegral!N) @@ -693,17 +477,7 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -В этом определении примечательны три детали. Во-первых, метод `opAssign` возвращает ссылку на текущий объект, благодаря чему поведение -`CheckedInt` становится сравнимым с поведением встроенных типов. Во- -вторых, истинное вычисление делается не «на месте», а напротив, «в сто -ронке». Собственно, состояние объекта изменяется лишь после удачного -выполнения проверки. В противном случае, если при вычислении выра -жения с `enforce` будет порождено исключение, мы рискуем испортить те -кущий объект. В-третьих, тело оператора фактически дублирует тело -метода `opBinary!"+"`, рассмотренного выше. Воспользуемся последним на -блюдением, чтобы задействовать имеющиеся реализации всех бинар -ных операторов в определении операторов присваивания, одновременно -выполняющих и бинарные операции. Вот новое определение: +В этом определении примечательны три детали. Во-первых, метод `opAssign` возвращает ссылку на текущий объект, благодаря чему поведение `CheckedInt` становится сравнимым с поведением встроенных типов. Во-вторых, истинное вычисление делается не «на месте», а напротив, «в сторонке». Собственно, состояние объекта изменяется лишь после удачного выполнения проверки. В противном случае, если при вычислении выражения с `enforce` будет порождено исключение, мы рискуем испортить текущий объект. В-третьих, тело оператора фактически дублирует тело метода `opBinary!"+"`, рассмотренного выше. Воспользуемся последним наблюдением, чтобы задействовать имеющиеся реализации всех бинарных операторов в определении операторов присваивания, одновременно выполняющих и бинарные операции. Вот новое определение: ```d struct CheckedInt(N) if (isIntegral!N) @@ -718,26 +492,15 @@ struct CheckedInt(N) if (isIntegral!N) } ``` -Можно было бы поступить и по-другому: определять бинарные операто -ры через операторы присваивания, определяемые с нуля. К этому выбо -ру можно прийти из соображений эффективности; для многих типов -изменение объекта «на месте» требует меньше памяти и выполняется -быстрее, чем создание нового объекта. +Можно было бы поступить и по-другому: определять бинарные операторы через операторы присваивания, определяемые с нуля. К этому выбору можно прийти из соображений эффективности; для многих типов изменение объекта «на месте» требует меньше памяти и выполняется быстрее, чем создание нового объекта. + +[В начало ⮍](#12-5-перегрузка-операторов-присваивания) [Наверх ⮍](#12-перегрузка-операторов) ## 12.6. Перегрузка операторов индексации -Язык D позволяет определять полностью абстрактный массив – массив, -который поддерживает все операции, обычно ожидаемые от массива, -но никогда не предоставляет адреса своих элементов клиентскому коду. -Перегрузка операторов индексации – необходимое условие реализации -этого средства. Чтобы обеспечить должный доступ по индексу, компи -лятор различает чтение и запись элементов. В последнем случае эле -мент массива находится слева от оператора присваивания, простой ли -это оператор `=` или выполняющийся «на месте» бинарный оператор, та -кой как `+=`. +Язык D позволяет определять полностью абстрактный массив – массив, который поддерживает все операции, обычно ожидаемые от массива, но никогда не предоставляет адреса своих элементов клиентскому коду. Перегрузка операторов индексации – необходимое условие реализации этого средства. Чтобы обеспечить должный доступ по индексу, компилятор различает чтение и запись элементов. В последнем случае элемент массива находится слева от оператора присваивания, простой ли это оператор `=` или выполняющийся «на месте» бинарный оператор, такой как `+=`. -Если никакого присваивания не выполняется, компилятор заменяет -выражение +Если никакого присваивания не выполняется, компилятор заменяет выражение ```d a[b1, b2, ..., bk] @@ -749,12 +512,9 @@ a[b1, b2, ..., bk] a.opIndex(b1, b2, ..., bk) ``` -для любого числа аргументов *k*. Сколько принимается аргументов, ка -кими должны быть их типы и каков тип результата, решает реализа -ция метода `opIndex`. +для любого числа аргументов *k*. Сколько принимается аргументов, какими должны быть их типы и каков тип результата, решает реализация метода `opIndex`. -Если результат применения оператора индексации участвует в при -сваивании слева, при снижении выражение +Если результат применения оператора индексации участвует в присваивании слева, при снижении выражение ```d a[b1, b2, ..., bk] = c @@ -766,26 +526,21 @@ a[b1, b2, ..., bk] = c a.opIndexAssign(c, b1, b2, ..., bk) ``` -Если к результату выражения с индексом применятся оператор увели -чения или уменьшения на единицу, выражение +Если к результату выражения с индексом применятся оператор увеличения или уменьшения на единицу, выражение ```d ‹оп› a[b1, b2, ..., bk] ``` -где в качестве `‹оп›` выступает или `++`, `--`, или унарный `-`, `+`, `~`, `*`, переписы -вается как +где в качестве `‹оп›` выступает или `++`, `--`, или унарный `-`, `+`, `~`, `*`, переписывается как ```d a.opIndexUnary!"‹оп›"(b1, b2, ..., bk) ``` -Постфиксные увеличение и уменьшение на единицу генерируются ав -томатически из соответствующих префиксных вариантов, как описано -в разделе 12.2.2. +Постфиксные увеличение и уменьшение на единицу генерируются автоматически из соответствующих префиксных вариантов, как описано в разделе 12.2.2. -Наконец, если полученный по индексу элемент изменяется «на месте», -при снижении выражение +Наконец, если полученный по индексу элемент изменяется «на месте», при снижении выражение ```d a[b1, b2, ..., bk] ‹оп›= c @@ -797,12 +552,7 @@ a[b1, b2, ..., bk] ‹оп›= c a.opIndexOpAssign!"‹оп›"(c, b1, b2, ..., bk) ``` -Эти замены позволяют типу операнда `a` полностью определить, каким -образом выполняется доступ к элементам, получаемым по индексу, -и как они обрабатываются. Для чего индексируемому типу брать на себя -ответственность за операторы присваивания? Казалось бы, более удач -ное решение – просто предоставить методу `opIndex` возвращать ссылку -на хранимый элемент, например: +Эти замены позволяют типу операнда `a` полностью определить, каким образом выполняется доступ к элементам, получаемым по индексу, и как они обрабатываются. Для чего индексируемому типу брать на себя ответственность за операторы присваивания? Казалось бы, более удачное решение – просто предоставить методу `opIndex` возвращать ссылку на хранимый элемент, например: ``` struct MyArray(T) @@ -811,33 +561,11 @@ struct MyArray(T) } ``` -Тогда какие бы операции присваивания и изменения-с-присваиванием -ни поддерживал тип `T`, они будут выполняться правильно. Предполо -жим, дан массив типа `MyArray!int` с именем `a`, тогда при вычислении вы -ражения `a[7] *= 2` сначала с помощью метода `opIndex` будет получено зна -чение типа `ref int`, а затем эта ссылка будет использована для умноже -ния «на месте» на 2. На самом деле, именно так и работают встроенные -массивы. +Тогда какие бы операции присваивания и изменения-с-присваиванием ни поддерживал тип `T`, они будут выполняться правильно. Предположим, дан массив типа `MyArray!int` с именем `a`, тогда при вычислении выражения `a[7] *= 2` сначала с помощью метода `opIndex` будет получено значение типа `ref int`, а затем эта ссылка будет использована для умножения «на месте» на 2. На самом деле, именно так и работают встроенные массивы. -К сожалению, это решение не без изъяна. Одна из проблем заключается -в том, что немалое число коллекций, построенных по принципу масси -ва, не пожелают предоставить доступ к своим элементам по ссылке. Они, -насколько это возможно, инкапсулируют расположение своих элемен -тов, обернув их в абстракцию. Преимущества такого подхода – обычные -плюсы сокрытия информации: у контейнера появляется свобода выбора -наилучшей стратегии хранения его элементов. Простой пример – опре -деление контейнера, содержащего объекты типа `bool`. Если бы контей -нер был обязан предоставлять доступ к `ref bool`, ему пришлось бы хра -нить каждое значение по отдельному адресу. Если же контейнер вправе -скрывать адреса, то он может сохранить восемь логических значений -в одном байте. +К сожалению, это решение не без изъяна. Одна из проблем заключается в том, что немалое число коллекций, построенных по принципу массива, не пожелают предоставить доступ к своим элементам по ссылке. Они, насколько это возможно, инкапсулируют расположение своих элементов, обернув их в абстракцию. Преимущества такого подхода – обычные плюсы сокрытия информации: у контейнера появляется свобода выбора наилучшей стратегии хранения его элементов. Простой пример – определение контейнера, содержащего объекты типа `bool`. Если бы контейнер был обязан предоставлять доступ к `ref bool`, ему пришлось бы хранить каждое значение по отдельному адресу. Если же контейнер вправе скрывать адреса, то он может сохранить восемь логических значений в одном байте. -Другой пример: для некоторых контейнеров доступ к данным неотде -лим от их изменения. Представим разреженный массив. Разреженные -массивы могут фиктивно содержать миллионы элементов, из которых -лишь горстка ненулевые, что позволяет разреженным массивам приме -нять стратегии хранения, экономичные в плане занимаемого места. -А теперь рассмотрим следующий код: +Другой пример: для некоторых контейнеров доступ к данным неотделим от их изменения. Представим разреженный массив. Разреженные массивы могут фиктивно содержать миллионы элементов, из которых лишь горстка ненулевые, что позволяет разреженным массивам применять стратегии хранения, экономичные в плане занимаемого места. А теперь рассмотрим следующий код: ```d SparseArray!double a; @@ -845,75 +573,45 @@ SparseArray!double a; a[8] += 2; ``` -Что должен предпринять массив, зависит как от его текущего содержи -мого, так и от новых данных: если ячейка `a[8]` не была ранее заполнена, -то создать ячейку со значением `2`; если ячейка была заполнена значени -ем `-2`, удалить эту ячейку, поскольку ее новым значением будет ноль, -а такие значения явно не сохраняются; если же ячейка содержала не -что помимо `-2`, выполнить сложение и записать полученное значение -обратно в ячейку. Реализовать эти действия или хотя бы большинство -из них невозможно, если требуется, чтобы метод `opIndex` возвращал -ссылку. +Что должен предпринять массив, зависит как от его текущего содержимого, так и от новых данных: если ячейка `a[8]` не была ранее заполнена, то создать ячейку со значением `2`; если ячейка была заполнена значением `-2`, удалить эту ячейку, поскольку ее новым значением будет ноль, а такие значения явно не сохраняются; если же ячейка содержала нечто помимо `-2`, выполнить сложение и записать полученное значение обратно в ячейку. Реализовать эти действия или хотя бы большинство из них невозможно, если требуется, чтобы метод `opIndex` возвращал ссылку. + +[В начало ⮍](#12-6-перегрузка-операторов-индексации) [Наверх ⮍](#12-перегрузка-операторов) ## 12.7. Перегрузка операторов среза -Массивы D предоставляют операторы среза `a[]` и `a[b1 .. b2]` (см. раз- -дел 4.1.3). Оба эти оператора могут быть перегружены пользователь -скими типами. Компилятор выполняет снижение, примерно как в слу -чае оператора индексации. +Массивы D предоставляют операторы среза `a[]` и `a[b1 .. b2]` (см. раздел 4.1.3). Оба эти оператора могут быть перегружены пользовательскими типами. Компилятор выполняет снижение, примерно как в случае оператора индексации. -Если нет никакого присваивания, компилятор переписывает `a[]` в виде -`a.opSlice()`, а `a[b1 .. b2]` – в виде `a.opSlice(b1, b2)`. +Если нет никакого присваивания, компилятор переписывает `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)`. +Снижения для операций со срезами делаются по образцу снижений для соответствующих операций, определенных для массивов. Во всех именах методов 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-7-перегрузка-операторов-среза) [Наверх ⮍](#12-перегрузка-операторов) ## 12.8. Оператор $ -В случае встроенных массивов язык D позволяет внутри индексных вы -ражений и среза обозначить длину массива идентификатором `$`. Напри -мер, выражение `a[0 .. $ - 1`] выбирает все элементы встроенного масси -ва a кроме последнего. +В случае встроенных массивов язык D позволяет внутри индексных выражений и среза обозначить длину массива идентификатором `$`. Например, выражение `a[0 .. $ - 1`] выбирает все элементы встроенного массива a кроме последнего. -Хотя этот оператор с виду довольно скромен, оказалось, что `$` сильно -повышает и без того хорошее настроение программиста на D. С другой -стороны, если бы оператор $ был «волшебным» и не допускал перегруз -ку, это бы неизменно раздражало, еще раз подтверждая, что встроен -ные типы должны лишь изредка обладать возможностями, недоступ -ными пользовательским типам. +Хотя этот оператор с виду довольно скромен, оказалось, что `$` сильно повышает и без того хорошее настроение программиста на D. С другой стороны, если бы оператор $ был «волшебным» и не допускал перегрузку, это бы неизменно раздражало, еще раз подтверждая, что встроенные типы должны лишь изредка обладать возможностями, недоступными пользовательским типам. Для пользовательских типов оператор `$` может быть перегружен так: -• для выражения `a[‹выраж›]`, где `a` имеет пользовательский тип: если -в `‹выраж›` встречается `$`, оно переписывается как `a.opDollar()`. Замена -одна и та же независимо от присваивания этого выражения; -• для выражения `a[‹выраж1›, ..., ‹выражk›]`: если в `‹выражi›` встречается `$`, -оно переписывается как `a.opDollar!(i)()`; -• для выражения `a[‹выраж1› .. ‹выраж2›]`: если в `‹выраж1›` или `‹выраж2›` встре -чается `$`, оно переписывается как `a.opDollar()`. +- для выражения `a[‹выраж›]`, где `a` имеет пользовательский тип: если в `‹выраж›` встречается `$`, оно переписывается как `a.opDollar()`. Замена одна и та же независимо от присваивания этого выражения; +- для выражения `a[‹выраж1›, ..., ‹выражk›]`: если в `‹выражi›` встречается `$`, оно переписывается как `a.opDollar!(i)()`; +- для выражения `a[‹выраж1› .. ‹выраж2›]`: если в `‹выраж1›` или `‹выраж2›` встречается `$`, оно переписывается как `a.opDollar()`. -Если `a` – результат некоторого выражения, это выражение вычисляется -только один раз. +Если `a` – результат некоторого выражения, это выражение вычисляется только один раз. + +[В начало ⮍](#12-8-оператор) [Наверх ⮍](#12-перегрузка-операторов) ## 12.9. Перегрузка foreach -Пользовательские типы могут существенным образом определять, как -цикл просмотра будет с ними работать. Это огромное благо для типов, -моделирующих коллекции, диапазоны, потоки и другие сущности, эле -менты которых можно перебирать. Более того, дела обстоят еще лучше: -есть целых два независимых способа организовать перегрузку, со свои -ми плюсами и минусами. +Пользовательские типы могут существенным образом определять, как цикл просмотра будет с ними работать. Это огромное благо для типов, моделирующих коллекции, диапазоны, потоки и другие сущности, элементы которых можно перебирать. Более того, дела обстоят еще лучше: есть целых два независимых способа организовать перегрузку, со своими плюсами и минусами. + +[В начало ⮍](#12-9-перегрузка-foreach) [Наверх ⮍](#12-перегрузка-операторов) ### 12.9.1. foreach с примитивами перебора -Первый способ определить, как цикл `foreach` должен работать с вашим -типом (структурой или классом), заключается в определении трех при -митивов перебора: свойства `empty` типа `bool`, сообщающего, остались ли -еще непросмотренные элементы, свойства `front`, возвращающего теку -щий просматриваемый элемент, и метода `popFront()`[^3], осуществляющего -переход к следующему элементу. Вот типичная реализация этих трех -примитивов: +Первый способ определить, как цикл `foreach` должен работать с вашим типом (структурой или классом), заключается в определении трех примитивов перебора: свойства `empty` типа `bool`, сообщающего, остались ли еще непросмотренные элементы, свойства `front`, возвращающего текущий просматриваемый элемент, и метода `popFront()`[^3], осуществляющего переход к следующему элементу. Вот типичная реализация этих трех примитивов: ```d struct SimpleList(T) @@ -933,8 +631,7 @@ public: } ``` -Имея такое определение, организовать перебор элементов списка про -ще простого: +Имея такое определение, организовать перебор элементов списка проще простого: ```d void process(SimpleList!int lst) @@ -946,9 +643,7 @@ void process(SimpleList!int lst) } ``` -Компилятор заменяет управляющий код `foreach` соответствующим цик -лом `for`, более неповоротливым, но мелкоструктурным аналогом, кото -рый и использует три рассмотренные примитива: +Компилятор заменяет управляющий код `foreach` соответствующим циклом `for`, более неповоротливым, но мелкоструктурным аналогом, который и использует три рассмотренные примитива: ```d void process(SimpleList!int lst) @@ -961,31 +656,17 @@ void process(SimpleList!int lst) } ``` -Если вы снабдите аргумент `value` ключевым словом `ref`, компилятор -заменит все обращения к `value` в теле цикла обращениями к свойству -`__c.front`. Таким образом, вы получаете возможность изменять элемен -ты списка напрямую. Конечно, и само ваше свойство `front` должно воз -вращать ссылку, иначе попытки использовать его как l-значение поро -дят ошибки. +Если вы снабдите аргумент `value` ключевым словом `ref`, компилятор заменит все обращения к `value` в теле цикла обращениями к свойству `__c.front`. Таким образом, вы получаете возможность изменять элементы списка напрямую. Конечно, и само ваше свойство `front` должно возвращать ссылку, иначе попытки использовать его как l-значение породят ошибки. -Последнее, но не менее важное: если просматриваемый объект предос -тавляет оператор среза без аргументов `lst[]`, `__c` инициализируется вы -ражением `lst[]`, а не `lst`. Это делается для того, чтобы разрешить «из -влечь» из контейнера средства перебора, не требуя определения трех -примитивов перебора. +Последнее, но не менее важное: если просматриваемый объект предоставляет оператор среза без аргументов `lst[]`, `__c` инициализируется выражением `lst[]`, а не `lst`. Это делается для того, чтобы разрешить «извлечь» из контейнера средства перебора, не требуя определения трех примитивов перебора. + +[В начало ⮍](#12-9-1-foreach-с-примитивами-перебора) [Наверх ⮍](#12-перегрузка-операторов) ### 12.9.2. foreach с внутренним перебором -Примитивы из предыдущего раздела образуют интерфейс перебора, ко -торый клиентский код может использовать, как заблагорассудится. Но -иногда лучше использовать *внутренний перебор*, когда просматривае -мая сущность полностью управляет процессом перебора и выполняет те -ло цикла самостоятельно. Такое перекладывание ответственности часто -может быть полезно, например, если полный просмотр коллекции пред -почтительнее выполнять рекурсивно (как в случае с деревьями). +Примитивы из предыдущего раздела образуют интерфейс перебора, который клиентский код может использовать, как заблагорассудится. Но иногда лучше использовать *внутренний перебор*, когда просматриваемая сущность полностью управляет процессом перебора и выполняет тело цикла самостоятельно. Такое перекладывание ответственности часто может быть полезно, например, если полный просмотр коллекции предпочтительнее выполнять рекурсивно (как в случае с деревьями). -Чтобы организовать цикл `foreach` с внутренним перебором, для вашей -структуры или класса нужно определить метод `opApply`[^4]. Например: +Чтобы организовать цикл `foreach` с внутренним перебором, для вашей структуры или класса нужно определить метод `opApply`[^4]. Например: ```d import std.stdio; @@ -1044,28 +725,11 @@ void main() 100 ``` -Компилятор упаковывает тело цикла (в данном случае `{ writeln(i); }`) -в делегат и передает его методу `opApply`. Компилятор организует испол -нение программы так, что код, выполняющий выход из цикла с помо -щью инструкции `break`, преждевременно возвращает `1` в качестве ре -зультата делегата, отсюда и манипуляции с `result` внутри `opApply`. +Компилятор упаковывает тело цикла (в данном случае `{ writeln(i); }`) в делегат и передает его методу `opApply`. Компилятор организует исполнение программы так, что код, выполняющий выход из цикла с помощью инструкции `break`, преждевременно возвращает `1` в качестве результата делегата, отсюда и манипуляции с `result` внутри `opApply`. -Зная все это, читать код метода `opApply` действительно легко: сначала -тело цикла применяется к корневому узлу, а затем рекурсивно к левому -и правому узлам. Простота реализации действительно имеет значение. -Если вы попробуете реализовать просмотр узлов дерева с помощью при -митивов `empty`, `front` и `popFront`, задача сильно усложнится. Так происхо -дит потому, что в методе `opApply` состояние итерации формируется неяв -но благодаря стеку вызовов. А при использовании трех примитивов пе -ребора вам придется управлять этим состоянием явно. +Зная все это, читать код метода `opApply` действительно легко: сначала тело цикла применяется к корневому узлу, а затем рекурсивно к левому и правому узлам. Простота реализации действительно имеет значение. Если вы попробуете реализовать просмотр узлов дерева с помощью примитивов `empty`, `front` и `popFront`, задача сильно усложнится. Так происходит потому, что в методе `opApply` состояние итерации формируется неявно благодаря стеку вызовов. А при использовании трех примитивов перебора вам придется управлять этим состоянием явно. -Упомянем еще одну достойную внимания деталь во взаимодействии -`foreach` и `opApply`. Переменная `i`, используемая в цикле, становится ча -стью типа делегата. К счастью, на тип этой переменной и даже на число -привязываемых к делегату переменных, задействованных в `foreach`, -ограничения не налагаются – все поддается настройке. Если вы опреде -лите метод `opApply` так, что он будет принимать делегат с двумя аргумен -тами, то сможете использовать цикл `foreach` следующего вида: +Упомянем еще одну достойную внимания деталь во взаимодействии `foreach` и `opApply`. Переменная `i`, используемая в цикле, становится частью типа делегата. К счастью, на тип этой переменной и даже на число привязываемых к делегату переменных, задействованных в `foreach`, ограничения не налагаются – все поддается настройке. Если вы определите метод `opApply` так, что он будет принимать делегат с двумя аргументами, то сможете использовать цикл `foreach` следующего вида: ```d // Вызывает метод object.opApply(delegate int(ref K k, ref V v){...}) @@ -1075,23 +739,13 @@ foreach (k, v; object) } ``` -На самом деле, просмотр ключей и значений встроенных ассоциатив -ных массивов реализован именно с помощью `opApply`. Для любого ассо -циативного массива типа `V[K]` справедливо, что делегат, принимаемый -методом `opApply`, ожидает в качестве параметров значения типов `V` и `K`. +На самом деле, просмотр ключей и значений встроенных ассоциативных массивов реализован именно с помощью `opApply`. Для любого ассоциативного массива типа `V[K]` справедливо, что делегат, принимаемый методом `opApply`, ожидает в качестве параметров значения типов `V` и `K`. + +[В начало ⮍](#12-9-2-foreach-с-внутренним-перебором) [Наверх ⮍](#12-перегрузка-операторов) ## 12.10. Определение перегруженных операторов в классах -Большинство рассмотренных замен включали вызовы методов с пара -метрами времени компиляции, таких как `opBinary(string)(T)`. Такие ме -тоды очень хорошо работают как внутри классов, так и внутри струк -тур. Единственная проблема в том, что методы с параметрами времени -компиляции неявно неизменяемы, и их нельзя переопределить, так что -для определения класса или интерфейса с переопределяемыми элемен -тами может потребоваться ряд дополнительных шагов. Простейшее ре -шение – написать, к примеру, метод `opBinary`, так чтобы он проталкивал -выполнение операции далее в обычный метод, который можно пере -определить: +Большинство рассмотренных замен включали вызовы методов с параметрами времени компиляции, таких как `opBinary(string)(T)`. Такие методы очень хорошо работают как внутри классов, так и внутри структур. Единственная проблема в том, что методы с параметрами времени компиляции неявно неизменяемы, и их нельзя переопределить, так что для определения класса или интерфейса с переопределяемыми элементами может потребоваться ряд дополнительных шагов. Простейшее решение – написать, к примеру, метод `opBinary`, так чтобы он проталкивал выполнение операции далее в обычный метод, который можно переопределить: ```d class A @@ -1119,11 +773,7 @@ class A } ``` -Такой подход позволяет решить поставленную задачу, но не оптималь -но, ведь оператор проверяется во время исполнения – действие, которое -может быть выполнено во время компиляции. Следующее решение по -зволяет исключить излишние затраты по времени за счет переноса про -верки внутрь обобщенной версии метода `opBinary`: +Такой подход позволяет решить поставленную задачу, но не оптимально, ведь оператор проверяется во время исполнения – действие, которое может быть выполнено во время компиляции. Следующее решение позволяет исключить излишние затраты по времени за счет переноса проверки внутрь обобщенной версии метода `opBinary`: ```d class A @@ -1155,18 +805,15 @@ class A } ``` -На этот раз каждому оператору соответствует свой метод. Вы, разумеет -ся, вправе выбрать операторы для перегрузки и способы их группирова -ния, соответствующие вашему случаю. +На этот раз каждому оператору соответствует свой метод. Вы, разумеется, вправе выбрать операторы для перегрузки и способы их группирования, соответствующие вашему случаю. + +[В начало ⮍](#12-10-определение-перегруженных-операторов-в-классах) [Наверх ⮍](#12-перегрузка-операторов) ## 12.11. Кое-что из другой оперы: opDispatch -Пожалуй, самая интересная из замен, открывающая максимум воз -можностей, – это замена с участием метода `opDispatch`. Именно она по -зволяет D встать в один ряд с гораздо более динамическими языками. +Пожалуй, самая интересная из замен, открывающая максимум возможностей, – это замена с участием метода `opDispatch`. Именно она позволяет D встать в один ряд с гораздо более динамическими языками. -Если некоторый тип `T` определяет метод `opDispatch`, компилятор пере -писывает выражение +Если некоторый тип `T` определяет метод `opDispatch`, компилятор переписывает выражение ```d a.fun(‹арг1›, ..., ‹аргk›) @@ -1178,16 +825,9 @@ a.fun(‹арг1›, ..., ‹аргk›) a.opDispatch!"fun"(‹арг1›, ..., ‹аргk›) ``` -для всех методов `fun`, которые должны были бы присутствовать, но не -определены, то есть для всех вызовов, которые бы иначе вызвали ошиб -ку «метод не определен». +для всех методов `fun`, которые должны были бы присутствовать, но не определены, то есть для всех вызовов, которые бы иначе вызвали ошибку «метод не определен». -Определение `opDispatch` может реализовывать много очень интерес -ных задумок разной степени динамичности. Рассмотрим пример мето -да `opDispatch`, реализующего подчинение альтернативному соглашению -именования методов класса. Для начала объявим простую функцию, -преобразующую идентификатор `такого_вида` в его альтернативу «в сти -ле верблюда» (camel-case) `такогоВида`: +Определение `opDispatch` может реализовывать много очень интересных задумок разной степени динамичности. Рассмотрим пример метода `opDispatch`, реализующего подчинение альтернативному соглашению именования методов класса. Для начала объявим простую функцию, преобразующую идентификатор `такого_вида` в его альтернативу «в стиле верблюда» (camel-case) `такогоВида`: ```d import std.ctype; @@ -1227,11 +867,7 @@ unittest } ``` -Вооружившись функцией `underscoresToCamelCase`, можно легко опреде -лить для некоторого класса метод `opDispatch`, заставляющий этот класс -принимать вызовы `a.метод_такого_вида()` и автоматически перенаправ -лять эти обращения к методам `a.методТакогоВида()` – и все это во время -компиляции. +Вооружившись функцией `underscoresToCamelCase`, можно легко определить для некоторого класса метод `opDispatch`, заставляющий этот класс принимать вызовы `a.метод_такого_вида()` и автоматически перенаправлять эти обращения к методам `a.методТакогоВида()` – и все это во время компиляции. ```d class A @@ -1256,30 +892,15 @@ unittest } ``` -Второй вызов не относится ни к одному из методов класса `A`, так что он -перенаправляется в метод `opDispatch` через вызов `a.opDispatch!"do_something_cool"(5, 6)`. `opDispatch`, в свою очередь, генерирует строку `"this.doSomethingCool(args)"`, а затем компилирует ее с помощью выражения `mixin`. -Учитывая, что с переменной `args` связана пара аргументов `5`, `6`, вызов -`mixin` в итоге сменяется вызовом `a.doSomethingCool(5, 6)` – старое доброе -перенаправление в своем лучшем проявлении. Миссия выполнена! +Второй вызов не относится ни к одному из методов класса `A`, так что он перенаправляется в метод `opDispatch` через вызов `a.opDispatch!"do_something_cool"(5, 6)`. `opDispatch`, в свою очередь, генерирует строку `"this.doSomethingCool(args)"`, а затем компилирует ее с помощью выражения `mixin`. Учитывая, что с переменной `args` связана пара аргументов `5`, `6`, вызов `mixin` в итоге сменяется вызовом `a.doSomethingCool(5, 6)` – старое доброе перенаправление в своем лучшем проявлении. Миссия выполнена! + +[В начало ⮍](#12-11-кое-что-из-другой-оперы-opdispatch) [Наверх ⮍](#12-перегрузка-операторов) ### 12.11.1. Динамическое диспетчирование с opDispatch -Хотя, конечно, интересно использовать `opDispatch` в разнообразных про -делках времени компиляции, реально интересные приложения требуют -динамичности. Динамические языки, такие как JavaScript или Small -talk, позволяют присоединять к объектам методы во время исполне -ния. Попробуем сделать нечто подобное на D: определим класс `Dynamic`, -позволяющий динамически добавлять, удалять и вызывать методы. +Хотя, конечно, интересно использовать `opDispatch` в разнообразных проделках времени компиляции, реально интересные приложения требуют динамичности. Динамические языки, такие как JavaScript или Smalltalk, позволяют присоединять к объектам методы во время исполнения. Попробуем сделать нечто подобное на D: определим класс `Dynamic`, позволяющий динамически добавлять, удалять и вызывать методы. -Во-первых, для таких динамических методов придется определить сиг -натуру времени исполнения. Здесь нам поможет тип `Variant` из модуля -`std.variant`. Это мастер на все руки: объект типа `Variant` может содер -жать практически любое значение. Такое свойство делает `Variant` иде -альным кандидатом на роль типа параметра и возвращаемого значения -динамического метода. Итак, определим сигнатуру такого динамиче -ского метода в виде делегата, который в качестве первого аргумента (иг -рающего роль `this`) принимает `Dynamic`, а вместо остальных аргументов – -массив элементов типа `Variant`, и возвращает результат типа `Variant`. +Во-первых, для таких динамических методов придется определить сигнатуру времени исполнения. Здесь нам поможет тип `Variant` из модуля `std.variant`. Это мастер на все руки: объект типа `Variant` может содержать практически любое значение. Такое свойство делает `Variant` идеальным кандидатом на роль типа параметра и возвращаемого значения динамического метода. Итак, определим сигнатуру такого динамического метода в виде делегата, который в качестве первого аргумента (играющего роль `this`) принимает `Dynamic`, а вместо остальных аргументов – массив элементов типа `Variant`, и возвращает результат типа `Variant`. ```d import std.variant; @@ -1287,12 +908,7 @@ import std.variant; alias Variant delegate(Dynamic self, Variant[] args...) DynMethod; ``` -Благодаря ... можно вызывать `DynMethod` с любым количеством аргумен -тов с уверенностью, что компилятор упакует их в массив. А теперь -определим класс `Dynamic`, который, как и обещано, позволит манипули -ровать методами во время исполнения. Чтобы обеспечить такие воз -можности, `Dynamic` определяет ассоциативный массив, отображающий -строки на элементы типа `DynMethod`: +Благодаря ... можно вызывать `DynMethod` с любым количеством аргументов с уверенностью, что компилятор упакует их в массив. А теперь определим класс `Dynamic`, который, как и обещано, позволит манипулировать методами во время исполнения. Чтобы обеспечить такие возможности, `Dynamic` определяет ассоциативный массив, отображающий строки на элементы типа `DynMethod`: ```d class Dynamic @@ -1345,62 +961,21 @@ unittest } ``` -Поскольку все методы должны соответствовать одной и той же сигнату -ре, добавление метода не обходится без некоторого синтаксического -шума. В этом примере довольно много незадействованных элементов: -добавляемый делегат не использует ни один из своих параметров и воз -вращает результат, не представляющий никакого интереса. Зато син -таксис вызова очень прозрачен. Это важно, так как обычно методы до -бавляются редко, а вызываются часто. Усовершенствовать класс `Dynamic` -можно разными путями. Например, можно определить информацион -ную функцию `getMethodInfo(string)`, возвращающую для заданного ме -тода число его параметров и их типы. +Поскольку все методы должны соответствовать одной и той же сигнатуре, добавление метода не обходится без некоторого синтаксического шума. В этом примере довольно много незадействованных элементов: добавляемый делегат не использует ни один из своих параметров и возвращает результат, не представляющий никакого интереса. Зато синтаксис вызова очень прозрачен. Это важно, так как обычно методы добавляются редко, а вызываются часто. Усовершенствовать класс `Dynamic` можно разными путями. Например, можно определить информационную функцию `getMethodInfo(string)`, возвращающую для заданного метода число его параметров и их типы. -Заметим, что в данном случае приходится идти на уступки, обычные -для решения о статическом или динамическом выполнении действий. -Чем больше вы делаете во время исполнения, тем чаще требуется соот -ветствовать общим форматам данных (`Variant` в нашем примере) и идти -на компромисс, жертвуя быстродействием (например, из-за поиска имен -методов во время исполнения). Взамен вы получаете возросшую гиб -кость: можно как угодно манипулировать определениями классов во -время исполнения, определять отношения динамического наследова -ния, взаимодействовать со скриптовыми языками, определять скрип -ты для собственных объектов и еще много чего. +Заметим, что в данном случае приходится идти на уступки, обычные для решения о статическом или динамическом выполнении действий. Чем больше вы делаете во время исполнения, тем чаще требуется соответствовать общим форматам данных (`Variant` в нашем примере) и идти на компромисс, жертвуя быстродействием (например, из-за поиска имен методов во время исполнения). Взамен вы получаете возросшую гибкость: можно как угодно манипулировать определениями классов во время исполнения, определять отношения динамического наследования, взаимодействовать со скриптовыми языками, определять скрипты для собственных объектов и еще много чего. + +[В начало ⮍](#12-11-1-динамическое-диспетчирование-с-opdispatch) [Наверх ⮍](#12-перегрузка-операторов) ## 12.12. Итоги и справочник -Пользовательские типы могут перегружать большинство операторов. -Есть несколько исключений, таких как «запятая» `,`, логическая конъ -юнкция `&&`, логическая дизъюнкция `||`, проверка на идентичность `is`, -тернарный оператор `?:`, а также унарные операторы получения адреса `&` -и `typeid`. Было решено, что перегрузка этих операторов добавит скорее -путаницы, чем гибкости. +Пользовательские типы могут перегружать большинство операторов. Есть несколько исключений, таких как «запятая» `,`, логическая конъюнкция `&&`, логическая дизъюнкция `||`, проверка на идентичность `is`, тернарный оператор `?:`, а также унарные операторы получения адреса `&` и `typeid`. Было решено, что перегрузка этих операторов добавит скорее путаницы, чем гибкости. -Кстати, о путанице. Заметим, что перегрузка операторов – это мощный -инструмент, к которому прилагается инструкция с предупреждением -той же мощности. В языке D лучший совет для вас: не используйте опе -раторы в экзотических целях, вроде определения целых встроенных -предметно-ориентированных языков (Domain-Specific Embedded Lan -guage, DSEL). Если желаете определять встроенные предметно-ориен -тированные языки, то для этой цели лучше всего подойдут строки -и выражение `mixin` (см. раздел 2.3.4.2) с вычислением функций на этапе -компиляции (см. раздел 5.12). Эти средства позволяют выполнить син -таксический разбор входных конструкций на DSEL, представленных -в виде строки времени компиляции, а затем сгенерировать соответству -ющий код на D. Такой подход требует больше труда, но пользователи -вашей библиотеки это оценят. +Кстати, о путанице. Заметим, что перегрузка операторов – это мощный инструмент, к которому прилагается инструкция с предупреждением той же мощности. В языке D лучший совет для вас: не используйте операторы в экзотических целях, вроде определения целых встроенных предметно-ориентированных языков (Domain-Specific Embedded Language, DSEL). Если желаете определять встроенные предметно-ориентированные языки, то для этой цели лучше всего подойдут строки и выражение `mixin` (см. раздел 2.3.4.2) с вычислением функций на этапе компиляции (см. раздел 5.12). Эти средства позволяют выполнить синтаксический разбор входных конструкций на DSEL, представленных в виде строки времени компиляции, а затем сгенерировать соответствующий код на D. Такой подход требует больше труда, но пользователи вашей библиотеки это оценят. -Определение `opDispatch` открывает новые горизонты, но это средство -также нужно использовать с умом. Чрезмерная динамичность может -снизить быстродействие программы за счет лишних манипуляций и ос -лабить проверку типов (например, не стоит забывать, что если в преды -дущем фрагменте кода вместо `a.helloWorld()` написать `a.heloWorld()`, код -все равно скомпилируется, а ошибка проявится лишь во время испол -нения). +Определение `opDispatch` открывает новые горизонты, но это средство также нужно использовать с умом. Чрезмерная динамичность может снизить быстродействие программы за счет лишних манипуляций и ослабить проверку типов (например, не стоит забывать, что если в предыдущем фрагменте кода вместо `a.helloWorld()` написать `a.heloWorld()`, код все равно скомпилируется, а ошибка проявится лишь во время исполнения). -В табл. 12.1 в сжатой форме представлена информация из этой главы. -Используйте эту таблицу как шпаргалку, когда будете перегружать -операторы для собственных типов. +В табл. 12.1 в сжатой форме представлена информация из этой главы. Используйте эту таблицу как шпаргалку, когда будете перегружать операторы для собственных типов. *Таблица 12.1. Перегруженные операторы* @@ -1432,6 +1007,8 @@ guage, DSEL). Если желаете определять встроенные |`a[] ‹оп›= c`|`a.opSliceOpAssign!"‹оп›"(c)`| |`a[b1 .. b2] ‹оп›= c`|`a.opSliceOpAssign!"‹оп›"(c, b1, b2)`| +[В начало ⮍](#12-12-итоги-и-справочник) [Наверх ⮍](#12-перегрузка-операторов) + [^1]: Автор использует понятия «тип» и «алгебра» не совсем точно. Тип определяет множество значений и множество операций, производимых над ними. Алгебра – это набор операций над определенным множеством. То есть уточнение «с алгебрами» – избыточно. – *Прим. науч. ред.* [^2]: В данном коде отсутствует проверка перехода за границы для оператора отрицания. – *Прим. науч. ред.* [^3]: Для перегрузки `foreach_reverse` служат примитивы `popBack` и `back` аналогичного назначения. – *Прим. науч. ред.*