Перейти к основному содержимому

Управляющие конструкции и операторы Nim

Разработчику Архитектору

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.

В Nim нет отдельного тернарного оператора: вместо него if как выражение. Отличие when от if — первое решается на этапе компиляции (ветки в бинарник не попадают). Типы и let/var — в главе про типы.


Управляющие конструкции и операторы Nim

Условные конструкции

Основной условной конструкцией в Nim является if. Она проверяет истинность логического выражения и выполняет соответствующий блок кода. Синтаксис конструкции if следует стандартному шаблону:

if условие1:
блок1
elif условие2:
блок2
else:
блок3

Разбор:

  • if проверяет первое булево условие и запускает соответствующий блок.
  • elif добавляет дополнительную проверку, если предыдущая ветка не сработала.
  • else содержит запасной путь, когда ни одно условие не истинно.
  • Отступы формируют блоки кода и определяют область действия каждой ветви.
  • Такой шаблон покрывает базовую ветвящуюся логику в Nim.

Практический пример:

let score = 82
var grade = ""

if score >= 90:
grade = "A"
elif score >= 75:
grade = "B"
else:
grade = "C"

echo "Оценка: ", grade

Разбор:

  • score задаёт входное значение для ветвления.
  • if/elif/else последовательно проверяют пороги.
  • В grade записывается результат выбранной ветки.
  • Для 82 сработает ветка elif score >= 75, итог — "B".
  • echo выводит итоговую оценку.

Каждое условие представляет собой выражение, результатом которого является булево значение (true или false). Если первое условие истинно, выполняется первый блок, и остальные ветви игнорируются. Если первое условие ложно, проверяется следующее, и так далее. Ветка else выполняется только в том случае, если ни одно из предыдущих условий не оказалось истинным.

Поскольку if является выражением, его можно использовать в присваиваниях:

let x = if a > b: a else: b

Разбор:

  • if используется как выражение, которое возвращает значение.
  • Условие a > b сравнивает два числа.
  • Если условие истинно, в x попадёт a, иначе b.
  • let делает результат неизменяемым после вычисления.
  • Это idiomatic-замена тернарного оператора в Nim.

В этом примере переменная x получает значение большего из двух чисел a и b. Такая форма записи заменяет традиционный тернарный оператор, который в Nim отсутствует. Конструкция if с else всегда должна быть полной, когда используется как выражение, иначе компилятор выдаст ошибку.

Nim также предоставляет специальную форму условного выражения — when. Эта конструкция работает на этапе компиляции и используется для условной компиляции кода. Выражения внутри when должны быть известны во время компиляции и часто используются для проверки констант, флагов сборки или наличия определённых модулей:

when defined(windows):
echo "Сборка под Windows"
elif defined(linux):
echo "Сборка под Linux"
else:
echo "Неизвестная платформа"

Разбор:

  • when выполняется на этапе компиляции, а не во время запуска программы.
  • defined(windows) и defined(linux) проверяют compile-time флаги платформы.
  • В итоговый бинарник попадает только выбранная ветка.
  • Это позволяет держать платформозависимый код в одном месте без runtime-проверок.
  • При отсутствии известных флагов используется ветка else.

Конструкция when не генерирует исполняемый код для невыбранных ветвей, что делает её эффективным инструментом для написания кроссплатформенного кода.

ifwhen
Когда вычисляетсяВо время выполненияНа этапе компиляции
УсловиеЛюбое boolstatic, defined(...), константы
Невыбранные веткиСуществуют в бинарникеИсключаются из сборки
Типичное применениеЛогика программыПлатформа, флаги -d:, выбор API

Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В Nim синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…


Циклы

Nim предлагает несколько видов циклов, каждый из которых предназначен для решения определённых задач.


Цикл while

Цикл while выполняет блок кода до тех пор, пока условие остаётся истинным. Проверка условия происходит перед каждой итерацией:

var i = 0
while i < 10:
echo i
inc i

Разбор:

  • var i = 0 инициализирует счётчик цикла.
  • while i < 10 выполняет тело, пока условие остаётся истинным.
  • echo i печатает текущее значение счётчика.
  • inc i увеличивает i на единицу, продвигая цикл к завершению.
  • Без inc цикл стал бы бесконечным.

Если условие изначально ложно, тело цикла не выполнится ни разу. Цикл while подходит для ситуаций, когда количество итераций заранее неизвестно.

continue пропускает оставшуюся часть текущей итерации:

var n = 0
while n < 5:
inc n
if n mod 2 == 0:
continue
echo n # 1, 3, 5

