dlang-book/03-инструкции
Alexander Zhirov ec805e7f08 4 глава 2023-02-05 19:45:16 +03:00
..
src 4 глава 2023-02-05 19:45:16 +03:00
README.md 4 глава 2023-02-05 19:45:16 +03:00

README.md

3. Инструкции

Эта глава содержит обязательные определения всех инструкций языка 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.1. Инструкция-выражение

Как уже говорилось (см. раздел 1.2), достаточно в конце выражения поставить точку с запятой, чтобы получить инструкцию:

a = b + c;
transmogrify(a + b);

При этом не любое выражение можно превратить в инструкцию. Если инструкция, которая должна получиться, не выполняет никакого действия, например:

1 + 1 == 2; // Абсолютная истина

то компилятор диагностирует ошибку.

В начало ⮍ Наверх ⮍

3.2. Составная инструкция

Составная инструкция это (возможно, пустая) последовательность инструкций, заключенных в фигурные скобки. Инструкции исполняются по порядку. Скобки ограничивают лексический контекст (пространство имен): идентификаторы, определенные внутри такого блока, не видны за его пределами.

Идентификатор, определенный внутри данного пространства имен, перекрывает одноименный идентификатор, определенный вне этого пространства:

uint widgetCount;
...
void main()
{
    writeln(widgetCount); // Выводит значение глобальной переменной
    auto widgetCount = getWidgetCount();
    writeln(widgetCount); // Выводит значение локальной переменной
}

При первом вызове функции writeln будет напечатано значение глобальной переменной widgetCount, при втором происходит обращение к локальной переменной widgetCount. Для доступа к глобальному идентификатору после того, как был определен перекрывающий его локальный идентификатор, служит точка, поставленная перед идентификатором (как уже говорилось в разделе 2.2.1), например writeln(.widgetCount). Тем не менее запрещается определять идентификатор, если он перекрывает идентификатор, определенный в блоке верхнего уровня:

void main()
{
    auto widgetCount = getWidgetCount();
    // Откроем вложенный блок
    {
        auto widgetCount = getWidgetCount(); // Ошибка!
    }
}

Если идентификаторы не перекрываются, то один и тот же идентификатор можно использовать внутри разных составных инструкций:

void main()
{
    {
        auto i = 0;
        ...
    }
    {
        auto i = "eye"; // Без проблем
    ...
    }
    double i = 3.14; // Тоже без проблем
}

Такой подход объясняется просто. Возможность перекрывать глобальные идентификаторы необходима, чтобы писать качественный модульный код, который собирается из нескольких отдельно скомпилированных частей; вы же не хотите, чтобы добавленная в локальное пространство имен глобальная переменная внезапно спутала все карты, запретив компиляцию невинных локальных переменных. С другой стороны, перекрытие локальных идентификаторов бесполезно с точки зрения модульности (поскольку в D составная инструкция никогда не простирается на несколько модулей) и обычно указывает либо на недосмотр (который вот-вот превратится в ошибку), либо на злокачественную функцию, вышедшую из-под контроля.

В начало ⮍ Наверх ⮍

3.3. Инструкция if

Во многих примерах уже встречалась условная инструкция D if, которая очень похожа на то, чего вы могли от нее ожидать:

if (выражение) инструкция1

или

if (выражение) инструкция1 else инструкция2

Достойна внимания одна деталь относительно инструкций, которыми управляет if. В отличие от других языков, в D нет «пустой инструкции», то есть отдельно точка с запятой сама по себе не является инструкцией и порождает ошибку. Это правило автоматически ограждает программистов от ошибок вроде следующей:

if (a == b);
    writeln("a и b равны");

В коротком коде подобная глупость очевидна, и вы легко ее устраните, но все меняется, когда выражение длиннее, сама инструкция затерялась в дебрях кода, а на часах полвторого ночи. Если вы действительно хотите применить if к пустой инструкции, то можете использовать наиболее близкий аналог пустую составную инструкцию:

if (a == b) {}

Это очень полезно, когда вы переделываете код, то и дело заключая фрагменты кода в комментарии и возвращая их обратно.

Часть условной инструкции с ключевым словом else всегда привязана к ближайшей части с ключевым словом if, так что отступы в следующем коде сделаны верно:

if (a == b)
    if (b == c)
        writeln("Все равны");
    else
        writeln("a не равно b. Но так ли это?");

