diff --git a/03-инструкция/README.md b/03-инструкция/README.md index 17138bd..73c6c46 100644 --- a/03-инструкция/README.md +++ b/03-инструкция/README.md @@ -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`, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. – *Прим. науч. ред.*