Разбор:

  • while n < 5 ограничивает число итераций.
  • inc n увеличивает счётчик в начале каждой итерации.
  • Для чётных n срабатывает continue, и echo не выполняется.
  • Для нечётных значений печатается только n.
  • В итоге выводятся 1, 3, 5.

Цикл for

Цикл for в Nim используется для итерации по последовательностям, диапазонам, строкам, массивам и другим итерируемым объектам. Он автоматически связывает переменную цикла с текущим элементом:

for i in 0..9:
echo i

Разбор:

  • for итерирует по диапазону значений.
  • 0..9 включает обе границы диапазона.
  • i автоматически принимает значения от 0 до 9.
  • echo i выводит каждое значение на отдельной итерации.

Диапазон 0..9 включает обе границы. Чтобы исключить верхнюю границу, используется оператор ..<:

for i in 0..<10:
echo i

Разбор:

  • ..< создаёт полуинтервал с исключённой верхней границей.
  • Диапазон 0..<10 эквивалентен последовательности 0..9.
  • Такой синтаксис удобен для индексации коллекций длины 10.
  • Остальная логика цикла идентична обычному for.

Цикл for также работает с коллекциями:

let fruits = @["яблоко", "банан", "апельсин"]
for fruit in fruits:
echo fruit

Разбор:

  • @[...] создаёт динамическую последовательность строк.
  • for fruit in fruits перебирает элементы коллекции по значениям.
  • На каждой итерации fruit содержит текущую строку.
  • echo fruit печатает элементы в исходном порядке.
  • Такой цикл читабелен и не требует ручных индексов.

В отличие от многих других языков, переменная цикла в Nim является неизменяемой (let), и её нельзя переназначить внутри тела цикла. Это способствует функциональному стилю программирования и предотвращает случайные ошибки.


Бесконечный цикл

Отдельного ключевого слова loop в Nim нет. Бесконечный цикл пишут через while true:; пустое тело — через discard:

while true:
echo "Итерация"
break # выход из цикла

Разбор:

  • while true формирует бесконечный цикл.
  • Тело выполняется до тех пор, пока не встретится явный выход.
  • break немедленно завершает цикл.
  • В примере цикл делает одну итерацию: печать + выход.
  • Этот шаблон полезен для опроса, event-loop и long-running задач.

Для вложенных циклов используют метки (block / for с именем) и break имяМетки — см. ниже в разделе про метки.


Управление потоком выполнения

Внутри циклов Nim предоставляет два оператора для управления потоком: break и continue.

Оператор break немедленно прекращает выполнение цикла и передаёт управление на первую инструкцию после цикла. Оператор continue прерывает текущую итерацию и переходит к следующей.

Оба оператора могут быть помечены метками, что позволяет управлять вложенными циклами:

outerLoop:
for i in 0..5:
for j in 0..5:
if i * j == 6:
break outerLoop

Разбор:

  • outerLoop: задаёт метку внешнего цикла.
  • Вложенные циклы перебирают пары значений i и j.
  • Условие i * j == 6 ищет конкретную комбинацию произведения.
  • break outerLoop прерывает сразу внешний цикл, а не только внутренний.
  • Метки упрощают управление сложными вложенными структурами.

Метка outerLoop указывает, какой именно цикл должен быть прерван. Это особенно полезно при работе с глубоко вложенными структурами.


Выражения case

Конструкция case в Nim служит для выбора одного из нескольких возможных путей выполнения на основе значения выражения. Она аналогична оператору switch в C-подобных языках, но обладает рядом важных отличий.

Синтаксис case выглядит следующим образом:

case выражение
of шаблон1:
блок1
of шаблон2, шаблон3:
блок2
else:
блок3

Разбор:

  • case выбирает одну ветку по значению выражения.
  • of задаёт конкретные совпадения (одно или несколько через запятую).
  • else обрабатывает все остальные значения.
  • В отличие от цепочки if/elif, здесь логика компактнее для дискретных вариантов.
  • Конструкция хорошо подходит для enum, кодов состояния и фиксированных категорий.

Выражение в заголовке case должно иметь дискретный тип — целое число, перечисление, строку или другой тип, поддерживающий сравнение. Каждый шаблон в ветке of может быть константой, списком констант через запятую или диапазоном.

Важной особенностью case в Nim является обязательность обработки всех возможных значений. Если выражение имеет ограниченный набор значений (например, перечислимый тип), компилятор требует, чтобы все варианты были явно указаны либо покрыты веткой else. Это предотвращает ошибки, связанные с неполным перебором случаев.