Вторая функция writeln вызывается, когда a == b и b != c, потому что часть else привязана к внутреннему (второму) условию if. Если вы, напротив, хотите связать else с первым if, «буферизуйте» второе выражение с if с помощью пары фигурных скобок:

if ( a == b )
{
    if ( b == с )
        writeln("Все одинаковое");
}
else
    writeln("a отличается от b. Или это не так?");

Каскадные множественные конструкции if-else задаются в проверенном временем стиле C:

auto opt = getOption();
if (opt == "help")
{
    ...
}
else if (opt == "quiet")
{
    ...
}
else if (opt == "verbose")
{
    ...
}
else
{
    stderr.writefln("Неизвестная опция '%s'", opt);
}

В начало ⮍ Наверх ⮍

3.4. Инструкция static if

Теперь, когда вы уже разогрелись на нескольких простых инструкциях (спасибо, что подавили этот зевок), можно взглянуть на нечто более необычное.

Если вы хотите «закомментировать» (или оставить) какие-то инструкции в зависимости от проверяемого во время компиляции логического условия, то вам пригодится инструкция static if1. Например2:

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 срезает внешние скобки, если они есть (их необязательно ставить, если с помощью условной инструкции контролируется только одна инструкция; в нашем примере выше они используются только для того, чтобы не нарушить обязательство насчет стиля). Если вы действительно хотите поставить скобки (в их традиционном значении), просто добавьте еще одну пару:

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:

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:

if (a)
    static if (b) writeln("a и b ненулевые");
    else writeln("b равно нулю");

В начало ⮍ Наверх ⮍

3.5. Инструкция switch

Лучше всего сразу проиллюстрировать работу инструкции switch примером:

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 выглядит так:

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 после кода, соответствующего метке.

switch (s)
{
    case 'a': writeln("a"); // Вывести "a" и перейти к следующей метке
    case 'b': writeln("b"); // Ошибка! Неявный переход запрещен!
    default: break;
}

Если вы действительно хотите, чтобы после кода метки 'a' выполнился код метки 'b', вам придется явно указать это компилятору с помощью особой формы инструкции goto:

switch (s)
{
    case 'a': writeln("a"); goto case; // Вывести "a" и перейти к следующей метке
    case 'b': writeln("b");            // После выполнения 'a' мы попадем сюда
    default: break;
}

Если же вы случайно забыли написать break или return, компилятор любезно напомнит вам об этом. Можно было бы вообще отказаться от использования инструкции break в конструкции switch, но это нарушило бы обязательство компилировать C-подобный код по правилам языка C либо не компилировать его вообще.

Для меток, вычисляемых во время компиляции, действует запрет: вычисленные значения не должны пересекаться. Пример некорректного кода:

switch (s)
{
    case 'a' .. case 'z': ... break;
    // Попытка задать особую обработку для 'w'
    case 'w': ... break; // Ошибка! Case-метки не могут пересекаться!
    default: break;
}

Метка default должна быть обязательно объявлена. Если она не объявлена, компилятор сообщит об ошибке. Это сделано для того, чтобы предотвратить типичную для программистов ошибку пропуск некоторого подмножества значений по недосмотру. Если такой опасности не существует, используйте default: break;, таким образом, аккуратно оформив ваше предположение. В следующем разделе описано, как статически гарантировать обработку всех возможных значений switch-условия.

В начало ⮍ Наверх ⮍

3.6. Инструкция final switch

Инструкция switch обычно используется в связке с перечисляемым типом для обработки каждого из всех его возможных значений. Если во время эксплуатации число вариантов меняется, все зависимые переключатели неожиданно перестают соответствовать новому положению дел; каждую такую инструкцию необходимо вручную найти и изменить.

Теперь очевидно, что для получения масштабируемого решения следует заменить «переключение» на основе меток виртуальными функциями; в этом случае нет необходимости обрабатывать различные случаи в одном месте, но вместо этого обработка распределяется по разным реализациям интерфейса. Но в жизни не бывает все идеально: определение интерфейсов и классов требует серьезных усилий на начальном этапе работы над программой, чего можно избежать, остановившись на альтернативном решении с переключателем switch. В таких ситуациях может пригодиться инструкция final switch, статически «принуждающая» метки case покрывать все возможные значения перечисляемого типа:

enum DeviceStatus { ready, busy, fail }
...
void process(DeviceStatus status)
{
    final switch (status)
    {
        case DeviceStatus.ready:
            ...
        case DeviceStatus.busy:
            ...
        case DeviceStatus.fail:
            ...
    }
}

Предположим, что при эксплуатации кода было добавлено еще одно возможное состояние устройства:

enum DeviceStatus { ready, busy, fail, initializing /* добавлено */ }

После этого изменения попытка перекомпилировать функцию process будет встречена отказом на следующем основании:

Error: final switch statement must handle all values

(Ошибка: инструкция final switch должна обрабатывать все значения)

Инструкция final switch требует, чтобы все значения типа enum были явно обработаны. Метки с интервалами вида case в1: .. case в2:, а также метку default: использовать запрещено.

Исходный код

В начало ⮍ Наверх ⮍

3.7. Циклы

3.7.1. Инструкция while (цикл с предусловием)

Да, именно так:

while (выражение) инструкция

Сначала вычисляется ‹выражение›. Если оно ненулевое, выполняется ‹инструкция› и цикл возобновляется: снова вычисляется ‹выражение› и т. д. Иначе управление передается инструкции, расположенной сразу после цикла while.

В начало ⮍ Наверх ⮍

3.7.2. Инструкция do-while (цикл с постусловием)

Если нужен цикл, который обязательно выполнится хотя бы раз, подойдет цикл с постусловием:

do инструкция while (выражение);

Обратите внимание на обязательную точку с запятой в конце инструкции. Кроме того, после do должна быть хотя бы одна ‹инструкция›. Цикл с постусловием эквивалентен циклу с предусловием, в котором сначала один раз выполняется ‹инструкция›.

В начало ⮍ Наверх ⮍

3.7.3. Инструкция for (цикл со счетчиком)

Синтаксис цикла со счетчиком:

for (определение счетчика; выр1; выр2) инструкция

Любое из выражений ‹определение счетчика›, выр1 и выр2 (или все сразу) можно опустить. Если нет выражения выр1, считается, что оно истинно. Выражение ‹определение счетчика› это или объявление значения (например, auto i = 0; или float w;), или выражение с точкой с запятой в конце (например, i = 10;). По семантике использования цикл со счетчиком идентичен одноименным инструкциям из других языков: сначала вычисляется ‹определение счетчика›, затем выр1; если оно истинно, выполняется ‹инструкция›, потом вычисляется выр2, после чего выполнение цикла продолжается новым вычислением выр1.

В начало ⮍ Наверх ⮍

3.7.4. Инструкция foreach (цикл просмотра)

Самое удобное, безопасное и зачастую быстрое средство просмотра значений в цикле инструкция foreach3, у которой есть несколько вариантов. Простейший вариант цикла просмотра:

foreach (идентификатор; выражение1 .. выражение2) инструкция

выражение1 и выражение2 могут быть числами или указателями. Попросту говоря, ‹идентификатор› проходит интервал от (включая) выражения1 до (не включая) выражения2. Ради понятности этого неформального определения в нем не освещены некоторые детали. Например, сколько раз вычисляется выражение2 в процессе выполнения цикла один или несколько? Или что происходит, если выражение1 >= выражение2? Все это легко узнать, взглянув на семантически эквивалентный код, приведенный ниже. Техника представления высокоуровневых конструкций в терминах эквивалентных конструкций более простого (под)языка (в виде абстракций более низкого уровня) называется снижением (lowering). Она будет широко использоваться на протяжении всей этой главы.

{
    auto __n = выражение2;
    auto идентификатор = true ? выражение1 : выражение2;
    for (; идентификатор < __n; ++идентификатор ) инструкция
}

Здесь идентификатор __n генерируется компилятором, что гарантирует отсутствие конфликтов с другими идентификаторами4 («свежее слово» в лексиконе тех, кто пишет компиляторы).

(Зачем нужны внешние фигурные скобки? Они гарантируют, что ‹идентификатор› не «просочится» за пределы цикла foreach, а также благодаря им вся эта конструкция это одна инструкция.)

Теперь ясно, что и выражение1, и выражение2 вычисляются всего один раз, а тип значения ‹идентификатор› определяется по правилам для тернарной условной операции (см. раздел 2.3.16) вот в чем роль знаков ?:, никак не проявляющих себя при исполнении программы. Осторожное «примирение» типов, достигнутое благодаря знакам ?:, гарантирует предотвращение или, по крайней мере, выявление потенциальной неразберихи с числами разного размера и точности, а также конфликтов между типами со знаком и без знака.

