# Управляющие инструкции (statement)

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

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

x = y + 1;

if (z)
   x = y + 1;

{
   x = y + 1;
}

{
   if (z)
      x = y + 1;
}

# Инструкция if-else (если-иначе)

Эта инструкция позволяет в зависимости от какого-то условия выполнять или не выполнять блок кода, либо выбирать блок кода для исполнения по какому-то условию.

'if' '(' <условие> ')'
   <блок>

'if' '(' <условие> ')'
   <блок-1>
'else'
   <блок-2>

Инструкция вычисляет значение выражения <условие> и

  • в первой форме: если результат вычисления преобразуется в true, выполняет блок кода, иначе пропускает этот блок

  • во второй форме: если результат вычисления преобразуется в true, выполняет <блок-1>, иначе выполняет <блок-2>

Пример:

var x = 1
var y

if (x > 0)
   y = true;      # условие x > 0 преобразуется в true,
                  # выполняется выражение y = true

if (x < 1)
   y = true;
else
   y = false;     # условие x < 1 преобразуется в false,
                  # выполняется выражение y = false

Поскольку сама инструкция if-else является блоком, их можно соединять друг с другом:

if (<условие-1>)
   <блок-1>
else if (<условие-2>)
   <блок-2>
...
else if (<условие-N>)
   <блок-N>
else
<блок-N+1>

Завершающий else без условия не обязателен. При этом else "прикрепляется" к ближайшему if:

if (<условие-1>)
   if (<условие-2>)
      <блок-1>
   else
      <блок-2>

эквивалентно

if (<условие-1>)
{
   if (<условие-2>)
      <блок-1>
   else
      <блок-2>
}

а не

if (<условие-1>)
{
   if (<условие-2>)
      <блок-1>
}
else
{
   <блок-2>
}

# Инструкция switch-case (выборПо-выбор)

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

'switch' '(' <селектор> ')'
'{'
   ['case' <вариант-1-1> [',' <вариант-1-2>] ... [,] ':' <блок-case-1>]
   ...
   ['case' <вариант-N-1> [',' <вариант-N-2>] ... [,] ':' <блок-case-N>]
   ['else' <блок-else>]
'}'

<селектор> и <вариант-k-i> могут быть произвольными выражениями. Количество вариантов в разных ветвях case может быть различным.

Инструкция работает следующим образом. Сначала вычисляется значение выражения <селектор> (обозначим его символом s). Затем последовательно слева-направо сверху-вниз вычисляются выражения <вариант-k-i> (обозначим результаты их вычисления символами c-k-i), и результат их вычисления сравнивается с s при помощи оператора ==. Если сравнение s с c-k-i возвращает true, то оставшиеся <вариант-k-i> не вычисляются, а выполняется <блок-case-k>, затем управление передаётся на следующую за switch инструкцию или выражение.

Если ни для одного <вариант-k-i> сравнение с s не вернуло true, то выполняется блок-else. Затем выполнение передаётся на следующую за switch инструкцию или выражение.

Если ветвь else отсутствует и ни одно сравнение не вернуло true, то управление просто передаётся дальше. Если нет ни одной ветви case, но есть ветвь else, то она будет выполняться всегда. Если нет ни ветвей case, ни ветви else, то инструкция не делает ничего, кроме вычисления выражения <селектор>.

После выполнения <блок-case-k> не происходит "проваливание" в следующую ветвь case (как в C++), поэтому писать break (прервать) после каждого блока не нужно. Однако внутри блоков можно использовать оператор break, чтобы немедленно завершить выполнение блока и передать управление следующей за switch инструкции или выражению.

Пример:

var x = "3";
var y;

switch (x)
{
   case "1", "2":
      y = "1 or 2";
   case "3"
      y = "3";       # управление попадает в этот блок
   else
      y = ""
}

# Инструкция while (пока)

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

'while' '(' <условие> ')'
   <блок>

Инструкция вычисляет выражение <условие>, и если результат вычисления может быть преобразован в

  • true, то выполняет блок и возвращается к вычислению условия

  • false, то не выполняет блок и передаёт управление следующему за блоком выражению или инструкции

Если значение условия на первой же итерации равно или преобразуется в false, то блок не будет выполнен ни разу. Иначе блок кода будет выполнен столько раз, сколько раз вычисление условия вернуло true (или значение, которое преобразуется в true).

Внутри блока можно использовать инструкцию break (прервать), чтобы немедленно прервать выполнение цикла и передать управление следующей за циклом инструкции или выражению. Инструкция return (вернуть) также немедленно завершает цикл.