Конструкция case также является выражением и может возвращать значение:

let message = case day
of 1: "Понедельник"
of 2: "Вторник"
of 3: "Среда"
of 4: "Четверг"
of 5: "Пятница"
of 6, 7: "Выходные"
else: "Недопустимый день"

Разбор:

  • case используется как выражение и возвращает строку.
  • Для каждого номера дня задано своё текстовое представление.
  • of 6, 7 объединяет два значения в одну ветку.
  • else делает обработку полной и предотвращает "дырки" в логике.
  • Результат сразу присваивается в message.

Исключения и обработка ошибок

Интерактивное демо — часть сценариев на Python (try / except); в Nim — try / except, но раскрутка стека та же. Подробнее: ошибки и исключения.

Play ITЗагрузка интерактивного демо…

Nim использует механизм исключений для обработки ошибок времени выполнения. Исключения выбрасываются с помощью оператора raise и перехватываются с помощью блока try/except:

try:
riskyOperation()
except IOError:
echo "Ошибка ввода-вывода"
except ValueError:
echo "Некорректное значение"
except:
echo "Неизвестная ошибка"

Разбор:

  • try оборачивает потенциально аварийный код.
  • except IOError ловит ошибки ввода-вывода.
  • except ValueError перехватывает ошибки некорректных значений.
  • Финальный except без типа действует как fallback для прочих исключений.
  • Порядок важен: от более специфичных обработчиков к более общему.

Блок except может указывать конкретный тип исключения или быть общим. Общий обработчик должен располагаться последним.

Nim также поддерживает блок finally, который выполняется независимо от того, было ли выброшено исключение:

try:
openFile()
processFile()
except:
echo "Ошибка при обработке файла"
finally:
closeFile()

Разбор:

  • В блоке try выполняются операции открытия и обработки файла.
  • except реагирует на исключения и сохраняет управляемое поведение.
  • finally выполняется всегда, независимо от успеха или ошибки.
  • closeFile() в finally гарантирует освобождение ресурса.
  • Это базовый безопасный шаблон для работы с файлами и соединениями.

Это гарантирует корректное освобождение ресурсов даже в случае возникновения ошибки.


Операторы

Play ITЗагрузка интерактивного демо…

Помимо управляющих конструкций, Nim предоставляет богатый набор операторов, которые влияют на логику программы.

Логические операторы включают and, or, not. Они имеют стандартную семантику и поддерживают короткое замыкание: в выражении a and b подвыражение b не вычисляется, если a ложно; в выражении a or b подвыражение b не вычисляется, если a истинно.

Операторы сравнения — ==, !=, <, <=, >, >= — работают со всеми сравнимыми типами. Nim не допускает неявного приведения типов при сравнении, что повышает безопасность кода.

Операторы присваивания включают простое присваивание =, а также составные формы, такие как +=, -= и другие. Однако важно отметить, что в Nim оператор = используется только для объявления переменных (var, let, const), а изменение существующей переменной осуществляется с помощью = только в контексте var.

var total = 0
let active = true
let hasRole = false

if active and (not hasRole):
total += 10

echo total

Разбор:

  • and и not — логические операторы с коротким замыканием.
  • active and (not hasRole) истинно, поэтому блок if выполняется.
  • += — составное присваивание, эквивалентно total = total + 10.
  • let используется для неизменяемых флагов, var — для накапливаемого счётчика.
  • Итоговый echo выводит 10.

Nim также поддерживает пользовательские операторы и перегрузку существующих, что позволяет создавать выразительные DSL-подобные конструкции. Полная таблица приоритетов — в руководстве Nim.


Блоки и область видимости

В языке Nim основной единицей группировки кода является блок. Блок определяется отступами — как в Python — и может содержать последовательность выражений, переменных, вызовов функций и вложенных конструкций. Каждый блок создаёт собственную лексическую область видимости — переменные, объявленные внутри блока, недоступны за его пределами.

Nim поддерживает три ключевых способа объявления переменных:

  • let — неизменяемая связь, значение присваивается один раз и не может быть изменено;
  • var — изменяемая переменная, значение можно переназначать;
  • const — константа времени компиляции, вычисляется до запуска программы.

Область видимости переменных строго следует правилам вложенности блоков. Это означает, что переменная, объявленная во внешнем блоке, доступна во всех вложенных, но не наоборот. Такая модель упрощает анализ кода и предотвращает случайные коллизии имён.

Особое внимание заслуживает поведение переменных в циклах. В Nim каждая итерация цикла for создаёт новую область видимости для переменной цикла. Это исключает распространённую ошибку, наблюдаемую в других языках, когда замыкания захватывают одну и ту же переменную, а не её значение на конкретной итерации.