Заметим, что компилятор принудительно не назначает __n какой-либо особый тип, то есть вы можете использовать этот вариант цикла foreach с пользовательскими типами, для которых определены оператор сравнения «меньше» (<) и оператор увеличения на единицу (мы научимся делать это в главе 12). Кроме того, если для типа не определен оператор <, но определен оператор сравнения на равенство, компилятор автоматически заменит оператор < оператором != при снижении. В этом случае не может быть проверена корректность задания интервала, поэтому вы должны удостовериться, что верхняя граница может быть достигнута с помощью повторного применения оператора ++, начиная с нижней границы. Иначе результат будет непредсказуемым.5

Заметим, что вы можете определить нужный тип счетчика внутри части ‹идентификатор› определения цикла. Обычно такое объявление излишне, но полезно, если вы хотите, чтобы тип счетчика удовлетворял ряду особых условий, исключал путаницу в знаковости/беззнаковости или при необходимости использования неявного преобразования типов:

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.5. Цикл просмотра для работы с массивами

Перейдем к другому варианту инструкции foreach, предназначенному для работы с массивами и срезами:

foreach (идентификатор; выражение) инструкция

‹выражение› должно быть массивом (линейным или ассоциативным), срезом или иметь пользовательский тип. Последний случай мы рассмотрим в главе 12, а сейчас сосредоточимся на массивах и срезах. После того как ‹выражение› было один раз вычислено, ссылка на него сохраняется в закрытой временной переменной. (Сам массив не копируется.) Затем переменной с именем ‹идентификатор› по очереди присваивается каждый из элементов массива и выполняется ‹инструкция›. Так же как и в случае с циклом просмотра с интервалами, допускается указание типа перед ‹идентификатором›.

Инструкция foreach предполагает, что во время итераций длина массива изменяться не будет; если вы задумали иное, возможно, вам стоит задействовать простой цикл просмотра и побольше внимания.

Обновление во время итерации

Присваивание переменной ‹идентификатор› внутри ‹инструкции› не отражается на состоянии массива. Если вы действительно хотите изменить элемент, рассматриваемый в текущей итерации, определите ‹идентификатор› как ссылку, расположив перед ним ref или ref ‹тип›. Например:

void scale(float[] array, float s)
{
    foreach (ref e; array)
    {
        e *= s; // Обновляет массив "на месте"
    }
}

В приведенный код можно после ref добавить полное определение переменной e (включая ее тип), например ref float e. Однако на этот раз соответствие должно быть точным: ref запрещает преобразования типов!

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 по нескольким причинам, самая очевидная из которых разный размер этих типов.

Где я?

Иногда полезно иметь доступ к индексу итерации. Следующий вариант цикла просмотра позволяет привязать идентификатор к этому значению:

foreach (идентификатор1, идентификатор2; выражение) инструкция

Так что можно написать:

void print(int[] array)
{
    foreach (i, e; array)
    {
        writefln("array[%s] = %s;", i, e);
    }
}

Эта функция печатает содержание массива в виде, соответствующем коду на D. При выполнении print([5, 2, 8]) выводится:

array[0] = 5;
array[1] = 2;
array[2] = 8;

Гораздо интереснее наблюдать за обращением к индексу элемента при работе с ассоциативными массивами:

void print(double[string] map)
{
    foreach (i, e; map)
    {
        writefln("array['%s'] = %s;", i, e);
    }
}

Теперь print(["Луна": 1.283, "Солнце": 499.307, "Проксима Центавра": 133814298.759]) выведет

array['Проксима Центавра'] = 1.33814e+08;
array['Солнце'] = 499.307;
array['Луна'] = 1.283;

Обратите внимание: элементы напечатаны не в том порядке, в каком они заданы в литерале. Интересно, что экспериментируя с тем же кодом, но в разных реализациях языка или разных версиях одной и той же реализации, можно наблюдать изменение порядка. Дело в том, что в ассоциативных массивах применяются таинственные методики, повышающие эффективность хранения и выборки элементов за счет отказа от гарантированного упорядочивания.