Инструкция continue (продолжить) позволяет перейти на следующую итерацию, не выполняя до конца тело цикла. В случае вложенных циклов, инструкция действует на текущий.

Пример:

x = 2;
r = 0;
while (x-- > 0)
   r += 2;

# цикл выполнился два раза, значение r == 4
# ниже этот же цикл переписан с помощью инструкции break

x = 2;
r = 0;
while (true)
{
   if (x-- > 0)
      r += 2;
   else
      break;

}

# Инструкция for (для)

Инструкция for имеет синтаксис

'for' '(' [ <инициализатор> ] ';' [ <условие> ] ';' [ <шаг-цикла> ] ')'
   <блок>

Здесь

  • <инициализатор> - одно или несколько выражений, разделенных запятыми, либо объявление одной или нескольких переменных (var)

  • <условие> - выражение, результатом вычисления которого должно быть значение типа Boolean либо значение, которое приводится к этому типу

  • <шаг-цикла> - одно или несколько выражений, разделенных запятыми

Инструкция выполняется следующим образом. <инициализатор> вычисляется один раз перед входом в цикл. Затем на каждой итерации цикла вычисляется выражение <условие>. Если результатом вычисления будет true (либо значение, которое приводится к true), выполняется тело цикла <блок>, затем вычиcляется <шаг-цикла>, далее поток выполнения возвращается к выражению <условие>. Когда <условие> становится ложным (false), цикл завершается.

Цикл for можно преобразовать в цикл while:

<инициализатор>;
while (<условие>)
{
   <блок>;
   <шаг-цикла>;
}

Внутри цикла можно использовать инструкции break и continue.

Выражения <инициализатор>, <условие> и <шаг-цикла> являются необязательными, любое из них можно пропустить. Если пропущено <условие>, оно считается всегда истинным; в этом случае цикл можно завершить только с помощью break, return или throw.

Примеры:

# суммирование чисел от 1 до 10
var r = 0
for (var x = 1; x <= 10; x++)
   r += x;

# "вечный" цикл обработки данных
for(;;)
{
   var data = fetch();
   if (data)
      process(data);
   else
      break;
}

# проход по диагонали матрицы
for (var x = 1, y = 1; x < 10 && y < 10; x++, y++)
   var e = matrix[x][y];

# Инструкция forall ( дляВсех ) и итераторы

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

Объект итератора имеет тип Iterator и предоставляет два базовых метода:

  • hasNext(): Boolean (естьСледующий) - возвращает true, если из последовательности можно получить следующий элемент, иначе - возвращает false

  • next(): Any (следующий) - если предыдущий вызов hasNext вернул true, метод next возвращает следующий элемент последовательности, иначе возникает исключение

Например, если в последовательности два элемента:

  • первый вызов hasNext вернет true, next вернет первый элемент

  • второй вызов hasNext вернет true, next вернет второй элемент

  • третий вызов hasNext вернет false, при вызове next возникнет исключение

Итераторы также имеют дополнительные методы, которые описаны ниже в разделе Методы Iterator.

Все стандартные коллекции и строки предоставляют итераторы для обработки их элементов. Ниже приведён пример суммирования элементов массива с помощью итератора:

var a = @[1, 2, 3]
var i = a.iterator()
var s = 0

while (i.hasNext())
   s += i.next();       # next последовательно возвращает 1, 2, и 3

# s == 6

Цикл forall предназначен для упрощения работы с итераторами. Эта инструкция имеет две формы:

'forall' '(' <фабрика-итераторов> '(' <множество>, <переменная-приёмник> ')' ')'
   <блок>

'forall' '(' <переменная-приёмник> 'in' <итерируемый-объект> ')'
   <блок>

В первой форме, инструкция вызывает функцию <фабрика-итераторов> с одним аргументом <множество>. Множество может быть произвольным выражением, значение которого подходит для фабрики итераторов. Фабрика итераторов возвращает итератор для переданного множества. Затем из итератора последовательно извлекаются все элементы, для каждого элемента его значение присваивается переменной <переменная-приёмник> и выполняется <блок>. Псевдокод:

var _iterator = <фабрика-итераторов>(<множество>);
while (_iterator.hasNext())
{
   var <переменная-приёмник> = _iterator.next();
   <блок>
}

Пример (предположим, что функция elements возвращает итератор для массива):

forall (elements(@[1, 2, 3], e))
   s += e;                          # e последовательно принимает
                                    # значения 1, 2 и 3

# s == 6

Встроенных фабрик итераторов в языке нет. Если <фабрика-итераторов> не определена, или возвращает не итератор, возникает исключение.