Пример:

var closures: seq[proc(): int] = @[]
for i in 0..2:
closures.add(proc(): int = i)

for c in closures:
echo c() # Выведет 0, 1, 2

Разбор:

  • closures хранит последовательность функций без параметров, возвращающих int.
  • В первом цикле добавляются замыкания, каждое захватывает своё значение i.
  • closures.add(...) помещает очередную функцию в коллекцию.
  • Во втором цикле функции вызываются через c().
  • Вывод 0, 1, 2 показывает корректную область видимости на каждой итерации.

Здесь каждая анонимная функция захватывает своё собственное значение i, а не общую переменную. Это поведение соответствует принципам функционального программирования и делает код более предсказуемым.


Выражения и вычисление значений

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

Это свойство особенно полезно при инициализации констант или параметров функций:

proc describeGrade(score: int): string =
case score
of 90..100: "Отлично"
of 80..<90: "Хорошо"
of 70..<80: "Удовлетворительно"
else: "Неудовлетворительно"

echo describeGrade(85) # Выведет "Хорошо"

Разбор:

  • describeGrade принимает балл и возвращает текстовую категорию.
  • Внутри используется case с диапазонами значений.
  • 90..100 включает оба конца, 80..<90 исключает 90.
  • else закрывает все оставшиеся значения.
  • Вызов describeGrade(85) попадает во вторую ветку и возвращает "Хорошо".

Такой подход устраняет необходимость в промежуточных переменных и делает код компактным без потери читаемости.

Nim также поддерживает так называемые expression blocks — блоки, заключённые в фигурные скобки {} или оформленные как block:. Они позволяют группировать несколько выражений и возвращать значение последнего из них:

let x = block:
let a = 10
let b = 20
a + b

echo x # Выведет 30

Разбор:

  • block: создаёт локальную область видимости и возвращает значение последнего выражения.
  • Внутри объявлены вспомогательные переменные a и b.
  • Выражение a + b становится результатом всего блока.
  • Этот результат присваивается в x.
  • Приём полезен, когда нужно компактно вычислить значение из нескольких шагов.

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


Продвинутые техники управления потоком

Метки и именованные блоки

Nim позволяет присваивать имена любому блоку с помощью ключевого слова block. Имя блока может использоваться с операторами break и continue для точного указания, какой именно цикл или блок должен быть прерван:

outer:
for i in 0..5:
for j in 0..5:
if i * j == 12:
break outer
echo "Итерация i:", i

Разбор:

  • outer: задаёт метку для внешнего цикла.
  • Внутренний цикл перебирает j для каждого i.
  • При выполнении условия i * j == 12 выполняется break outer.
  • Это завершает всю вложенную конструкцию сразу.
  • echo внизу срабатывает только пока метка не была прервана.

Без метки outer оператор break прервал бы только внутренний цикл. Метки дают полный контроль над вложенными структурами и делают логику явной.


Условное выполнение с if в выражениях

Поскольку if — это выражение, его можно встраивать прямо в аргументы функций, элементы коллекций или условия других конструкций:

let message = "Статус: " & (if active: "активен" else: "неактивен")

Разбор:

  • Оператор & склеивает строки в Nim.
  • В скобках if работает как выражение и возвращает одну из двух строк.
  • При active == true подставляется "активен", иначе "неактивен".
  • Результат целиком сохраняется в неизменяемую переменную message.

Такая форма записи заменяет тернарный оператор и сохраняет единообразие синтаксиса.


Обработка исключений как выражение

Блок try также может использоваться как выражение, если все его ветви (except, else, finally) возвращают совместимые типы:

let result = try:
riskyComputation()
except IOError:
defaultFallback()

Разбор:

  • try здесь используется как выражение с возвращаемым значением.
  • В успешном случае в result попадёт значение riskyComputation().
  • Если возникнет IOError, выполнится ветка except и вернётся defaultFallback().
  • Оба пути должны давать совместимые типы, чтобы присваивание в result было корректным.

Это позволяет интегрировать обработку ошибок непосредственно в поток вычислений, не нарушая линейности кода.


Операторы и их приоритет

Операторы в Nim можно перегружать; приоритет встроенных операторов задан в языке (*, /, +, -, сравнения, and/or с коротким замыканием). У пользовательских операторов приоритет выводится по первому символу имени (см. manual) — это упрощает DSL, но требует аккуратного выбора символов (~> наследует уровень от ~).

Строки склеивают оператором & (не +).