Тип индекса и самого элемента определяются по контексту. Можно действовать и по-другому, «навязывая» нужные типы одной из переменных идентификатор1 и идентификатор2 или обеим сразу. Однако помните, что идентификатор1 не может быть ссылкой (перед ним нельзя поставить ключевое слово ref).

Проделки

Во время итерации можно по-разному изменять просматриваемый массив:

  • Изменение массива «на месте». Во время итерации будут «видны» изменения еще не посещенных ячеек массива.
  • Изменение размера массива. Цикл повторяется, пока не будет просмотрено столько элементов массива, сколько в нем было до входа в цикл. Возможно, в результате изменения размера массив будет перемещен в другую область памяти; в этом случае последующее изменение массива не видно во время итерации, а также последующие изменения, вызванные во время самой итерации, не отразятся на массиве. Использовать такую технику не рекомендуется, поскольку правила перемещения массива в памяти зависят от реализации.
  • Освобождение выделенной под массив памяти (полное или частичное; в последнем случае говорят о «сжатии» массива) с помощью низкоуровневых функций управления памятью. Желая получить полный контроль и достичь максимальной эффективности, вы не пожалели времени на изучение низкоуровневого управления памятью по документации к своей реализации языка. Все, что можно предположить: 1) вы знаете, что творите, и 2) не скучно с вами лишь тому, кто написал собственный сборщик мусора.

Исходный код

В начало ⮍ Наверх ⮍

3.7.6. Инструкции continue и break

Инструкция continue выполняет переход к началу новой итерации цикла, определяемого ближайшей к ней инструкцией while, do-while, for или foreach. Инструкции, расположенные между continue и концом тела цикла не выполняются.

Инструкция break выполняет переход к коду, расположенному сразу после ближайшей к ней инструкции while, do-while, for, foreach, switch или final switch, мгновенно завершая ее выполнение.

Обе инструкции можно использовать с необязательной меткой, указывающей, к какой именно инструкции они относятся. «Пометка» инструкций continue и break значительно упрощает построение сложных вариантов итераций, позволяя обойтись без переменных состояния и инструкции goto, описанной в следующем разделе.

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.8. Инструкция goto (безусловный переход)

В связи с глобальным потеплением не будем горячиться по поводу инструкции goto. Достаточно сказать, что в D она имеет следующий синтаксис:

goto метка;

Идентификатор ‹метка› должен быть виден внутри функции, где вызывается goto. Метка определяется явно как идентификатор с двоеточием, расположенный перед инструкцией. Например:

int a;
...
mylabel: a = 1;
...
if (a == 0) goto mylabel;

Нельзя переопределять метки внутри одной и той же функции. Другое ограничение состоит в том, что goto не может «перепрыгнуть» точку определения значения, видимого в точке «приземления». Например:

void main()
{
    goto target;
    int x = 10;
    target: {} // Ошибка! goto заставляет пропустить определение x!
}

Наконец, инструкция goto не может выполнить переход за границу исключения (см. раздел 3.11). Таким образом, у инструкции goto почти нет ограничений, и именно это делает ее опасной. С помощью goto можно перейти куда угодно: вперед или назад, внутрь или за пределы инструкций if, внутрь и за пределы циклов, включая пресловутый переход прямо в середину тела цикла.

Тем не менее в D опасно не все, чего коснется goto. Если написать внутри конструкции switch инструкцию

goto case выражение;

то будет выполнен переход к соответствующей метке case ‹выражение›. Инструкция

goto case;

выполняет переход к следующей метке case. Инструкция

goto default;

выполняет переход к метке default. Несмотря на то что эти переходы ничуть не более структурированы, чем любые другие случаи использования goto, их легче отслеживать, поскольку они расположены в одном месте программы и значительно упрощают структуру инструкции switch:

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.9. Инструкция with

Созданная по примеру Паскаля инструкция with облегчает работу с конкретным объектом.

Синтаксис:

with (выражение) инструкция

сначала вычисляется ‹выражение›, после чего внутренние элементы верхнего уровня вложенности полученного объекта делаются видимыми внутри ‹инструкции›. Мы уже встречались со структурами в главе 1, поэтому рассмотрим пример с применением типа struct:

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 следующий код не скомпилируется:

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, осуществляется с помощью синтаксиса .идентификатор.

Заметим, что можно сделать неявной множественную вложенность объектов, написав:

