This commit is contained in:
Alexander Zhirov 2023-01-24 10:25:47 +03:00
parent 235bd723c2
commit c0dec81098
1 changed files with 136 additions and 5 deletions

View File

@ -1,11 +1,11 @@
# 3. Инструкции
- [3.1. Инструкция-выражение](#3-1-инструкция-выражение)
- [3.2. Составная инструкция]()
- [3.3. Инструкция if]()
- [3.4. Инструкция static if]()
- [3.5. Инструкция switch]()
- [3.6. Инструкция final switch]()
- [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 (цикл с постусловием)]()
@ -97,6 +97,8 @@ void main()
Такой подход объясняется просто. Возможность перекрывать глобальные идентификаторы необходима, чтобы писать качественный модульный код, который собирается из нескольких отдельно скомпилированных частей; вы же не хотите, чтобы добавленная в локальное пространство имен глобальная переменная внезапно спутала все карты, запретив компиляцию невинных локальных переменных. С другой стороны, перекрытие локальных идентификаторов бесполезно с точки зрения модульности (поскольку в D составная инструкция никогда не простирается на несколько модулей) и обычно указывает либо на недосмотр (который вот-вот превратится в ошибку), либо на злокачественную функцию, вышедшую из-под контроля.
[В начало ⮍](#3-2-составная-инструкция) [Наверх ⮍](#3-инструкции)
## 3.3. Инструкция if
Во многих примерах уже встречалась условная инструкция D `if`, которая очень похожа на то, чего вы могли от нее ожидать:
@ -170,6 +172,8 @@ else
}
```
[В начало ⮍](#3-3-инструкция-if) [Наверх ⮍](#3-инструкции)
## 3.4. Инструкция static if
Теперь, когда вы уже разогрелись на нескольких простых инструкциях (спасибо, что подавили этот зевок), можно взглянуть на нечто более необычное.
@ -257,5 +261,132 @@ if (a)
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`, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. *Прим. науч. ред.*