Во второй форме инструкция вычисляет выражение <итерируемый-объект>. Результатом вычисления выражения должен быть либо итератор, либо объект, из которого можно автоматически получить итератор (таковыми являются, например, все встроенные коллекции), иначе возникает исключение. Затем из итератора последовательно извлекаются все элементы, для каждого элемента его значение присваивается переменной <переменная-приёмник>, и выполняется <блок>. Псевдокод:

var _iterable = <итерируемый-объект>;

var _iterator;
if (_iterable is Iterator)
   _iterator = _iterable;
else
   _iterator = <get-iterator>(_iterable);

while (_iterator.hasNext())
{
   var <переменная-приёмник> = _iterator.next();
   <блок>
}

Пример:

var r = 0;
forall (e in @[1, 2, 3])
   r += e;                          # e последовательно принимает
                                    # значения 1, 2 и 3

# r равно 6

var cs = @[]
forall (c in "abc".chars())
   cs.pushBack(c);                  # e последовательно принимает
                                    # значения 97, 98 и 99

# cs содержит три элемента: 97, 98, 99

Цикл forall можно принудительно завершить при помощи инструкции break (прервать). После выполнения break управление немедленно передаётся следующей за циклом инструкции или выражению. Таким образом, код внутри блока после инструкции break не выполняется, для всех оставшихся элементов итератора блок также не выполняется.

func contains(ary, val)
{
   var result = false;
   forall (e in ary)
      if (e == val)
      {
         result = true;
         break;               # управление передаётся инструкции return
      }
   return result;
}

contains(@[1, 2, 3], 2);      # true, выполнены всего две итерации
                              # цикла для элементов 1 и 2
contains(@[1, 2, 3], 4);      # false

Инструкция return (вернуть) также немедленно завершает цикл:

func contains(ary, val)
{
   forall (e in ary)
      if (e == val)
         return true;

   return false;
}

Инструкция continue (продолжить) позволяет перейти на следующую итерацию, не выполняя до конца тело цикла. В случае вложенных циклов, инструкция действует на текущий.

# Пользовательские итераторы

В качестве итератора или фабрики итераторов в инструкции forall может выступать экземпляр класса. По умолчанию в цикле forall перебираются все поля класса в лексикографическом порядке (так же как и для Object):

class C
{
   constructor()
   {
      this.a = "1"
      this.b = "2"
   }
}

var r = ""
forall (e in new C())
   r += e.value

r == "12"                           # true

Чтобы экземпляр класса стал пользовательским итератором, на нем достаточно определить методы hasNext (либо естьСледующий) и next (либо следующий):

class Iter
{
   constructor()
   {
      this.data = @["a", "b", "c"]
      this.currentIndex = 1
   }

   hasNext()
   {
      return this.currentIndex <= this.data.size
   }

   next()
   {
      return this.data[this.currentIndex++]
   }
}

var r = ""
var i = new Iter()
forall (e in i)
   r += e                           # е будет последовательно принимать
                                    # значения "a", "b", "c"

r == "abc"                          # true
i.currentIndex == 4                 # true

При этом обязательно должны быть определены оба метода (в противном случае класс будет вести себя так же как Object). Если в классе одновременно определены и next, и следующий (либо hasNext и естьСледующий), то приоритет будет иметь метод с английским названием (т.е. next и hasNext).

Чтобы класс мог выступать в качестве пользовательской фабрики итераторов, на нем достаточно определить метод iterator (либо итератор), который должен создавать и возвращать итератор:

# используется класс Iter из предыдущего примера

class IterFactory
{
   iterator()
   {
      return new Iter()
   }
}

forall (e in new IterFactory())     # для получения итератора будет
                                    # вызван метод IterFactory::iterator
   ;                                # е будет последовательно принимать
                                    # значения "a", "b", "c"

Если в классе одновременно определены методы iterator и итератор, то приоритет будет иметь метод iterator.

# Методы Iterator

# join(separator: String): String (объединить)

Соединяет все элементы итератора в строку, разделяя отдельные элементы строкой separator. Если separator не является строкой, возникает исключение. Возможная реализация метода:

func join(iterator, separator)
{
   var result = ""
   var index = 1
   forall (e in iterator)
   {
      if (index++ != 1)
         result += separator
      result += String(e)
   }
   return result
}

Пример использования:

@[1, 2, 3].iterator().join(`:`)           # возвращает строку "1:2:3"

# Инструкция try-catch-finally (попытка-исключение-заключение)