with (выр1) with (выр2) ... with (вырn) инструкция

При использовании вложенных инструкций with нет угрозы неопределенности, так как язык запрещает во внутренней инструкции with перекрывать идентификатор, определенный во внешней инструкции with. В двух словах: в D локальному идентификатору запрещено перекрывать другой локальный идентификатор.

Исходный код

В начало ⮍ Наверх ⮍

3.10. Инструкция return

Чтобы немедленно вернуть значение из текущей функции, напишите

return выражение;

Эта инструкция вычисляет ‹выражение› и возвращает полученное значение в точку вызова функции, предварительно неявно преобразовав его (если требуется) к типу, возвращаемому этой функцией.

Если текущая функция имеет тип void, ‹выражение› должно быть опущено или представлять собой вызов функции, которая в свою очередь имеет тип void.

Выход из функции, возвращающей не void, должен осуществляться посредством инструкции return. Во время компиляции это трудно эффективно отследить, так что, возможно, иногда вы будете получать от компилятора необоснованные претензии.

В начало ⮍ Наверх ⮍

3.11. Обработка исключительных ситуаций

Язык программирования D поддерживает обработку ошибок с помощью механизма исключительных ситуаций, или исключений (exceptions). Исключение инициируется инструкцией throw, а обрабатывается инструкцией try. Чтобы породить исключение, обычно пишут:

throw new SomeException("Произошло нечто подозрительное");

Тип SomeException должен наследовать от встроенного класса Throwable. D не поддерживает создание исключительных ситуаций произвольных типов, отчасти потому, что, как мы скоро увидим, назначение определенного класса корневым облегчает обработку цепочек исключений разных типов.

Чтобы обработать исключение или просто быть в курсе, что оно произошло, используйте инструкцию try, которая в общем виде выглядит так:

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.12. Инструкция mixin

Благодаря главе 2 (см. раздел 2.3.4.2) мы узнали, что с помощью выражений mixin можно преобразовывать строки, известные во время компиляции, в выражения на D, которые компилируются как обычный код. Инструкции с mixin предоставляют еще больше возможностей, позволяя создавать с помощью mixin не только выражения, но также объявления и инструкции.

Предположим, вы хотите как можно быстрее выяснить число ненулевых разрядов в байте. Это число, называемое весом Хемминга, используется в решении множества прикладных задач, таких как шифрование, распределенные вычисления и приближенный поиск в базе данных. Простейший способ подсчета ненулевых битов в байте: последовательно суммировать значения младшего бита, сдвигая каждый раз введенное число на один разряд вправо. Более быстрый метод был впервые предложен Питером Вегнером и популяризирован Керниганом и Ричи в их классическом труде:

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 скомпилировать эту строку в обычный код. Генератор таблицы может выглядеть так:

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). Теперь, когда у нас есть нужный код в виде строки, для определения таблицы достаточно выполнить всего одно действие:

mixin(makeHammingWeightsTable("hwTable"));

unittest
{
    assert(hwTable[10] == 2);
    assert(hwTable[0] == 0);
    assert(hwTable[255] == 8);
}

Теоретически можно строить таблицы любого размера, но полученную программу всегда рекомендуется тестировать: из-за кэширования работа со слишком большими таблицами может на самом деле выполняться медленнее вычислений.

В качестве последнего средства (как тренер по айкидо скрепя сердце рекомендует ученикам газовый баллончик) стоит упомянуть сочетание импорта строки (инструкция import, см. раздел 2.2.5.1) и объявлений, созданных с помощью mixin, которое позволяет реализовать самую примитивную форму модульности текстовое включение. Полюбуйтесь:

mixin(import("widget.d"));

Выражение import считывает текст файла widget.d в строковый литерал, который выражение mixin тут же преобразует в код. Используйте этот трюк, только если действительно считаете, что без него ваша честь хакера поставлена на карту.

Исходный код

В начало ⮍ Наверх ⮍

3.13. Инструкция scope

Инструкция scope нововведение D, хотя и другие языки в той или иной форме реализуют подобную функциональность. Инструкция scope позволяет легко писать на D корректно работающий код и, главное, без проблем читать и понимать его впоследствии. Можно и другими средствами достичь свойственной коду со scope корректности, однако, за исключением самых заурядных примеров, результат окажется непостижимым.

Синтаксис:

scope(exit) инструкция

