This commit is contained in:
Alexander Zhirov 2023-01-25 09:10:02 +03:00
parent c0dec81098
commit 3f69cd0457
1 changed files with 754 additions and 20 deletions

View File

@ -6,22 +6,22 @@
- [3.4. Инструкция static if](#3-4-инструкция-static-if) - [3.4. Инструкция static if](#3-4-инструкция-static-if)
- [3.5. Инструкция switch](#3-5-инструкция-switch) - [3.5. Инструкция switch](#3-5-инструкция-switch)
- [3.6. Инструкция final switch](#3-6-инструкция-final-switch) - [3.6. Инструкция final switch](#3-6-инструкция-final-switch)
- [3.7. Циклы]() - [3.7. Циклы](#3-7-циклы)
- [3.7.1. Инструкция while (цикл с предусловием)]() - [3.7.1. Инструкция while (цикл с предусловием)](#3-7-1-инструкция-while-цикл-с-предусловием)
- [3.7.2. Инструкция do-while (цикл с постусловием)]() - [3.7.2. Инструкция do-while (цикл с постусловием)](#3-7-2-инструкция-do-while-цикл-с-постусловием)
- [3.7.3. Инструкция for (цикл со счетчиком)]() - [3.7.3. Инструкция for (цикл со счетчиком)](#3-7-3-инструкция-for-цикл-со-счетчиком)
- [3.7.4. Инструкция foreach (цикл просмотра)]() - [3.7.4. Инструкция foreach (цикл просмотра)](#3-7-4-инструкция-foreach-цикл-просмотра)
- [3.7.5. Цикл просмотра для работы с массивами]() - [3.7.5. Цикл просмотра для работы с массивами](#3-7-5-цикл-просмотра-для-работы-с-массивами)
- [3.7.6. Инструкции continue и break]() - [3.7.6. Инструкции continue и break](#3-7-6-инструкции-continue-и-break)
- [3.8. Инструкция goto (безусловный переход)]() - [3.8. Инструкция goto (безусловный переход)](#3-8-инструкция-goto-безусловный-переход)
- [3.9. Инструкция with]() - [3.9. Инструкция with](#3-9-инструкция-with)
- [3.10. Инструкция return]() - [3.10. Инструкция return](#3-10-инструкция-return)
- [3.11. Обработка исключительных ситуаций]() - [3.11. Обработка исключительных ситуаций](#3-11-обработка-исключительных-ситуаций)
- [3.12. Инструкция mixin]() - [3.12. Инструкция mixin](#3-12-инструкция-mixin)
- [3.13. Инструкция scope]() - [3.13. Инструкция scope](#3-13-инструкция-scope)
- [3.14. Инструкция synchronized]() - [3.14. Инструкция synchronized](#3-14-инструкция-synchronized)
- [3.15. Конструкция asm]() - [3.15. Конструкция asm](#3-15-конструкция-asm)
- [3.16. Итоги и справочник]() - [3.16. Итоги и справочник](#3-16-итоги-и-справочник)
Эта глава содержит обязательные определения всех инструкций языка D. D наследует внешний вид и функциональность языков семейства C в нем есть привычные инструкции `if`, `while`, `for` и другие. Наряду с этим D предлагает ряд новых интересных инструкций и некоторое усовершенствование старых. Если неизбежное перечисление с подробным описанием каждой инструкции заранее нагоняет на вас скуку, то вот вам несколько «отступлений» любопытных отличий D от других языков. Эта глава содержит обязательные определения всех инструкций языка D. D наследует внешний вид и функциональность языков семейства C в нем есть привычные инструкции `if`, `while`, `for` и другие. Наряду с этим D предлагает ряд новых интересных инструкций и некоторое усовершенствование старых. Если неизбежное перечисление с подробным описанием каждой инструкции заранее нагоняет на вас скуку, то вот вам несколько «отступлений» любопытных отличий D от других языков.
@ -302,10 +302,10 @@ switch (‹выражение›) ‹инструкция›
`‹выражение›` может иметь числовой, перечисляемый или строковый тип; `‹инструкция›` может содержать метки (ярлыки, labels), определенные следующим образом: `‹выражение›` может иметь числовой, перечисляемый или строковый тип; `‹инструкция›` может содержать метки (ярлыки, labels), определенные следующим образом:
1. `case ‹в›`: Перейти сюда, если `‹выражение› == ‹в›`. Чтобы можно было использовать внутри `в` запятые (см. раздел 2.3.18), все выражение требуется заключить в круглые скобки. 1. `case ‹в›`: перейти сюда, если `‹выражение› == ‹в›`. Чтобы можно было использовать внутри `в` запятые (см. раздел 2.3.18), все выражение требуется заключить в круглые скобки.
2. `case в1, в2, … , вn`: Каждая запись вида вk обозначает выражение. Рассматриваемая инструкция эквивалентна инструкции `case элемент1: case элемент2:, ... , case элементn:`. 2. `case в1, в2, … , вn`: каждая запись вида `вk` обозначает выражение. Рассматриваемая инструкция эквивалентна инструкции `case элемент1: case элемент2:, ... , case элементn:`.
3. `case в1: .. case в2`: Перейти сюда, если `‹выражение› >= в1` и `‹выражение› <= в2`. 3. `case в1: .. case в2`: перейти сюда, если `‹выражение› >= в1` и `‹выражение› <= в2`.
4. `default`: Перейти сюда, если никакой другой переход невозможен. 4. `default`: перейти сюда, если никакой другой переход невозможен.
`‹выражение›` вычисляется один раз для всех этих проверок. Выражение в каждой метке `case` это любое не противоречащее правилам языка выражение, которое можно проверить на равенство выражению `‹выражение›`, а также на неравенство в случае использования синтаксиса с интервалом. Обычно `case`-выражения представлены константами, вычисляемыми во время компиляции, но D разрешает использовать и переменные, гарантируя, что вычисления будут производиться в порядке следования альтернатив до первого совпадения. По завершении вычислений выполняется переход к соответствующей метке `case` или `default` и выполнение программы продолжается из этой точки. Для того чтобы покинуть ветвление, используется инструкция break, осуществляющая выход из инструкции `switch`. В отличие от языков C и C++, D запрещает неявный переход к следующей метке и требует инструкции `break` или `return` после кода, соответствующего метке. `‹выражение›` вычисляется один раз для всех этих проверок. Выражение в каждой метке `case` это любое не противоречащее правилам языка выражение, которое можно проверить на равенство выражению `‹выражение›`, а также на неравенство в случае использования синтаксиса с интервалом. Обычно `case`-выражения представлены константами, вычисляемыми во время компиляции, но D разрешает использовать и переменные, гарантируя, что вычисления будут производиться в порядке следования альтернатив до первого совпадения. По завершении вычислений выполняется переход к соответствующей метке `case` или `default` и выполнение программы продолжается из этой точки. Для того чтобы покинуть ветвление, используется инструкция break, осуществляющая выход из инструкции `switch`. В отличие от языков C и C++, D запрещает неявный переход к следующей метке и требует инструкции `break` или `return` после кода, соответствующего метке.
@ -388,5 +388,739 @@ Error: final switch statement must handle all values
[В начало ⮍](#3-6-инструкция-final-switch) [Наверх ⮍](#3-инструкции) [В начало ⮍](#3-6-инструкция-final-switch) [Наверх ⮍](#3-инструкции)
## 3.7. Циклы
### 3.7.1. Инструкция while (цикл с предусловием)
Да, именно так:
```d
while (‹выражение›) ‹инструкция›
```
Сначала вычисляется `‹выражение›`. Если оно ненулевое, выполняется `‹инструкция›` и цикл возобновляется: снова вычисляется `‹выражение›` и т. д. Иначе управление передается инструкции, расположенной сразу после цикла `while`.
[В начало ⮍](#3-7-1-инструкция-while-цикл-с-предусловием) [Наверх ⮍](#3-инструкции)
### 3.7.2. Инструкция do-while (цикл с постусловием)
Если нужен цикл, который обязательно выполнится хотя бы раз, подойдет цикл с постусловием:
```d
do ‹инструкция› while (‹выражение›);
```
Обратите внимание на обязательную точку с запятой в конце инструкции. Кроме того, после do должна быть хотя бы одна `‹инструкция›`. Цикл с постусловием эквивалентен циклу с предусловием, в котором сначала один раз выполняется `‹инструкция›`.
[В начало ⮍](#3-7-2-инструкция-do-while-цикл-с-постусловием) [Наверх ⮍](#3-инструкции)
### 3.7.3. Инструкция for (цикл со счетчиком)
Синтаксис цикла со счетчиком:
```d
for (‹определение счетчика›; выр1; выр2) ‹инструкция›
```
Любое из выражений ‹определение счетчика›, `выр1` и `выр2` (или все сразу) можно опустить. Если нет выражения `выр1`, считается, что оно истинно. Выражение `‹определение счетчика›` это или объявление значения (например, `auto i = 0;` или `float w;`), или выражение с точкой с запятой в конце (например, `i = 10;`). По семантике использования цикл со счетчиком идентичен одноименным инструкциям из других языков: сначала вычисляется `‹определение счетчика›`, затем `выр1`; если оно истинно, выполняется `‹инструкция›`, потом вычисляется `выр2`, после чего выполнение цикла продолжается новым вычислением `выр1`.
[В начало ⮍](#3-7-3-инструкция-for-цикл-со-счетчиком) [Наверх ⮍](#3-инструкции)
### 3.7.4. Инструкция foreach (цикл просмотра)
Самое удобное, безопасное и зачастую быстрое средство просмотра значений в цикле инструкция `foreach`[^3], у которой есть несколько вариантов. Простейший вариант цикла просмотра:
```d
foreach (‹идентификатор›; выражение1 .. выражение2) ‹инструкция›
```
`выражение1` и `выражение2` могут быть числами или указателями. Попросту говоря, `‹идентификатор›` проходит интервал от (включая) `выражения1` до (не включая) `выражения2`. Ради понятности этого неформального определения в нем не освещены некоторые детали. Например, сколько раз вычисляется `выражение2` в процессе выполнения цикла один или несколько? Или что происходит, если `выражение1 >= выражение2`? Все это легко узнать, взглянув на семантически эквивалентный код, приведенный ниже. Техника представления высокоуровневых конструкций в терминах эквивалентных конструкций более простого (под)языка (в виде абстракций более низкого уровня) называется *снижением* (*lowering*). Она будет широко использоваться на протяжении всей этой главы.
```d
{
auto __n = выражение2;
auto ‹идентификатор› = true ? выражение1 : выражение2;
for (; ‹идентификатор› < __n; ++идентификатор ) инструкция
}
```
Здесь идентификатор `__n` генерируется компилятором, что гарантирует отсутствие конфликтов с другими идентификаторами[^4] («свежее слово» в лексиконе тех, кто пишет компиляторы).
(Зачем нужны внешние фигурные скобки? Они гарантируют, что `‹идентификатор›` не «просочится» за пределы цикла `foreach`, а также благодаря им вся эта конструкция это одна инструкция.)
Теперь ясно, что и `выражение1`, и `выражение2` вычисляются всего один раз, а тип значения `‹идентификатор›` определяется по правилам для тернарной условной операции (см. раздел 2.3.16) вот в чем роль знаков `?:`, никак не проявляющих себя при исполнении программы. Осторожное «примирение» типов, достигнутое благодаря знакам `?:`, гарантирует предотвращение или, по крайней мере, выявление потенциальной неразберихи с числами разного размера и точности, а также конфликтов между типами со знаком и без знака.
Заметим, что компилятор принудительно не назначает `__n` какой-либо особый тип, то есть вы можете использовать этот вариант цикла `foreach` с пользовательскими типами, для которых определены оператор сравнения «меньше» (`<`) и оператор увеличения на единицу (мы научимся делать это в главе 12). Кроме того, если для типа не определен оператор `<`, но определен оператор сравнения на равенство, компилятор автоматически заменит оператор `<` оператором `!=` при снижении. В этом случае не может быть проверена корректность задания интервала, поэтому вы должны удостовериться, что верхняя граница может быть достигнута с помощью повторного применения оператора `++`, начиная с нижней границы. Иначе результат будет непредсказуемым.[^5]
Заметим, что вы можете определить нужный тип счетчика внутри части `‹идентификатор›` определения цикла. Обычно такое объявление излишне, но полезно, если вы хотите, чтобы тип счетчика удовлетворял ряду особых условий, исключал путаницу в знаковости/беззнаковости или при необходимости использования неявного преобразования типов:
```d
import std.math, std.stdio;
void main()
{
foreach (float elem; 1.0 .. 100.0)
{
writeln(log(elem)); // Получает логарифм с одинарной точностью
}
foreach (double elem; 1.0 .. 100.0)
{
writeln(log(elem)); // Двойная точность
}
foreach (elem; 1.0 .. 100.0)
{
writeln(log(elem)); // То же самое
}
}
```
[В начало ⮍](#3-7-4-инструкция-foreach-цикл-просмотра) [Наверх ⮍](#3-инструкции)
### 3.7.5. Цикл просмотра для работы с массивами
Перейдем к другому варианту инструкции `foreach`, предназначенному для работы с массивами и срезами:
```d
foreach (‹идентификатор›; ‹выражение›) ‹инструкция›
```
`‹выражение›` должно быть массивом (линейным или ассоциативным), срезом или иметь пользовательский тип. Последний случай мы рассмотрим в главе 12, а сейчас сосредоточимся на массивах и срезах. После того как `‹выражение›` было один раз вычислено, ссылка на него сохраняется в закрытой временной переменной. (Сам массив не копируется.) Затем переменной с именем `‹идентификатор›` по очереди присваивается каждый из элементов массива и выполняется `‹инструкция›`. Так же как и в случае с циклом просмотра с интервалами, допускается указание типа перед `‹идентификатором›`.
Инструкция `foreach` предполагает, что во время итераций длина массива изменяться не будет; если вы задумали иное, возможно, вам стоит задействовать простой цикл просмотра и побольше внимания.
**Обновление во время итерации**
Присваивание переменной `‹идентификатор›` внутри `‹инструкции›` не отражается на состоянии массива. Если вы действительно хотите изменить элемент, рассматриваемый в текущей итерации, определите `‹идентификатор›` как ссылку, расположив перед ним `ref` или `ref ‹тип›`. Например:
```d
void scale(float[] array, float s)
{
foreach (ref e; array)
{
e *= s; // Обновляет массив "на месте"
}
}
```
В приведенный код можно после `ref` добавить полное определение переменной e (включая ее тип), например `ref float e`. Однако на этот раз соответствие должно быть *точным*: `ref` запрещает преобразования типов!
```d
float[] arr = [ 1.0, 2.5, 4.0 ];
foreach (ref float elem; arr)
{
elem *= 2; // Без проблем
}
foreach (ref double elem; arr) // Ошибка!
{
elem /= 2;
}
```
Причина такого поведения программы проста: чтобы гарантировать корректное присваивание, `ref` ожидает точного совпадения представления. Несмотря на то что из значения типа `float` всегда можно получить значение типа `double`, вы не вправе обновить значение типа `float` присваиванием типа `double` по нескольким причинам, самая очевидная из которых разный размер этих типов.
**Где я?**
Иногда полезно иметь доступ к индексу итерации. Следующий вариант цикла просмотра позволяет привязать идентификатор к этому значению:
```d
foreach (идентификатор1, идентификатор2; ‹выражение›) ‹инструкция›
```
Так что можно написать:
```d
void print(int[] array)
{
foreach (i, e; array)
{
writefln("array[%s] = %s;", i, e);
}
}
```
Эта функция печатает содержание массива в виде, соответствующем коду на D. При выполнении `print([5, 2, 8])` выводится:
```d
array[0] = 5;
array[1] = 2;
array[2] = 8;
```
Гораздо интереснее наблюдать за обращением к индексу элемента при работе с ассоциативными массивами:
```d
void print(double[string] map)
{
foreach (i, e; map)
{
writefln("array['%s'] = %s;", i, e);
}
}
```
Теперь `print(["Луна": 1.283, "Солнце": 499.307, "Проксима Центавра": 133814298.759])` выведет
```d
array['Проксима Центавра'] = 1.33814e+08;
array['Солнце'] = 499.307;
array['Луна'] = 1.283;
```
Обратите внимание: элементы напечатаны не в том порядке, в каком они заданы в литерале. Интересно, что экспериментируя с тем же кодом, но в разных реализациях языка или разных версиях одной и той же реализации, можно наблюдать изменение порядка. Дело в том, что в ассоциативных массивах применяются таинственные методики, повышающие эффективность хранения и выборки элементов за счет отказа от гарантированного упорядочивания.
Тип индекса и самого элемента определяются по контексту. Можно действовать и по-другому, «навязывая» нужные типы одной из переменных `идентификатор1` и `идентификатор2` или обеим сразу. Однако помните, что `идентификатор1` не может быть ссылкой (перед ним нельзя поставить ключевое слово `ref`).
**Проделки**
Во время итерации можно по-разному изменять просматриваемый массив:
- *Изменение массива «на месте»*. Во время итерации будут «видны» изменения еще не посещенных ячеек массива.
- *Изменение размера массива*. Цикл повторяется, пока не будет просмотрено столько элементов массива, сколько в нем было до входа в цикл. Возможно, в результате изменения размера массив будет перемещен в другую область памяти; в этом случае последующее изменение массива не видно во время итерации, а также последующие изменения, вызванные во время самой итерации, не отразятся на массиве. Использовать такую технику не рекомендуется, поскольку правила перемещения массива в памяти зависят от реализации.
- *Освобождение выделенной под массив памяти (полное или частичное; в последнем случае говорят о «сжатии» массива) с помощью низкоуровневых функций управления памятью*. Желая получить полный контроль и достичь максимальной эффективности, вы не пожалели времени на изучение низкоуровневого управления памятью по документации к своей реализации языка. Все, что можно предположить: 1) вы знаете, что творите, и 2) не скучно с вами лишь тому, кто написал собственный сборщик мусора.
[В начало ⮍](#3-7-5-цикл-просмотра-для-работы-с-массивами) [Наверх ⮍](#3-инструкции)
### 3.7.6. Инструкции continue и break
Инструкция `continue` выполняет переход к началу новой итерации цикла, определяемого ближайшей к ней инструкцией `while`, `do-while`, `for` или `foreach`. Инструкции, расположенные между `continue` и концом тела цикла не выполняются.
Инструкция `break` выполняет переход к коду, расположенному сразу после ближайшей к ней инструкции `while`, `do-while`, `for`, `foreach`, `switch` или `final switch`, мгновенно завершая ее выполнение.
Обе инструкции можно использовать с необязательной меткой, указывающей, к какой именно инструкции они относятся. «Пометка» инструкций `continue` и `break` значительно упрощает построение сложных вариантов итераций, позволяя обойтись без переменных состояния и инструкции `goto`, описанной в следующем разделе.
```d
void fun(string[] strings)
{
loop: foreach (s; strings)
{
switch (s)
{
default: ...; break; // Выйти из инструкции switch
case "ls": ...; break; // Выйти из инструкции switch
case "rm": ...; break; // Выйти из инструкции switch
...
case "#": break loop; // Проигнорировать оставшиеся строки (прервать цикл foreach)
}
}
...
}
```
[В начало ⮍](#3-7-6-инструкции-continue-и-break) [Наверх ⮍](#3-инструкции)
## 3.8. Инструкция goto (безусловный переход)
В связи с глобальным потеплением не будем горячиться по поводу инструкции `goto`. Достаточно сказать, что в D она имеет следующий синтаксис:
```d
goto ‹метка›;
```
Идентификатор `‹метка›` должен быть виден внутри функции, где вызывается `goto`. Метка определяется явно как идентификатор с двоеточием, расположенный перед инструкцией. Например:
```d
int a;
...
mylabel: a = 1;
...
if (a == 0) goto mylabel;
```
Нельзя переопределять метки внутри одной и той же функции. Другое ограничение состоит в том, что `goto` не может «перепрыгнуть» точку определения значения, видимого в точке «приземления». Например:
```d
void main()
{
goto target;
int x = 10;
target: {} // Ошибка! goto заставляет пропустить определение x!
}
```
Наконец, инструкция `goto` не может выполнить переход за границу исключения (см. раздел 3.11). Таким образом, у инструкции `goto` почти нет ограничений, и именно это делает ее опасной. С помощью `goto` можно перейти куда угодно: вперед или назад, внутрь или за пределы инструкций `if`, внутрь и за пределы циклов, включая пресловутый переход прямо в середину тела цикла.
Тем не менее в D опасно не все, чего коснется `goto`. Если написать внутри конструкции `switch` инструкцию
```d
goto case ‹выражение›;
```
то будет выполнен переход к соответствующей метке `case ‹выражение›`. Инструкция
```d
goto case;
```
выполняет переход к следующей метке `case`. Инструкция
```d
goto default;
```
выполняет переход к метке `default`. Несмотря на то что эти переходы ничуть не более структурированы, чем любые другие случаи использования `goto`, их легче отслеживать, поскольку они расположены в одном месте программы и значительно упрощают структуру инструкции `switch`:
```d
enum Pref { superFast, veryFast, fast, accurate,regular, slow, slower };
Pref preference;
double coarseness = 1;
...
switch (preference)
{
case Pref.fast: ...; break;
case Pref.veryFast: coarseness = 1.5; goto case Pref.fast;
case Pref.superFast: coarseness = 3; goto case Pref.fast;
case Pref.accurate: ...; break;
case Pref.regular: goto default;
default: ...
...
}
```
При наличии инструкций `break` и `continue` с метками (см. раздел 3.7.6), исключений (см. раздел 3.11) и инструкции `scope` (мощное средство управления порядком выполнения программы, см. раздел 3.13) поклонникам `goto` все труднее найти себе достойное оправдание.
[В начало ⮍](#3-8-инструкция-goto-безусловный-переход) [Наверх ⮍](#3-инструкции)
## 3.9. Инструкция with
Созданная по примеру Паскаля инструкция `with` облегчает работу с конкретным объектом.
Синтаксис:
```d
with (‹выражение›) ‹инструкция›
```
сначала вычисляется `‹выражение›`, после чего внутренние элементы верхнего уровня вложенности полученного объекта делаются видимыми внутри `‹инструкции›`. Мы уже встречались со структурами в главе 1, поэтому рассмотрим пример с применением типа `struct`:
```d
import std.math, std.stdio;
struct Point
{
double x, y;
double norm() { return sqrt(x * x + y * y); }
}
void main()
{
Point p;
int z;
with (p)
{
x = 3; // Присваивает значение полю p.x
p.y = 4; // Хорошо, что все еще можно явно использовать p
writeln(norm()); // Выводит значение поля p.norm, то есть 5
z = 1; // Поле z осталось видимым
}
}
```
Изменения полей отражаются непосредственно на объекте, с которым работает инструкция `with`: она «распознает» в `p` l-значение и помнит об этом.
Если один из идентификаторов, включенный в область видимости с помощью инструкции `with`, перекрывает ранее определенный в функции идентификатор, то из-за возникшей неопределенности компилятор запрещает доступ к такому идентификатору. При том же определении структуры `Point` следующий код не скомпилируется:
```d
void fun()
{
Point p;
string y = "Я занимаюсь точкой (острю).";
with (p)
{
writeln(x, ":", y); // Ошибка! Полю p.y запрещено перекрывать переменную y!
}
}
```
Однако об ошибке сообщается только в случае *реальной*, а не *потенциальной* неопределенности. Например, если бы инструкция `with` в последнем примере вообще не использовала идентификатор `y`, этот код скомпилировался бы и запустился, несмотря на скрытую неопределенность. Кроме того, программа работала бы при замене строки `writeln(x, ":", y);` строкой `writeln(x, ":", p.y);`, поскольку явное указание принадлежности идентификатора `y` объекту `p` полностью исключает неопределенность.
Инструкция `with` может перекрывать различные идентификаторы уровня модуля (то есть глобальные идентификаторы). Доступ к идентификаторам, перекрытым инструкцией `with`, осуществляется с помощью синтаксиса `.идентификатор`.
Заметим, что можно сделать неявной множественную вложенность объектов, написав:
```d
with (выр1) with (выр2) ... with (вырn) ‹инструкция›
```
При использовании вложенных инструкций `with` нет угрозы неопределенности, так как язык запрещает во внутренней инструкции `with` перекрывать идентификатор, определенный во внешней инструкции `with`. В двух словах: в D локальному идентификатору запрещено перекрывать другой локальный идентификатор.
[В начало ⮍](#3-9-инструкция-with) [Наверх ⮍](#3-инструкции)
## 3.10. Инструкция return
Чтобы немедленно вернуть значение из текущей функции, напишите
```d
return ‹выражение›;
```
Эта инструкция вычисляет `‹выражение›` и возвращает полученное значение в точку вызова функции, предварительно неявно преобразовав его (если требуется) к типу, возвращаемому этой функцией.
Если текущая функция имеет тип `void`, `‹выражение›` должно быть опущено или представлять собой вызов функции, которая в свою очередь имеет тип `void`.
Выход из функции, возвращающей не `void`, должен осуществляться посредством инструкции `return`. Во время компиляции это трудно эффективно отследить, так что, возможно, иногда вы будете получать от компилятора необоснованные претензии.
[В начало ⮍](#3-10-инструкция-return) [Наверх ⮍](#3-инструкции)
## 3.11. Обработка исключительных ситуаций
Язык программирования D поддерживает обработку ошибок с помощью механизма исключительных ситуаций, или исключений (exceptions). Исключение инициируется инструкцией `throw`, а обрабатывается инструкцией `try`. Чтобы породить исключение, обычно пишут:
```d
throw new SomeException("Произошло нечто подозрительное");
```
Тип `SomeException` должен наследовать от встроенного класса `Throwable`. D не поддерживает создание исключительных ситуаций произвольных типов, отчасти потому, что, как мы скоро увидим, назначение определенного класса корневым облегчает обработку цепочек исключений разных типов.
Чтобы обработать исключение или просто быть в курсе, что оно произошло, используйте инструкцию `try`, которая в общем виде выглядит так:
```d
try ‹инструкция›
catch (И1 и1) инструкция1
catch (И2 и2) инструкция2
...
catch (Иn иn) инструкцияn
finally инструкцияf
```
Можно опустить компонент `finally`, как и любой из компонентов `catch` (или даже все компоненты `catch`). Однако должно соблюдаться условие: в инструкции `try` должен быть хотя бы один компонент `finally` или `catch`. `Иk` это типы, которые, как уже сказано, должны наследовать от `Throwable`. Идентификаторы `иk` связаны с захваченным объектом-исключением и могут отсутствовать.
Семантика всей инструкции такова. Сначала выполняется `‹инструкция›`. Если при ее выполнении возникает исключение (назовем его `‹их›` и будем считать, что оно имеет тип `‹Их›`), то предпринимаются попытки сопоставить типы `И1, И2, ..., Иn` с типом `‹Их›`. «Побеждает» первый тип `Иk`, который совпадает с `‹Их›` или является его предком. С объектом-исключением `‹их›` связывается идентификатор `иk` и выполняется `инструкцияk`. Исключение считается обработанным, поэтому если во время выполнения самой `инструкцииk` также возникнет исключение, информация о нем не будет разослана компонентам `catch`, содержащим возможные обработчики текущего исключения. Если ни один из типов `Иk` не подходит, исключение `‹их›` всплывает по стеку вызовов с целью поиска обработчика.
Если компонент `finally` присутствует, `инструкцияf` выполняется абсолютно во всех случаях: независимо от того, порождается исключение или нет, и даже если исключение было обработано одним из компонентов `catch` и в результате было порождено новое исключение. Этот код гарантированно выполняется (если, конечно, не помешают бесконечные циклы и системные вызовы, вызывающие останов программы). Если и `инструкцияf` порождает исключение, оно будет присоединено к текущей цепочке исключений. Механизм исключений языка D подробно описан в главе 9.
Инструкция `goto` (см. раздел 3.8) не может совершить переход внутрь инструкций `‹инструкция›`, `инструкция1`, `...`, `инструкцияn` и `инструкцияf`, кроме случая, когда `goto` находится внутри самой инструкции.
[В начало ⮍](#3-11-обработка-исключительных-ситуаций) [Наверх ⮍](#3-инструкции)
## 3.12. Инструкция mixin
Благодаря главе 2 (см. раздел 2.3.4.2) мы узнали, что с помощью выражений `mixin` можно преобразовывать строки, известные во время компиляции, в выражения на D, которые компилируются как обычный код. Инструкции с `mixin` предоставляют еще больше возможностей, позволяя создавать с помощью `mixin` не только выражения, но также объявления и инструкции.
Предположим, вы хотите как можно быстрее выяснить число ненулевых разрядов в байте. Это число, называемое весом Хемминга, используется в решении множества прикладных задач, таких как шифрование, распределенные вычисления и приближенный поиск в базе данных. Простейший способ подсчета ненулевых битов в байте: последовательно суммировать значения младшего бита, сдвигая каждый раз введенное число на один разряд вправо. Более быстрый метод был впервые предложен Питером Вегнером и популяризирован Керниганом и Ричи в их классическом труде:
```d
uint bitsSet(uint value)
{
uint result;
for (; value; ++result)
{
value &= value - 1;
}
return result;
}
unittest
{
assert(bitsSet(10) == 2);
assert(bitsSet(0) == 0);
assert(bitsSet(255) == 8);
}
```
Этот метод быстрее, чем самый очевидный, потому что цикл выполняется ровно столько раз, сколько ненулевых битов во введенном значении. Но функция `bitsSet` все равно тратит время на управление инструкциями; более быстрый метод это обращение к нужной ячейке таблицы (в терминах D к нужному элементу массива). Можно улучшить результат, заполнив таблицу еще во время компиляции; вот здесь и пригодится объявление с помощью инструкции `mixin`. Задумка в том, чтобы сначала создать строку, которая выглядит как объявление линейного массива, а затем с помощью `mixin` скомпилировать эту строку в обычный код. Генератор таблицы может выглядеть так:
```d
import std.conv;
string makeHammingWeightsTable(string name, uint max = 255)
{
string result = "immutable ubyte["~to!string(max + 1)~"] "~name~" = [ ";
foreach (b; 0 .. max + 1)
{
result ~= to!string(bitsSet(b)) ~ ", ";
}
return result ~ "];";
}
```
Вызов функции `makeHammingWeightsTable` возвращает строку `"immutable ubyte[256] t = [ 0, 1, 1, 2, ..., 7, 7, 8, ];"`. Квалификатор `immutable` (см. главу 8) указывает, что таблица никогда не изменится после инициализации. С библиотечной функцией `to!string` мы впервые встретились в разделе 1.6. Эта функция преобразует в строку любое значение (в данном случае значения типа `uint`, возвращаемые функцией `bitsSet`). Теперь, когда у нас есть нужный код в виде строки, для определения таблицы достаточно выполнить всего одно действие:
```d
mixin(makeHammingWeightsTable("hwTable"));
unittest
{
assert(hwTable[10] == 2);
assert(hwTable[0] == 0);
assert(hwTable[255] == 8);
}
```
Теоретически можно строить таблицы любого размера, но полученную программу всегда рекомендуется тестировать: из-за кэширования работа со слишком большими таблицами может на самом деле выполняться медленнее вычислений.
В качестве последнего средства (как тренер по айкидо скрепя сердце рекомендует ученикам газовый баллончик) стоит упомянуть сочетание импорта строки (инструкция `import`, см. раздел 2.2.5.1) и объявлений, созданных с помощью `mixin`, которое позволяет реализовать самую примитивную форму модульности текстовое включение. Полюбуйтесь:
```d
mixin(import("widget.d"));
```
Выражение `import` считывает текст файла `widget.d` в строковый литерал, который выражение `mixin` тут же преобразует в код. Используйте этот трюк, только если действительно считаете, что без него ваша честь хакера поставлена на карту.
[В начало ⮍](#3-12-инструкция-mixin) [Наверх ⮍](#3-инструкции)
## 3.13. Инструкция scope
Инструкция `scope` нововведение D, хотя и другие языки в той или иной форме реализуют подобную функциональность. Инструкция `scope` позволяет легко писать на D корректно работающий код и, главное, без проблем читать и понимать его впоследствии. Можно и другими средствами достичь свойственной коду со `scope` корректности, однако, за исключением самых заурядных примеров, результат окажется непостижимым.
Синтаксис:
```d
scope(exit) ‹инструкция›
```
`‹инструкция›` принудительно выполняется после того, как поток управления покинет текущую область видимости (контекст). Результат будет таким же, что и при использовании компонента `finally` инструкции `try`, но в общем случае инструкция `scope` более масштабируема. С помощью `scope(exit)` удобно гарантировать, что, оставляя контекст, вы «навели порядок». Допустим, в вашем приложении используется флаг `g_verbose` («говорливый»), который вы хотите временно отключить. Тогда можно написать:
```d
bool g_verbose;
...
void silentFunction()
{
auto oldVerbose = g_verbose;
scope(exit) g_verbose = oldVerbose;
g_verbose = false;
...
}
```
Остаток кода «молчаливой» функции `silentFunction` может быть любым, с досрочными выходами и возможными исключениями, но вы можете быть полностью уверены, что по окончании ее выполнения, даже если наступит конец света или начнется потоп, флаг `g_verbose` будет корректно восстановлен.
Чтобы в общих чертах представить действие инструкции `scope(exit)`, определим для нее *снижение*, то есть общий метод преобразования кода, содержащего `scope(exit)`, в эквивалентный код с другими инструкциями, такими как `try`. Мы уже неформально применяли технику снижения, рассматривая работу цикла со счетчиком в терминах цикла с предусловием, а цикла просмотра в терминах цикла со счетчиком.
Рассмотрим блок, содержащий инструкцию `scope(exit)`:
```d
{
инструкции1
scope(exit) инструкция2
инструкции3
}
```
Пусть явно отображенный вызов `scope` первый в этом блоке, то есть `инструкции1` не содержат вызовов `scope` (но инструкции `инструкция2` и `инструкции3` могут его содержать). Применив технику снижения, преобразуем этот код в код следующего вида:
```d
{
инструкции1
try
{
инструкции3
}
finally
{
инструкция2
}
}
```
На этом преобразование не заканчивается. Инструкции `инструкция2` и `инструкции3` также подвергаются снижению, поскольку могут содержать дополнительные инструкции `scope`. (Процесс снижения всегда конечен, так как фрагменты всегда строго меньше исходной последовательности.) Это означает, что код, содержащий несколько инструкций `scope(exit)`, вполне корректен, даже в таких странных случаях, как `scope(exit) scope(exit) scope(exit) writeln("?")`. Посмотрим, что происходит в любопытном случае, когда в одном и том же блоке встречаются две инструкции `scope(exit)`:
```d
{
инструкции1
scope(exit) инструкция2
инструкции3
scope(exit) инструкция4
инструкции5
}
```
Предположим, что ни одна из инструкций не содержит ни одного дополнительного вызова инструкции `scope`. Воспользовавшись снижением, получим:
```d
{
инструкции1
try
{
инструкции3
try
{
инструкции5
}
finally
{
инструкция4
}
}
finally
{
инструкция2
}
}
```
Этот громоздкий код поможет нам выяснить порядок выполнения нескольких инструкций `scope(exit)` в одном блоке. Проследив порядок выполнения инструкций в полученном коде, можно сделать вывод, что инструкция `инструкция4` выполняется до инструкции `инструкция2`. Обобщенно, инструкции `scope(exit)` выполняются по схеме LIFO[^6]: в порядке, обратном их следованию в тексте программы.
Отслеживать порядок выполнения инструкций `scope` гораздо легче, чем порядок выполнения эквивалентного кода с конструкцией `try/finally`; элементарно: инструкция `scope` гарантирует, что управляемая ею инструкция будет выполнена при выходе из контекста. Это позволяет вам защитить свой код от ошибок без неудобной иерархии конструкций `try/finally` простым перечислением нужных действий в одной строке.
Предыдущий пример демонстрирует еще одно прекрасное свойство инструкции `scope` масштабируемость. С учетом огромной масштабируемости эта инструкция просто неотразима. (В конце концов, если бы требовалось лишь изредка выполнять одну-единственную инструкцию `scope`, можно было бы вручную написать ее сниженный эквивалент по указанной выше методике.) Функциональность нескольких инструкций `scope(exit)` требует увеличения длины кода программы при использовании самих инструкций `scope(exit)` и одновременного увеличения как длины, так и глубины кода при использовании эквивалентных инструкций `try`. Причем в глубину код масштабируется очень слабо, к тому же приходится делить «владения» с другими составными инструкциями, такими как `if` или `for`. Еще один подходящий вариант масштабируемого решения применение деструкторов в стиле C++ (также поддерживаемых D; см. главу 7), если только вам удастся снизить стоимость определения новых типов. Но если приходится определять класс только потому, что понадобился его деструктор (а зачем еще нужен класс типа `CleanerUpper`[^7]?), то в плане масштабируемости это решение даже хуже вложенных инструкций `try`. Вкратце, если классы вакуумная сварка, а инструкции `try` жевательная резинка, то инструкция `scope(exit)` эпоксидный суперклей.
Инструкция `scope(success) ‹инструкция›` включает `‹инструкцию›` в «график» программы только в случае обычного выхода из текущей области видимости (не в результате исключения). Выполним снижение для инструкции `scope(success)`. Код
```d
{
инструкции1
scope(success) инструкция2
инструкции3
}
```
превращается в
```d
{
инструкции1
bool __succeeded = true;
try
{
инструкции3
}
catch(Exception e)
{
__succeeded = false;
throw e;
}
finally
{
if (__succeeded) инструкция2
}
}
```
Далее, инструкции `инструкция2` и `инструкции3` также подвергаются снижению; процесс повторяется, пока не останется вложенных инструкций `scope`.
Перейдем к более мрачному варианту инструкции `scope` инструкции `scope(failure) ‹инструкция›`. Такая запись предписывает выполнить `‹инструкцию›` только при выходе из текущего контекста в результате возникшей исключительной ситуации.
Снижение для инструкции `scope(failure)` практически идентично снижению для инструкции `scope(success)`, с тем лишь отличием, что флаг `__succeeded` проверяется на равенство `false`, а не `true`. Код
```d
{
инструкции1
scope(failure) инструкция2
инструкции3
}
```
превращается в
```d
{
инструкции1
bool __succeeded = true;
try
{
инструкции3
}
catch(Exception e)
{
__succeeded = false;
throw e;
}
finally
{
if (!__succeeded) инструкция2
}
}
```
Далее выполняется снижение инструкций `инструкция2` и `инструкции3`.
Инструкция `scope` может пригодиться во многих ситуациях. Предположим, вы хотите создать файл способом транзакции то есть не оставляя на диске «частично созданный» файл, если в процессе его создания произойдет сбой. Здесь можно поступить так:
```d
import std.contracts, std.stdio;
void transactionalCreate(string filename)
{
string tempFilename = filename ~ ".fragment";
scope(success)
{
std.file.rename(tempFilename, filename);
}
auto f = File(tempFilename, "w");
... // Спокойно пишете в f
}
```
Инструкция `scope(success)` заранее определяет цель работы функции. Эквивалентный код без `scope` получился бы гораздо более замысловатым; к тому же обычно программист слишком занят кодом, выполняющимся при ожидаемых условиях, чтобы найти время для обработки маловероятных ситуаций. Поэтому необходимо, чтобы язык максимально облегчал обработку ошибок.
Большой плюс такого стиля программирования состоит в том, что весь код обработки ошибок собран в начале функции `transactionalCreate` и никак не затрагивает основной код. При всей своей простоте функция `transactionalCreate` очень надежна: вы получаете или готовый файл, или временный файл-фрагмент, но только не «битый» файл, который кажется нормальным.
[В начало ⮍](#3-13-инструкция-scope) [Наверх ⮍](#3-инструкции)
## 3.14. Инструкция synchronized
Инструкция `synchronized` имеет следующий синтаксис:
```d
synchronized (выражение1, выражение2...) ‹инструкция›
```
С ее помощью можно расставлять контекстные блокировки в многопоточных программах. Семантика инструкции `synchronized` определена в главе 13.
[В начало ⮍](#3-14-инструкция-synchronized) [Наверх ⮍](#3-инструкции)
## 3.15. Конструкция asm
D нарушил бы свой обет быть языком для системного программирования, если бы не предоставил некоторые средства для взаимодействия с ассемблером. И если вы любите трудности, то будете счастливы узнать, что в D есть тщательно определенный встроенный язык ассемблера для Intel x86. Кроме того, этот язык переносим между всеми реализациями D, работающими на машинах x86. Поскольку ассемблер зависит только от машины, а не от операционной системы, на первый взгляд это средство D не кажется революционным, тем не менее вы будете удивлены. Исторически сложилось, что каждая операционная система определяет собственный синтаксис ассемблера, не совместимый с другими ОС, поэтому, например, код, написанный для Windows, не будет работать под управлением Linux, так как синтаксисы ассемблеров этих операционных систем разительно отличаются друг от друга (что вряд ли оправданно). D разрубает этот гордиев узел, отказавшись от внешнего ассемблера, специфичного для каждой системы. Вместо этого компилятор сам выполняет синтаксический анализ и распознает инструкции ассемблерного языка. Чтобы написать код на ассемблере, делайте так:
```d
asm ‹инструкция на ассемблере›
```
или так:
```d
asm { ‹инструкции на ассемблере› }
```
Идентификаторы, видимые перед конструкцией `asm`, доступны и внутри нее: ассемблерный код может использовать сущности D. Ассемблер D описывается в главе 11; он покажется знакомым любому, кто работал с ассемблером x86. Всю информацию по ассемблеру D вы найдете в документации.
[В начало ⮍](#3-15-конструкция-asm) [Наверх ⮍](#3-инструкции)
## 3.16. Итоги и справочник
D предоставляет все ожидаемые обычные инструкции, предлагая при этом и несколько новинок, таких как `static if`, `final switch` и `scope`. Таблица 3.1 краткий справочник по всем инструкциям языка D (за подробностями обращайтесь к соответствующим разделам этой главы).
*Таблица 3.1. Справочник по инструкциям (`‹и›` инструкция, `‹в›` выражение, `o` объявление, `х` идентификатор)*
|Инструкция|Описание|
|-|-|
|`‹в›;`|Вычисляет `‹в›`. Ничего не изменяющие выражения, включающие лишь встроенные типы и операторы, запрещены ([см. раздел 3.1]())|
|`{и1 ... и2}`|Выполняет инструкции от `и1` до `и2` по порядку, пока управление не будет явно передано в другую область видимости (например, инструкцией `return`) ([см. раздел 3.2]())|
|`asm ‹и›`|Машиннозависимый ассемблерный код (здесь `‹и›` обозначает ассемблерный код, а не инструкцию на языке D). В настоящее время поддерживается ассемблер x86 с единым синтаксисом для всех поддерживаемых операционных систем ([см. раздел 3.15]())|
|`break;`|Прерывает выполнение инструкции `switch`, `for`, `foreach`, `while` или `do-while` с переходом к инструкции, следующей сразу за ней ([см. раздел 3.7.6]())|
|`break x;`|Прерывает выполнение инструкции `switch`, `for`, `foreach`, `while` или `do-while`, имеющей метку `x:`, с переходом к инструкции, следующей сразу за ней ([см. раздел 3.7.6]())|
|`continue;`|Начинает новую итерацию текущего (ближайшего к ней) цикла `for`, `foreach`, `while` или `do-while` с пропуском оставшейся части этого цикла ([см. раздел 3.7.6]())|
|`continue x;`|Начинает новую итерацию цикла `for`, `foreach`, `while` или `do-while`, снабженного меткой `x:`, с пропуском оставшейся части этого цикла ([см. раздел 3.7.6]())|
|`do ‹и› while (‹в›);`|Выполняет `‹и›` один раз и продолжает ее выполнять, пока `‹в›` истинно|
|`for (и1 в1; в2) и2`|Выполняет `и1`, которая может быть инструкцией-выражением, определением значения или просто точкой с запятой, и пока `в1` истинно, выполняет `и2`, после чего вычисляет `в2`|
|`foreach (x; в1 .. в2) ‹и›`|Выполняет `‹и›`, инициализируя переменную `x` значением `в1` и затем последовательно увеличивая ее на 1, пока `x в2`. Цикл не выполняется, если `в1 = в2`. Как `в1`, так и `в2` вычисляются всего один раз ([см. раздел 3.7.4]())|
|`foreach (refопц x; ‹в›) ‹и›`|Выполняет `‹и›`, объявляя переменную `x` и привязывая ее к каждому из элементов `‹в›` поочередно. Результатом вычисления `‹в›` должен быть массив или любой пользовательский тип-диапазон. Если присутствует ключевое слово `ref`, изменения `x` будут отражаться и на просматриваемой сущности ([см. раздел 3.7.5]())|
|`foreach (x1, refопц x2; ‹в›) ‹и›`|Аналогична предыдущей, но вводит дополнительное значение `x1`. Если `‹в›` это ассоциативный массив, то `x1` привязывается к ключу, а `x2` к рассматриваемому значению. Иначе `x1` привязывается к целому числу, показывающему количество проходов цикла (начиная с 0) ([см. раздел 3.7.5]())|
|`goto x;`|Выполняет переход к метке `x`, которая должна быть определена в текущей функции как `x:` ([см. раздел 3.8]())|
|`goto case;`|Выполняет переход к следующей метке `case` текущей инструкции `switch` ([см. раздел 3.8]())|
|`goto case x;`|Выполняет переход к метке `case x` текущей инструкции `switch` ([см. раздел 3.8]())|
|`goto default;`|Выполняет переход к метке обработчика по умолчанию `default` текущей инструкции `switch` ([см. раздел 3.8]())|
|`if (‹в›) ‹и›`|Выполняет `‹и›`, если `‹в›` ненулевое ([см. раздел 3.3]())|
|`if (‹в›) и1 else и2`|Выполняет `и1`, если `‹в›` ненулевое, иначе выполняет `и2`. Компонент `else`, расположенный в конце, относится к последней инструкции `if` или `static if` ([см. раздел 3.3]())|
|`static if (‹в›)о/и›`|Вычисляет `‹в›` во время компиляции и, если `‹в›` ненулевое, компилирует объявление или инструкцию `о/и›`. Если объявление или инструкция `о/и›` заключены в `{` и `}`, то одна пара таких скобок срезается ([см. раздел 3.4]())|
|`static if (‹в›)о/и1 else о/к2`|Аналогична предыдущей плюс в случае ложности `‹в›` компилирует `о/и2`. Часть `else`, расположенная в конце, относится к последней инструкции `if` или `static if` ([см. раздел 3.4]())|
|`return ‹в›опц;`|Возврат из текущей функции. Возвращаемое значение должно быть таким, чтобы его можно было неявно преобразовать к объявленному возвращаемому типу. `‹в›` может быть опущено, если возвращаемый тип функции `void`|
|`scope(exit) ‹и›`|Выполняет `‹и›`, каким бы образом ни был осуществлен выход из текущего контекста (то есть с помощью `return`, из-за необработанной ошибки или по исключительной ситуации). Вложенные инструкции `scope` (в том числе с ключевыми словами `failure` и `success`) выполняются в порядке, обратном их определению в коде программы ([см. раздел 3.13]())|
|`scope(failure) ‹и›`|Выполняет `‹и›`, если выход из текущего контекста осуществлен по исключительной ситуации ([см. раздел 3.13]())|
|`scope(success) ‹и›`|Выполняет `‹и›` при нормальном выходе из текущего контекста (через `return` или по достижении конца контекста) ([см. раздел 3.13]())|
|`switch (‹в›) ‹и›`|Вычисляет `‹в›` и выполняет переход к метке `case`, соответствующей `‹в›` и расположенной внутри `‹и›` ([см. раздел 3.5]())|
|`final switch (‹в›) ‹и›`|Аналогична предыдущей, но работает только с перечисляемым и значениями и во время компиляции проверяет, обработаны ли все возможные значения с помощью меток `case` ([см. раздел 3.6]())|
|`synchronized (в1, в2…)‹и›`|Выполняет `‹и›`, в то время как объекты, возвращаемые `в1`, `в2` и т.д., заблокированы. Выражения `вi` должны возвращать объект типа `class` ([см. раздел 3.14]())|
|`throw (‹в›);`|Вычисляет `‹в›` и порождает соответствующее исключение с переходом в ближайший подходящий обработчик `catch`. `‹в›` должно иметь тип `Throwable` или наследующий от него ([см. раздел 3.11]())|
|`try ‹и› catch(Т1 x1) и1 ... catch(Тn xn) иn finally иf`|Выполняет `‹и›`. Если при этом возникает исключение, пытается сопоставить его тип с типами `Т1`, `...`, `Тn` по порядку. Если `k`-е сопоставление оказалось удачным, то далее сопоставления не производятся и выполняется `иk`. В любом случае (завершилось выполнение `‹и›` исключением или нет) перед выходом из `try` выполняется `иf`. Все компоненты `catch` и `finally` (но не то и другое одновременно) могут быть опущены ([см. раздел 3.11]())|
|`while (‹в›) ‹и›`|Выполняет `‹и›`, пока `‹в›` ненулевое (цикл не выполняется, если уже при первом вычислении `‹в›` оказывается нулевым)|
|`with (‹в›) ‹и›`|Вычисляет `‹в›`, затем выполняет `‹и›`, как если бы она была членом типа `‹в›`: все используемые в `‹и›` идентификаторы сначала ищутся в пространстве имен, определенном `‹в›` ([см. раздел 3.9]())|
[В начало ⮍](#3-16-итоги-и-справочник) [Наверх ⮍](#3-инструкции)
[^1]: Да-да, это «еще одно место, где используется ключевое слово `static»`. [^1]: Да-да, это «еще одно место, где используется ключевое слово `static»`.
[^2]: Тип `enum` будет рассмотрен позже. Для понимания примера надо знать, что значения объявленные как `enum`, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. *Прим. науч. ред.* [^2]: Тип `enum` будет рассмотрен позже. Для понимания примера надо знать, что значения объявленные как `enum`, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. *Прим. науч. ред.*
[^3]: Существует также цикл `foreach_reverse`, который работает аналогично `foreach`, но перебирает значения в обратном порядке.
[^4]: Идентификаторы, начинающиеся с двух подчеркиваний, описаны в разделе 2.1. *Прим. пер.*
[^5]: В стандартной библиотеке (STL) C++ для определения завершения цикла последовательно используется оператор `!=` на том основании, что (не)равенство более общая форма сравнения, так как она применима к большему количеству типов. Подход D не менее общий, но при этом, когда это возможно, для повышения безопасности вычислений использует `<`, не проигрывая ни в обобщенности, ни в эффективности.
[^6]: LIFO акроним «Last In First Out» (последним пришел первым ушел). *Прим. пер.*
[^7]: CleanerUpper «уборщик» (от англ. clean up убирать, чистить). *Прим. пер.*