dlang-book/03-инструкция/README.md

393 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3. Инструкции
- [3.1. Инструкция-выражение](#3-1-инструкция-выражение)
- [3.2. Составная инструкция](#3-2-составная-инструкция)
- [3.3. Инструкция if](#3-3-инструкция-if)
- [3.4. Инструкция static if](#3-4-инструкция-static-if)
- [3.5. Инструкция switch](#3-5-инструкция-switch)
- [3.6. Инструкция final switch](#3-6-инструкция-final-switch)
- [3.7. Циклы]()
- [3.7.1. Инструкция while (цикл с предусловием)]()
- [3.7.2. Инструкция do-while (цикл с постусловием)]()
- [3.7.3. Инструкция for (цикл со счетчиком)]()
- [3.7.4. Инструкция foreach (цикл просмотра)]()
- [3.7.5. Цикл просмотра для работы с массивами]()
- [3.7.6. Инструкции continue и break]()
- [3.8. Инструкция goto (безусловный переход)]()
- [3.9. Инструкция with]()
- [3.10. Инструкция return]()
- [3.11. Обработка исключительных ситуаций]()
- [3.12. Инструкция mixin]()
- [3.13. Инструкция scope]()
- [3.14. Инструкция synchronized]()
- [3.15. Конструкция asm]()
- [3.16. Итоги и справочник]()
Эта глава содержит обязательные определения всех инструкций языка D. D наследует внешний вид и функциональность языков семейства C в нем есть привычные инструкции `if`, `while`, `for` и другие. Наряду с этим D предлагает ряд новых интересных инструкций и некоторое усовершенствование старых. Если неизбежное перечисление с подробным описанием каждой инструкции заранее нагоняет на вас скуку, то вот вам несколько «отступлений» любопытных отличий D от других языков.
Если вы желаете во время компиляции проверять какие-то условия, то вас, скорее всего, заинтересует инструкция `static if` (см. раздел 3.4). Ее возможности гораздо шире, чем просто настройка набора флагов; тем, кто каким-либо образом использует обобщенный код, `static if` принесет ощутимую пользу. Инструкция `switch` (см. раздел 3.5) выглядит и действует в основном так же, как и ее тезка из языка C, но оперирует также строками, позволяя одновременно сопоставлять целые диапазоны. Для корректной обработки небольших конечных множеств значений пригодится инструкция `final switch` (см. разд. 3.6), которая работает с перечисляемыми типами и заставит вас реализовать обработчик абсолютно для каждого возможного значения. Инструкция `foreach` (см. разделы 3.7.4 и 3.7.5) помогает организовывать последовательные итерации; классическая инструкция `for` предоставляет больше возможностей, но и более многословна. Инструкция `mixin` (см. раздел 3.12) вставляет заранее определенный шаблонный код. Инструкция `scope` (см. раздел 3.13) значительно облегчает написание корректного безотказного кода с правильной обработкой ошибок, заменяя сумбурную конструкцию `try`/`catch`/`finally`, которой иначе вам пришлось бы воспользоваться.
[В начало ⮍](#3-инструкции)
## 3.1. Инструкция-выражение
Как уже говорилось (см. раздел 1.2), достаточно в конце выражения поставить точку с запятой, чтобы получить инструкцию:
```d
a = b + c;
transmogrify(a + b);
```
При этом не любое выражение можно превратить в инструкцию. Если инструкция, которая должна получиться, не выполняет никакого действия, например:
```d
1 + 1 == 2; // Абсолютная истина
```
то компилятор диагностирует ошибку.
[В начало ⮍](#3-1-инструкция-выражение) [Наверх ⮍](#3-инструкции)
## 3.2. Составная инструкция
Составная инструкция это (возможно, пустая) последовательность инструкций, заключенных в фигурные скобки. Инструкции исполняются по порядку. Скобки ограничивают лексический контекст (пространство имен): идентификаторы, определенные внутри такого блока, не видны за его пределами.
Идентификатор, определенный внутри данного пространства имен, перекрывает одноименный идентификатор, определенный вне этого пространства:
```d
uint widgetCount;
...
void main()
{
writeln(widgetCount); // Выводит значение глобальной переменной
auto widgetCount = getWidgetCount();
writeln(widgetCount); // Выводит значение локальной переменной
}
```
При первом вызове функции `writeln` будет напечатано значение глобальной переменной `widgetCount`, при втором происходит обращение к локальной переменной `widgetCount`. Для доступа к глобальному идентификатору после того, как был определен перекрывающий его локальный идентификатор, служит точка, поставленная перед идентификатором (как уже говорилось в разделе 2.2.1), например `writeln(.widgetCount)`. Тем не менее запрещается определять идентификатор, если он перекрывает идентификатор, определенный в блоке верхнего уровня:
```d
void main()
{
auto widgetCount = getWidgetCount();
// Откроем вложенный блок
{
auto widgetCount = getWidgetCount(); // Ошибка!
}
}
```
Если идентификаторы не перекрываются, то один и тот же идентификатор можно использовать внутри разных составных инструкций:
```d
void main()
{
{
auto i = 0;
...
}
{
auto i = "eye"; // Без проблем
...
}
double i = 3.14; // Тоже без проблем
}
```
Такой подход объясняется просто. Возможность перекрывать глобальные идентификаторы необходима, чтобы писать качественный модульный код, который собирается из нескольких отдельно скомпилированных частей; вы же не хотите, чтобы добавленная в локальное пространство имен глобальная переменная внезапно спутала все карты, запретив компиляцию невинных локальных переменных. С другой стороны, перекрытие локальных идентификаторов бесполезно с точки зрения модульности (поскольку в D составная инструкция никогда не простирается на несколько модулей) и обычно указывает либо на недосмотр (который вот-вот превратится в ошибку), либо на злокачественную функцию, вышедшую из-под контроля.
[В начало ⮍](#3-2-составная-инструкция) [Наверх ⮍](#3-инструкции)
## 3.3. Инструкция if
Во многих примерах уже встречалась условная инструкция D `if`, которая очень похожа на то, чего вы могли от нее ожидать:
```d
if (выражение) инструкция1
```
или
```d
if (выражение) инструкция1 else инструкция2
```
Достойна внимания одна деталь относительно инструкций, которыми управляет `if`. В отличие от других языков, в D нет «пустой инструкции», то есть отдельно точка с запятой сама по себе *не является* инструкцией и порождает ошибку. Это правило автоматически ограждает программистов от ошибок вроде следующей:
```d
if (a == b);
writeln("a и b равны");
```
В коротком коде подобная глупость очевидна, и вы легко ее устраните, но все меняется, когда выражение длиннее, сама инструкция затерялась в дебрях кода, а на часах полвторого ночи. Если вы действительно хотите применить `if` к пустой инструкции, то можете использовать наиболее близкий аналог пустую составную инструкцию:
```d
if (a == b) {}
```
Это очень полезно, когда вы переделываете код, то и дело заключая фрагменты кода в комментарии и возвращая их обратно.
Часть условной инструкции с ключевым словом `else` всегда привязана к ближайшей части с ключевым словом `if`, так что отступы в следующем коде сделаны верно:
```d
if (a == b)
if (b == c)
writeln("Все равны");
else
writeln("a не равно b. Но так ли это?");
```
Вторая функция `writeln` вызывается, когда `a == b` и `b != c`, потому что часть `else` привязана к внутреннему (второму) условию `if`. Если вы, напротив, хотите связать `else` с первым `if`, «буферизуйте» второе выражение с `if` с помощью пары фигурных скобок:
```d
if ( a == b )
{
if ( b == с )
writeln("Все одинаковое");
}
else
writeln("a отличается от b. Или это не так?");
```
Каскадные множественные конструкции `if-else` задаются в проверенном временем стиле C:
```d
auto opt = getOption();
if (opt == "help")
{
...
}
else if (opt == "quiet")
{
...
}
else if (opt == "verbose")
{
...
}
else
{
stderr.writefln("Неизвестная опция '%s'", opt);
}
```
[В начало ⮍](#3-3-инструкция-if) [Наверх ⮍](#3-инструкции)
## 3.4. Инструкция static if
Теперь, когда вы уже разогрелись на нескольких простых инструкциях (спасибо, что подавили этот зевок), можно взглянуть на нечто более необычное.
Если вы хотите «закомментировать» (или оставить) какие-то инструкции в зависимости от проверяемого во время компиляции логического условия, то вам пригодится инструкция `static if`[^1]. Например[^2]:
```d
enum size_t
g_maxDataSize = 100_000_000,
g_maxMemory = 1_000_000_000;
...
double transmogrify(double x)
{
static if (g_maxMemory / 4 > g_maxDataSize)
{
alias double Numeric;
}
else
{
alias float Numeric;
}
Numeric[] y;
... // Сложные вычисления
return y[0];
}
```
Инструкция `static if` позволяет осуществлять выбор во время компиляции и очень похожа на директиву `#if` языка C. Встречая `static if`, компилятор вычисляет условие. Если оно ненулевое, компилируется соответствующий код; иначе компилируется код, соответствующий выражению `else` (если таковое присутствует). В рассмотренном примере `static if` используется для переключения между экономичным (в отношении памяти) режимом работы (благодаря применению типа `float`, занимающего меньше места) и более точным режимом (благодаря применению более точного типа `double`). В обобщенном коде можно встретить и более мощные и выразительные примеры использования инструкции `static if`.
Выражение, проверяемое в `static if`, это любое логическое выражение, которое можно вычислить во время компиляции. К разрешенным выражениям относится большое подмножество выражений языка, включая арифметические операции со значениями любых числовых типов, манипуляции с массивами, выражения `is` с типами в качестве аргументов (см. раздел 2.3.4.3) и даже вызовы функций (вычисление функций во время компиляции действительно выдающееся средство). Вычисления во время компиляции подробно описаны в главе 5.
**Срезание скобок**
В примере с функцией `transmogrify` хорошо заметна одна странная особенность, а именно: тип `Numeric` определен внутри пары скобок `{` и `}`. Из-за этого он должен быть виден только локально, внутри пространства имен, созданного при помощи этих скобок (и, следовательно, недоступен внутри включающей этот блок функции), что подрывает наш план на корню. Такое поведение также показало бы, насколько практически бесполезна многообещающая инструкция `static if`. Поэтому `static if` использует скобки для *группирования*, а не для *управления пространствами имен*. Там, где в фокус внимания попадают пространства имен и области видимости, `static if` срезает внешние скобки, если они есть (их необязательно ставить, если с помощью условной инструкции контролируется только одна инструкция; в нашем примере выше они используются только для того, чтобы не нарушить обязательство насчет стиля). Если вы действительно хотите поставить скобки (в их традиционном значении), просто добавьте еще одну пару:
```d
import std.stdio;
void main()
{
static if (real.sizeof > double.sizeof)
{{
auto maximorum = real.max;
writefln("Действительно большие числа - до %s!", maximorum);
}}
... /* maximorum здесь не виден */ ...
}
```
**Не только инструкция**
Эта глава называется «Инструкции», а раздел «Инструкция `static if`». Поэтому вы вправе немного удивиться, узнав, что `static if` это не только инструкция, но и *объявление* (*declaration*). О «неинструкционности» `static if` свидетельствует не только срезание скобок, но и то, что `static` if может располагаться везде, где может быть расположено объявление, в том числе на недоступных инструкциям уровнях модулей, структур и классов. Например, мы можем определить `Numeric` глобально, просто вынеся соответствующий код за пределы функции `transmogrify`:
```d
enum size_t
g_maxDataSize = 100_000_000,
g_maxMemory = 1_000_000_000;
...
// Объявление Numeric будет видно в контексте модуля
static if (g_maxMemory / 4 > g_maxDataSize)
{
alias double Numeric;
}
else
{
alias float Numeric;
}
double transmogrify(double x)
{
Numeric[] y;
... // Сложные вычисления
return y[0];
}
```
**На два вида if один else**
У `static if` нет пары в виде `static else`. Вместо этого просто используется обычное ключевое слово `else`. В соответствии с логикой `else` привязывается к ближайшему `if` независимо от того, `static if` это или просто
`if`:
```d
if (a)
static if (b) writeln("a и b ненулевые");
else writeln("b равно нулю");
```
[В начало ⮍](#3-4-инструкция-static-if) [Наверх ⮍](#3-инструкции)
## 3.5. Инструкция switch
Лучше всего сразу проиллюстрировать работу инструкции `switch` примером:
```d
import std.stdio;
void classify(char c)
{
write("Вы передали ");
switch (c)
{
case '#':
writeln("знак решетки.");
break;
case '0': .. case '9':
writeln("цифру.");
break;
case 'A': .. case 'Z': case 'a': .. case 'z':
writeln("ASCII-знак.");
break;
case '.', ',', ':', ';', '!', '?':
writeln("знак препинания.");
break;
default:
writeln("всем знакам знак!");
break;
}
}
```
В общем виде инструкция `switch` выглядит так:
```d
switch (выражение) инструкция
```
`‹выражение›` может иметь числовой, перечисляемый или строковый тип; `‹инструкция›` может содержать метки (ярлыки, labels), определенные следующим образом:
1. `case ‹в›`: Перейти сюда, если `‹выражение› == ‹в›`. Чтобы можно было использовать внутри `в` запятые (см. раздел 2.3.18), все выражение требуется заключить в круглые скобки.
2. `case в1, в2, … , вn`: Каждая запись вида вk обозначает выражение. Рассматриваемая инструкция эквивалентна инструкции `case элемент1: case элемент2:, ... , case элементn:`.
3. `case в1: .. case в2`: Перейти сюда, если `‹выражение› >= в1` и `‹выражение› <= в2`.
4. `default`: Перейти сюда, если никакой другой переход невозможен.
`‹выражение›` вычисляется один раз для всех этих проверок. Выражение в каждой метке `case` это любое не противоречащее правилам языка выражение, которое можно проверить на равенство выражению `‹выражение›`, а также на неравенство в случае использования синтаксиса с интервалом. Обычно `case`-выражения представлены константами, вычисляемыми во время компиляции, но D разрешает использовать и переменные, гарантируя, что вычисления будут производиться в порядке следования альтернатив до первого совпадения. По завершении вычислений выполняется переход к соответствующей метке `case` или `default` и выполнение программы продолжается из этой точки. Для того чтобы покинуть ветвление, используется инструкция break, осуществляющая выход из инструкции `switch`. В отличие от языков C и C++, D запрещает неявный переход к следующей метке и требует инструкции `break` или `return` после кода, соответствующего метке.
```d
switch (s)
{
case 'a': writeln("a"); // Вывести "a" и перейти к следующей метке
case 'b': writeln("b"); // Ошибка! Неявный переход запрещен!
default: break;
}
```
Если вы действительно хотите, чтобы после кода метки `'a'` выполнился код метки `'b'`, вам придется явно указать это компилятору с помощью особой формы инструкции `goto`:
```d
switch (s)
{
case 'a': writeln("a"); goto case; // Вывести "a" и перейти к следующей метке
case 'b': writeln("b"); // После выполнения 'a' мы попадем сюда
default: break;
}
```
Если же вы случайно забыли написать `break` или `return`, компилятор любезно напомнит вам об этом. Можно было бы вообще отказаться от использования инструкции `break` в конструкции `switch`, но это нарушило бы обязательство компилировать C-подобный код по правилам языка C либо не компилировать его вообще.
Для меток, вычисляемых во время компиляции, действует запрет: вычисленные значения не должны пересекаться. Пример некорректного кода:
```d
switch (s)
{
case 'a' .. case 'z': ... break;
// Попытка задать особую обработку для 'w'
case 'w': ... break; // Ошибка! Case-метки не могут пересекаться!
default: break;
}
```
Метка `default` должна быть обязательно объявлена. Если она не объявлена, компилятор сообщит об ошибке. Это сделано для того, чтобы предотвратить типичную для программистов ошибку пропуск некоторого подмножества значений по недосмотру. Если такой опасности не существует, используйте `default: break;`, таким образом, аккуратно оформив ваше предположение. В следующем разделе описано, как статически гарантировать обработку всех возможных значений `switch`-условия.
[В начало ⮍](#3-5-инструкция-switch) [Наверх ⮍](#3-инструкции)
## 3.6. Инструкция final switch
Инструкция `switch` обычно используется в связке с перечисляемым типом для обработки каждого из всех его возможных значений. Если во время эксплуатации число вариантов меняется, все зависимые переключатели неожиданно перестают соответствовать новому положению дел; каждую такую инструкцию необходимо вручную найти и изменить.
Теперь очевидно, что для получения масштабируемого решения следует заменить «переключение» на основе меток виртуальными функциями; в этом случае нет необходимости обрабатывать различные случаи в одном месте, но вместо этого обработка распределяется по разным реализациям интерфейса. Но в жизни не бывает все идеально: определение интерфейсов и классов требует серьезных усилий на начальном этапе работы над программой, чего можно избежать, остановившись на альтернативном решении с переключателем `switch`. В таких ситуациях может пригодиться инструкция `final switch`, статически «принуждающая» метки `case` покрывать все возможные значения перечисляемого типа:
```d
enum DeviceStatus { ready, busy, fail }
...
void process(DeviceStatus status)
{
final switch (status)
{
case DeviceStatus.ready:
...
case DeviceStatus.busy:
...
case DeviceStatus.fail:
...
}
}
```
Предположим, что при эксплуатации кода было добавлено еще одно возможное состояние устройства:
```d
enum DeviceStatus { ready, busy, fail, initializing /* добавлено */ }
```
После этого изменения попытка перекомпилировать функцию `process` будет встречена отказом на следующем основании:
```sh
Error: final switch statement must handle all values
```
*(Ошибка: инструкция final switch должна обрабатывать все значения)*
Инструкция `final switch` требует, чтобы все значения типа `enum` были явно обработаны. Метки с интервалами вида `case в1: .. case в2:`, а также метку `default`: использовать запрещено.
[В начало ⮍](#3-6-инструкция-final-switch) [Наверх ⮍](#3-инструкции)
[^1]: Да-да, это «еще одно место, где используется ключевое слово `static»`.
[^2]: Тип `enum` будет рассмотрен позже. Для понимания примера надо знать, что значения объявленные как `enum`, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. *Прим. науч. ред.*