‹инструкция› принудительно выполняется после того, как поток управления покинет текущую область видимости (контекст). Результат будет таким же, что и при использовании компонента finally инструкции try, но в общем случае инструкция scope более масштабируема. С помощью scope(exit) удобно гарантировать, что, оставляя контекст, вы «навели порядок». Допустим, в вашем приложении используется флаг g_verbose («говорливый»), который вы хотите временно отключить. Тогда можно написать:

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):

{
    инструкции1
    scope(exit) инструкция2
    инструкции3
}

Пусть явно отображенный вызов scope первый в этом блоке, то есть инструкции1 не содержат вызовов scope (но инструкции инструкция2 и инструкции3 могут его содержать). Применив технику снижения, преобразуем этот код в код следующего вида:

{
    инструкции1
    try
    {
        инструкции3
    }
    finally
    {
        инструкция2
    }
}

На этом преобразование не заканчивается. Инструкции инструкция2 и инструкции3 также подвергаются снижению, поскольку могут содержать дополнительные инструкции scope. (Процесс снижения всегда конечен, так как фрагменты всегда строго меньше исходной последовательности.) Это означает, что код, содержащий несколько инструкций scope(exit), вполне корректен, даже в таких странных случаях, как scope(exit) scope(exit) scope(exit) writeln("?"). Посмотрим, что происходит в любопытном случае, когда в одном и том же блоке встречаются две инструкции scope(exit):

{
    инструкции1
    scope(exit) инструкция2
    инструкции3
    scope(exit) инструкция4
    инструкции5
}

Предположим, что ни одна из инструкций не содержит ни одного дополнительного вызова инструкции scope. Воспользовавшись снижением, получим:

{
    инструкции1
    try
    {
        инструкции3
        try
        {
            инструкции5
        }
        finally
        {
            инструкция4
        }
    }
    finally
    {
        инструкция2
    }
}

Этот громоздкий код поможет нам выяснить порядок выполнения нескольких инструкций scope(exit) в одном блоке. Проследив порядок выполнения инструкций в полученном коде, можно сделать вывод, что инструкция инструкция4 выполняется до инструкции инструкция2. Обобщенно, инструкции scope(exit) выполняются по схеме LIFO6: в порядке, обратном их следованию в тексте программы.

Отслеживать порядок выполнения инструкций scope гораздо легче, чем порядок выполнения эквивалентного кода с конструкцией try/finally; элементарно: инструкция scope гарантирует, что управляемая ею инструкция будет выполнена при выходе из контекста. Это позволяет вам защитить свой код от ошибок без неудобной иерархии конструкций try/finally простым перечислением нужных действий в одной строке.

Предыдущий пример демонстрирует еще одно прекрасное свойство инструкции scope масштабируемость. С учетом огромной масштабируемости эта инструкция просто неотразима. (В конце концов, если бы требовалось лишь изредка выполнять одну-единственную инструкцию scope, можно было бы вручную написать ее сниженный эквивалент по указанной выше методике.) Функциональность нескольких инструкций scope(exit) требует увеличения длины кода программы при использовании самих инструкций scope(exit) и одновременного увеличения как длины, так и глубины кода при использовании эквивалентных инструкций try. Причем в глубину код масштабируется очень слабо, к тому же приходится делить «владения» с другими составными инструкциями, такими как if или for. Еще один подходящий вариант масштабируемого решения применение деструкторов в стиле C++ (также поддерживаемых D; см. главу 7), если только вам удастся снизить стоимость определения новых типов. Но если приходится определять класс только потому, что понадобился его деструктор (а зачем еще нужен класс типа CleanerUpper7?), то в плане масштабируемости это решение даже хуже вложенных инструкций try. Вкратце, если классы вакуумная сварка, а инструкции try жевательная резинка, то инструкция scope(exit) эпоксидный суперклей.

Инструкция scope(success) ‹инструкция› включает ‹инструкцию› в «график» программы только в случае обычного выхода из текущей области видимости (не в результате исключения). Выполним снижение для инструкции scope(success). Код

{
    инструкции1
    scope(success) инструкция2
    инструкции3
}

превращается в