Механизм try-catch-finally предназначен для упрощения обработки исключительных ситуаций (ошибок времени выполнения и других проблем, которые не позволяют продолжить нормальное исполнение программы). Код, который может вызвать исключительную ситуацию (контролируемый код) "оборачивается" в конструкцию try. Если внутри контролируемого кода (непосредственно или в любой из функций, которые из него вызываются) возникает исключение, поток управления передаётся в блок catch (обработчик исключительной ситуации), привязанный к этому try. В обработчик можно передать произвольное значение, в котором обычно находится дополнительная информация об исключительной ситуации.

Исключительная ситуация может возникнуть в нативном коде (например, если передать в функцию string значение, которое нельзя привести к строке). Также можно вызвать исключительную ситуацию с помощью инструкции throw.

Код в блоке finally будет выполнен в независимости от того было исключение или нет. В finally обычно располагают код очистки ресурсов, например, закрытие файлов. Если обработка исключений не требуется, то можно пропустить catch и использовать инструкцию try-finally. Инструкция try-catch-finally со всеми тремя блоками неявно преобразуется в try-finally с вложенным try-catch.

Инструкция try-catch-finally имеет синтаксис:

'try'
'{'
   <контролируемый-код>
'}'
'catch' [ (<переменная-приёмник>) ]
'{'
   <обработчик>
'}'
[
'finally'
'{'
   <очистка ресурсов>
'}'
]

Инструкция try-finally имеет синтаксис:

'try'
'{'
   <контролируемый-код>
'}'
'finally'
'{'
   <очистка ресурсов>
'}'

Контролируемый код и обработчик могут состоять из нуля и более инструкций и выражений.

Инструкция throw ( вызватьИсключение ) имеет синтаксис:

'throw' <выражение>

Механизм try-catch работает следующим образом. Интерпретатор последовательно выполняет инструкции и выражения контролируемого кода. Далее,

  • если в ходе выполнения возникает исключительная ситуация, то выполнение контролируемого кода в try немедленно прекращается, и управление передаётся в ветвь catch. Если в catch было указано имя переменной приёмника, то при входе в блок интерпретатор создаст её с типом Exception в текущей области видимости. Если исключительная ситуация была вызвана инструкцией throw, то интерпретатор присваивает свойству thrown этой переменной результат вычисления <выражение>. <выражение> вычисляется в той же области видимости, в которой находится инструкция throw. Если исключение возникло в нативном коде, в переменную-приёмник записывается значение, которое было передано из нативного кода. По окончании ветви catch выполнение передаётся следующей за ней инструкции или выражению;

  • если внутри контролируемого кода не возникает исключение, то ветвь catch не выполняется, а управление передаётся следующей за ней инструкции или выражению.

Пример:

func checkNonNegative(val)
{
   if (val < 0)
      throw "Expected positive number, but got " + val;
}

try
{
   checkNonNegative(-1);      # внутри функции checkNonNegative возникает
                              # исключение, управление передаётся в ветвь
                              # catch,  функция doSomething не вызывается
   doSomething();
}
catch (e)                     # e содержит объект исключения
{
# обработка исключения
   e.thrown;            # строка "Expected positive number, but got -1"
   e.брошенноеЗначение; # строка "Expected positive number, but got -1"

   e.trace;       # строка со стеком вызовов
   e.стекВызовов; # строка со стеком вызовов
}

try
{
   checkNonNegative(1);       # внутри функции checkNonNegative исключение не
                              # возникает, код обработчика catch не выполняется
   doSomething();             # вызывается функция doSomething
}
catch (e)
{
# ...
}

try
{
   throw 1;
}
catch                        # без объявления переменной
{
}

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

try
{
   var x = -1
   try
   {
      checkNonNegative(x);
   }
   catch (e)                     # управление попадает в эту ветвь
   {
      # обработка исключения
   }
}
catch (e)                        # в эту ветвь управление не попадёт
{
# ...
}

Если исключение возникает в ветви catch, то оно может быть обработано в вышестоящей инструкции try:

try
{
   try
   {
      throw 1;
   }
   catch (e)            # после throw 1 управление передаётся в
                        # этот catch, e == 1
   {
      throw 2;
   }
}
catch (e)               # после throw 2 управление передаётся в
                        # этот catch, e == 2
{
# обработка исключения
}

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

throw 1; # аварийное завершение программы

Чтобы получить из перехваченного объекта Exception полную информацию о произошедшем исключении, можно воспользоваться методом formatDiagnostics (получитьДиагностику). Он возвращает строку, в которой содержится текст сообщения об ошибке, стек вызовов и, по возможности, текст строки файла, в которой возникло исключение.