{
    инструкции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. Код

{
    инструкции1
    scope(failure) инструкция2
    инструкции3
}

превращается в

{
    инструкции1
    bool __succeeded = true;
    try
    {
        инструкции3
    }
    catch(Exception e)
    {
        __succeeded = false;
        throw e;
    }
    finally
    {
        if (!__succeeded) инструкция2
    }
}

Далее выполняется снижение инструкций инструкция2 и инструкции3.

Инструкция scope может пригодиться во многих ситуациях. Предположим, вы хотите создать файл способом транзакции то есть не оставляя на диске «частично созданный» файл, если в процессе его создания произойдет сбой. Здесь можно поступить так:

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.14. Инструкция synchronized

Инструкция synchronized имеет следующий синтаксис:

synchronized (выражение1, выражение2...) инструкция

С ее помощью можно расставлять контекстные блокировки в многопоточных программах. Семантика инструкции synchronized определена в главе 13.

В начало ⮍ Наверх ⮍

3.15. Конструкция asm

D нарушил бы свой обет быть языком для системного программирования, если бы не предоставил некоторые средства для взаимодействия с ассемблером. И если вы любите трудности, то будете счастливы узнать, что в D есть тщательно определенный встроенный язык ассемблера для Intel x86. Кроме того, этот язык переносим между всеми реализациями D, работающими на машинах x86. Поскольку ассемблер зависит только от машины, а не от операционной системы, на первый взгляд это средство D не кажется революционным, тем не менее вы будете удивлены. Исторически сложилось, что каждая операционная система определяет собственный синтаксис ассемблера, не совместимый с другими ОС, поэтому, например, код, написанный для Windows, не будет работать под управлением Linux, так как синтаксисы ассемблеров этих операционных систем разительно отличаются друг от друга (что вряд ли оправданно). D разрубает этот гордиев узел, отказавшись от внешнего ассемблера, специфичного для каждой системы. Вместо этого компилятор сам выполняет синтаксический анализ и распознает инструкции ассемблерного языка. Чтобы написать код на ассемблере, делайте так:

asm инструкция на ассемблере

или так:

asm { инструкции на ассемблере }

Идентификаторы, видимые перед конструкцией asm, доступны и внутри нее: ассемблерный код может использовать сущности D. Ассемблер D описывается в главе 11; он покажется знакомым любому, кто работал с ассемблером x86. Всю информацию по ассемблеру D вы найдете в документации.

В начало ⮍ Наверх ⮍

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 (‹в›); Выполняет ‹и› один раз и продолжает ее выполнять, пока ‹в› истинно (см. раздел 3.7.2)
for (и1 в1; в2) и2 Выполняет и1, которая может быть инструкцией-выражением, определением значения или просто точкой с запятой, и пока в1 истинно, выполняет и2, после чего вычисляет в2 (см. раздел 3.7.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 (см. раздел 3.10)
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 (‹в›) ‹и› Выполняет ‹и›, пока ‹в› ненулевое (цикл не выполняется, если уже при первом вычислении ‹в› оказывается нулевым) (см. раздел 3.13)
with (‹в›) ‹и› Вычисляет ‹в›, затем выполняет ‹и›, как если бы она была членом типа ‹в›: все используемые в ‹и› идентификаторы сначала ищутся в пространстве имен, определенном ‹в› (см. раздел 3.9)

В начало ⮍ Наверх ⮍


  1. Да-да, это «еще одно место, где используется ключевое слово static». ↩︎

  2. Тип enum будет рассмотрен позже. Для понимания примера надо знать, что значения объявленные как enum, определены на этапе компиляции, неизменны и могут использоваться в конструкциях, вычисляемых на этапе компиляции. Прим. науч. ред. ↩︎

  3. Существует также цикл foreach_reverse, который работает аналогично foreach, но перебирает значения в обратном порядке. ↩︎

  4. Идентификаторы, начинающиеся с двух подчеркиваний, описаны в разделе 2.1. Прим. пер. ↩︎

  5. В стандартной библиотеке (STL) C++ для определения завершения цикла последовательно используется оператор != на том основании, что (не)равенство более общая форма сравнения, так как она применима к большему количеству типов. Подход D не менее общий, но при этом, когда это возможно, для повышения безопасности вычислений использует <, не проигрывая ни в обобщенности, ни в эффективности. ↩︎

  6. LIFO акроним «Last In First Out» (последним пришел первым ушел). Прим. пер. ↩︎

  7. CleanerUpper «уборщик» (от англ. clean up убирать, чистить). Прим. пер. ↩︎