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

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

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

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

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


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

На практике этот раздел про ежедневную "механику" кода — как писать ветвления, циклы и арифметику так, чтобы поведение программы оставалось предсказуемым и в Debug, и в Release.

Ниже разберём не только синтаксис, но и типичные решения, которые делают код Zig читаемым в команде.


Общая философия управления потоком в Zig

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

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


Блоки как основа структуры

Блок в Zig — это последовательность выражений, заключённая в фигурные скобки {}. Блок сам по себе является выражением и имеет тип, определяемый последним выражением внутри него. Если блок завершается точкой с запятой или не содержит выражений, его тип — void.

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

const result = {
var x: i32 = 10;
var y: i32 = 20;
x + y
};
// result имеет значение 30 и тип i32

Разбор:

  • Блок в фигурных скобках выступает как выражение и возвращает значение последней строки.
  • x и y имеют локальную область видимости, поэтому не "утекают" наружу.
  • Выражение x + y становится итоговым значением блока и присваивается result.
  • Такой подход помогает компактно вычислять значения с промежуточными шагами.

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


Условное выполнение — if

Конструкция if в Zig используется для выбора одного из двух возможных путей выполнения на основе логического условия. Условие должно быть выражением типа bool. В отличие от C-подобных языков, в Zig отсутствуют неявные преобразования значений в логический тип, поэтому любое условие должно быть явно булевым.

Синтаксис if в Zig:

const std = @import("std");

const threshold: i32 = 100;
const value: i32 = 150;
if (value > threshold) {
std.debug.print("Выше порога\n", .{});
} else {
std.debug.print("Ниже или равно порогу\n", .{});
}

Разбор:

  • Условие value > threshold имеет тип bool; сравнение целых выполняется без неявных приведений.
  • Ветка if выполняется только при истинном условии, иначе — ветка else.
  • Обе ветки оформлены блоками {}, что позволяет держать в них несколько операторов.

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

Ключевая особенность if в Zig — возможность использовать его как выражение. Это означает, что if может возвращать значение, которое затем используется в других частях программы.

const max = if (a > b) a else b;

Разбор:

  • if в Zig является выражением, поэтому его результат можно сразу присваивать.
  • Условие a > b должно быть строго булевым (bool), неявных привидений нет.
  • Обе ветки (a и b) должны быть совместимы по типу.
  • Эта запись заменяет тернарный оператор и остаётся прозрачной для чтения.

В этом примере переменная max получает значение большего из двух чисел. Такая форма записи заменяет собой тернарный оператор, который в Zig отсутствует как отдельная конструкция. Язык считает, что if как выражение достаточно выразителен и не требует дополнительного синтаксиса.

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

const message = if (score >= 90) {
std.debug.print("Отлично!\n", .{});
"excellent"
} else if (score >= 70) {
std.debug.print("Хорошо.\n", .{});
"good"
} else {
std.debug.print("Нужно улучшить.\n", .{});
"needs_improvement"
};

Разбор:

  • Каждая ветка if / else if / else — блок-выражение; последняя строка блока задаёт возвращаемое значение.
  • std.debug.print внутри ветки — побочный эффект, строковый литерал "excellent" — итоговое значение ветки.
  • Переменная message получает один из трёх строковых статусов в зависимости от score.

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

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

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


Циклы — while и for

Zig предоставляет два основных цикла: while и for. Оба являются выражениями и могут возвращать значения.


Цикл while

Цикл while выполняет тело до тех пор, пока условие остаётся истинным. Условие, как и в if, должно быть типа bool.

var i: usize = 0;
while (i < 10) {
std.debug.print("{}\n", .{i});
i += 1;
}

Разбор:

  • while повторяет тело, пока условие i < 10 истинно.
  • usize подходит для счётчика, особенно в задачах индексации и обхода коллекций.
  • На каждой итерации текущее значение i печатается в консоль.
  • i += 1 явно двигает цикл вперёд и предотвращает бесконечное выполнение.

Цикл while в Zig поддерживает дополнительные возможности, расширяющие его гибкость. Одна из таких возможностей — continue expression. Это выражение, вычисляемое перед каждой итерацией и доступное внутри тела цикла через специальный синтаксис.

var i: usize = 0;
while (i < 10) : (i += 1) {
std.debug.print("{}\n", .{i});
}

Разбор:

  • Выражение после : выполняется после каждой итерации как единый шаг обновления состояния.
  • Такой синтаксис уменьшает дублирование и делает структуру цикла более декларативной.
  • Даже при continue это выражение будет выполнено перед следующей итерацией.
  • Паттерн полезен для предсказуемых счётчиков и конечных автоматов.

Выражение после двоеточия (i += 1) выполняется после каждой итерации, включая случаи, когда используется continue. Это позволяет централизовать логику изменения состояния цикла и избежать дублирования кода.

Цикл while также может возвращать значение. Для этого используется метка и оператор break с передачей значения:

const result = blk: {
var i: usize = 0;
while (i < 10) : (i += 1) {
if (some_condition(i)) {
break :blk i;
}
}
break :blk 10; // значение по умолчанию, если условие не выполнилось
};

Разбор:

  • Метка blk: превращает блок в именованное выражение с возвращаемым значением.
  • break :blk i досрочно завершает блок и возвращает текущий индекс.
  • Финальный break :blk 10 задаёт значение по умолчанию, если цикл завершился без совпадения.

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


Цикл for

Цикл for в Zig предназначен для итерации по массивам, срезам, указателям на массивы и другими последовательностями. Он не является универсальным счётчиком, как в C, а работает исключительно с коллекциями.

const numbers = [_]i32{ 1, 2, 3, 4, 5 };
for (numbers) |value| {
std.debug.print("{}\n", .{value});
}

Разбор:

  • for (numbers) итерирует элементы массива по порядку.
  • |value| связывает текущий элемент с локальным именем в теле цикла.
  • Данные читаются без модификации, потому что берётся значение, а не указатель.
  • Такой цикл подходит для простого обхода и агрегирования.

Синтаксис |value| называется деструктуризацией. Он извлекает текущий элемент коллекции и связывает его с именем value внутри тела цикла.

Можно одновременно получать и значение, и индекс:

for (numbers) |value, index| {
std.debug.print("[{}] = {}\n", .{ index, value });
}

Разбор:

  • Второй параметр цикла |value, index| даёт одновременно элемент и его позицию.
  • Порядок фиксирован: сначала значение, затем индекс (usize).
  • Удобно для логов, таблиц и алгоритмов, где важна позиция элемента.

Порядок аргументов фиксирован: сначала значение, затем индекс.

Цикл for также может использоваться с изменяемыми коллекциями. Для этого требуется добавить модификатор var:

var mutable_numbers = [_]i32{ 1, 2, 3 };
for (mutable_numbers) |*value| {
value.* += 10;
}
// теперь mutable_numbers = { 11, 12, 13 }

Разбор:

  • |*value| даёт указатель на элемент, что позволяет менять исходный массив.
  • value.* разыменовывает указатель и предоставляет доступ к реальному числу.
  • Прибавление 10 происходит in-place, без создания копии коллекции.
  • Итоговая мутация видна после цикла во всех последующих вычислениях.

Здесь |*value| означает, что value — это указатель на текущий элемент. Разыменование value.* даёт доступ к самому элементу для изменения.

Как и while, цикл for может быть частью выражения и возвращать значение через метки и break.


Операторы ветвления и перехода

Zig предоставляет ограниченный набор операторов управления потоком — break, continue, return и unreachable.


break

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

const found_index = outer: {
for (items) |item, i| {
if (item == target) break :outer i;
}
break :outer null;
};

Разбор:

  • Метка outer: позволяет выйти из блока с конкретным возвращаемым значением.
  • При успешном поиске break :outer i сразу возвращает индекс совпадения.
  • Если совпадения нет, возвращается null как явно выраженный сценарий "не найдено".
  • Это избавляет код от флагов и ручных пост-проверок после цикла.

continue

Оператор continue завершает текущую итерацию цикла и переходит к следующей. Если в цикле указано выражение после двоеточия (например, : (i += 1)), оно выполняется перед началом новой итерации.


return

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


unreachable

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

switch (enum_value) {
.A => handleA(),
.B => handleB(),
.C => handleC(),
else => unreachable, // все варианты перечисления учтены
}

Разбор:

  • unreachable сообщает компилятору, что ветка else недостижима при полном покрытии enum.
  • Если позже добавить новый вариант enum и забыть обработать его, else может сработать в runtime и аварийно завершить программу.
  • Такой приём помогает держать switch исчерпывающим и ловить забытые варианты на ревью.

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


Выбор по значению — switch

Конструкция switch в Zig — это мощный инструмент для выбора одной из множества веток на основе значения. Она работает с целыми числами, перечислениями, символьными литералами и некоторыми другими типами.

const Color = enum { Red, Green, Blue };

fn describeColor(color: Color) []const u8 {
return switch (color) {
.Red => "красный",
.Green => "зелёный",
.Blue => "синий",
};
}

Разбор:

  • enum фиксирует конечный набор допустимых значений цвета.
  • switch используется как выражение и сразу возвращает строковое описание.
  • Компилятор проверяет исчерпывающий охват вариантов Color.
  • Такой стиль исключает "проваливание" между ветками и делает логику строгой.

Ключевая особенность switch в Zig — исчерпывающий охват. Компилятор требует, чтобы все возможные значения были обработаны явно или покрыты веткой else. Это исключает ошибки, связанные с неполным рассмотрением вариантов.

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

switch (x) {
0 => handleZero(),
1 => handleOne(),
else => handleOther(),
}

Разбор:

  • Для целых типов с большим диапазоном else закрывает все невыписанные значения.
  • Каждая ветка завершается автоматически, без break, как в C.
  • Компилятор проверяет, что все пути возвращают совместимый тип, если switch используется как выражение.

Ветка else может быть заменена на конкретные диапазоны с помощью синтаксиса ...:

switch (code) {
200...299 => handleSuccess(),
400...499 => handleClientError(),
500...599 => handleServerError(),
else => handleUnknown(),
}

Разбор:

  • Синтаксис 200...299 задаёт включительный диапазон значений.
  • Один switch заменяет длинную цепочку if для классификации кодов.
  • else обрабатывает любые значения вне перечисленных диапазонов.

Конструкция switch также является выражением и может возвращать значения, что делает её удобной для инициализации переменных или возврата из функций.

Ветки switch не требуют явного break. Выполнение не "проваливается" в следующую ветку, как в C. Каждая ветка завершается автоматически, что устраняет распространённый класс ошибок.


Операторы в Zig

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

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


Арифметические операторы

Основные арифметические операторы — +, -, *, /, %. Все они работают только с совместимыми числовыми типами. Например, сложение i32 и u32 невозможно без явного преобразования одного из операндов.

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

  • +, -, * — безопасные операторы с проверкой переполнения.
  • +%, -%, *% — "wrapping" операторы, которые игнорируют переполнение и оборачивают значение по модулю размера типа (аналогично поведению в C).
  • +|, -|, *| — операторы, возвращающие ошибку при переполнении вместо аварийного завершения.

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

const a: u8 = 255;
const b = a +% 1; // b == 0, без паники

Разбор:

  • Тип u8 хранит значения в диапазоне 0..255, и верхняя граница здесь достигается.
  • Оператор +% выполняет wrapping-сложение с оборачиванием по модулю размера типа.
  • Поэтому результат становится 0, а не ошибкой переполнения.
  • Такой оператор полезен в низкоуровневых алгоритмах с циклической арифметикой.

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


Логические и побитовые операторы

Логические операторы and, or, not работают исключительно с типом bool. Операторы and и or используют короткое замыкание (short-circuit) — правый операнд не вычисляется, если результат уже определён левым — как в C, Java или Rust. Побитовые операторы &, |, ^, ~, &lt;&lt;, &gt;&gt; применяются к битам целочисленных типов и вычисляют оба операнда. Zig не смешивает логические и побитовые операции, что исключает путаницу.

Для опциональных указателей и nullable-значений идиоматичнее явная распаковка, чем цепочка and:

if (ptr != NULL && ptr->valid)

в Zig часто пишут так:

if (ptr) |p| {
if (p.valid) {
// ...
}
}

Разбор:

  • if (ptr) |p| распаковывает nullable-указатель: внутри блока p гарантированно не null.
  • Вложенный if (p.valid) проверяет уже поле структуры, а не сам указатель.
  • Такой стиль делает оба уровня проверки явными и читаемыми при ревью.

Такой стиль делает проверку на null и доступ к полям явными; это не отменяет short-circuit у and/or для обычных bool.


Операторы сравнения

Операторы ==, !=, <, <=, >, >= доступны для большинства скалярных типов. Сравнение указателей возможно только если они указывают на один и тот же объект или находятся в пределах одного массива. Сравнение структур выполняется поэлементно, при условии, что все поля поддерживают сравнение.

Zig не разрешает сравнение значений разных типов, даже если они логически совместимы (например, i32 и i64). Это требует явного приведения, что повышает надёжность кода.


Управление ошибками

Одна из ключевых особенностей Zig — встроенная система управления ошибками без исключений. Ошибки представляются как значения специального типа error, который может комбинироваться с любым другим типом через объединение !.

Например:

const FileError = error{ FileNotFound, AccessDenied, DiskFull };

fn openFile(path: []const u8) FileError!std.fs.File {
// ...
}

Разбор:

  • error{ ... } объявляет набор именованных ошибок домена.
  • Запись FileError!std.fs.File читается как "либо ошибка из FileError, либо значение std.fs.File".
  • Вызывающий код обязан обработать union через try, catch или if, иначе компилятор предупредит.

Здесь функция возвращает либо файл, либо одну из перечисленных ошибок. Тип FileError!std.fs.File читается как "либо ошибка из FileError, либо значение типа std.fs.File".


Обработка ошибок — catch и try

Для обработки ошибок используются два основных механизма: catch и try.

Оператор catch перехватывает ошибку — можно обработать её или вернуть вызывающему коду:

const std = @import("std");

fn load() !void {
_ = openFile("data.txt") catch |err| {
std.debug.print("Не удалось открыть: {}\n", .{err});
return err;
};
}

Разбор:

  • Сигнатура !void означает, что функция может завершиться ошибкой вместо обычного успеха.
  • catch |err| локально перехватывает ошибку и даёт доступ к её значению в err.
  • Блок печатает диагностическое сообщение и возвращает ту же ошибку выше по стеку.
  • _ = ... показывает, что успешный результат намеренно игнорируется.

Оператор try передаёт ошибку выше по стеку вызовов:

const file = try openFile("data.txt");

Разбор:

  • try разворачивает успешное значение std.fs.File в переменную file.
  • При ошибке текущая функция немедленно завершается и возвращает её вызывающему коду.
  • Эквивалентная запись длиннее: openFile(...) catch |err| return err.

Если произойдёт ошибка, текущая функция немедленно завершится и вернёт эту ошибку вызывающему коду. Это эквивалентно:

const file = openFile("data.txt") catch |err| return err;

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


Сопоставление ошибок — switch

Поскольку ошибки в Zig — это перечислимые значения, их можно обрабатывать с помощью switch:

const std = @import("std");

const result = openFile("config.ini") catch |err| {
switch (err) {
error.FileNotFound => std.debug.print("Файл не найден\n", .{}),
error.AccessDenied => std.debug.print("Доступ запрещён\n", .{}),
else => std.debug.print("Другая ошибка\n", .{}),
}
return;
};
_ = result; // использовать file

Разбор:

  • catch |err| перехватывает ошибку из openFile и позволяет обработать её локально.
  • switch (err) классифицирует конкретные ошибки и печатает разные сообщения.
  • После обработки return; завершает функцию; result используется только при успешном открытии.

Для error union с полезной нагрузкой удобнее if с двумя ветками:

if (openFile("config.ini")) |file| {
_ = file;
} else |err| switch (err) {
error.FileNotFound => std.debug.print("Файл не найден\n", .{}),
error.AccessDenied => std.debug.print("Доступ запрещён\n", .{}),
else => {},
}

Разбор:

  • if (openFile(...)) |file| — ветка успеха: внутри доступен реальный std.fs.File.
  • else |err| — ветка ошибки: далее switch по конкретным кодам.
  • Такой шаблон удобен, когда нужно и значение, и раздельная обработка ошибок в одном выражении.

Такая обработка гарантирует, что все возможные ошибки учтены, а компилятор требует полного покрытия вариантов.


Компиляционное время и управление потоком

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


if и while во время компиляции

Конструкции if и while могут быть помечены как comptime, что заставляет их выполняться на этапе компиляции:

const std = @import("std");

comptime {
if (@import("builtin").mode == .Debug) {
@compileLog("Режим отладки");
}
}

Разбор:

  • Блок comptime { ... } выполняется только при сборке, не во время запуска программы.
  • Условие по builtin.mode выбирает ветку в зависимости от режима оптимизации.
  • @compileLog пишет диагностику в лог компилятора, полезно для отладки метакода.

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


comptime-параметры и функции

Функции в Zig могут принимать параметры времени компиляции. Это позволяет создавать обобщённый код без шаблонов или макросов:

fn createArray(comptime T: type, comptime len: usize) [len]T {
var arr: [len]T = undefined;
for (arr) |*elem| {
elem.* = T(0);
}
return arr;
}

const zeros = createArray(i32, 10); // создаёт [10]i32, заполненный нулями

Разбор:

  • comptime T и comptime len требуют compile-time известные параметры типа и длины.
  • Возвращаемый тип [len]T генерируется с учётом аргументов и фиксируется компилятором.
  • Цикл с |*elem| инициализирует каждый элемент массива через указатель.
  • На выходе получается специализированный массив [10]i32 без runtime-генериков.

Здесь T и len известны на этапе компиляции, поэтому функция генерирует конкретный код без накладных расходов во время выполнения.

Такой подход заменяет собой метапрограммирование, сохраняя читаемость и типобезопасность.


Отложенное выполнение — defer и errdefer

Zig предоставляет два специальных оператора для отложенного выполнения кода: defer и errdefer. Оба они гарантируют, что определённый блок кода будет выполнен при выходе из текущей области видимости, но различаются условиями срабатывания.


Оператор defer

Оператор defer выполняет указанное выражение или блок при любом выходе из текущей области — будь то нормальный возврат через return, выход через break, завершение функции или даже возникновение ошибки. Это делает defer идеальным инструментом для освобождения ресурсов, закрытия файлов, разблокировки мьютексов и других задач очистки.

fn processFile(path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();

// работа с файлом
_ = try file.readAllAlloc(std.heap.page_allocator, 1024);
}

Разбор:

  • defer file.close(); гарантирует закрытие дескриптора при любом выходе из функции.
  • try внутри тела пробрасывает ошибки чтения, но defer всё равно выполнится перед выходом.
  • Порядок defer — LIFO: последний объявленный выполняется первым.

В этом примере file.close() будет вызван независимо от того, завершится ли функция успешно или вернёт ошибку. Порядок выполнения defer — обратный порядку их объявления, что соответствует принципу LIFO (last in, first out). Это позволяет корректно управлять вложенными ресурсами.


Оператор errdefer

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

fn allocateAndInit(allocator: std.mem.Allocator) ![]u8 {
const buffer = try allocator.alloc(u8, 1024);
errdefer allocator.free(buffer);

try initializeData(buffer);
return buffer;
}

Разбор:

  • errdefer allocator.free(buffer); срабатывает только если функция завершится ошибкой.
  • При успешном return buffer память передаётся вызывающему коду и не освобождается здесь.
  • Паттерн защищает от утечек при частично выполненной инициализации.

Если инициализация завершится ошибкой, errdefer освободит память. Если всё пройдёт успешно, память передаётся вызывающему коду, и ответственность за её освобождение переходит к нему.

Оба оператора работают на уровне лексической области видимости, а не функции, что позволяет использовать их внутри блоков, циклов и условий.


Метки и управление вложенными конструкциями

Zig поддерживает именованные метки для точного управления потоком выполнения в сложных вложенных структурах. Метка ставится перед блоком, циклом или конструкцией if, и затем используется с break или continue.

outer_loop: for (matrix) |row| {
for (row) |cell| {
if (cell == target) break :outer_loop;
}
}

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

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

const result = find_block: {
for (items) |item| {
if (item.isValid()) {
break :find_block item.value;
}
}
break :find_block default_value;
};

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


Указатели и управление памятью в управляющих конструкциях

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

Указатели в Zig бывают двух видов:

  • *T — указатель на одиночное значение типа T,
  • [*]T — указатель на первый элемент массива неизвестной длины.

В циклах for часто используется синтаксис |*elem| для получения указателя на текущий элемент, что позволяет изменять его напрямую:

var numbers = [_]i32{ 1, 2, 3 };
for (numbers) |*n| {
n.* *= 2;
}
// numbers теперь { 2, 4, 6 }

Поскольку Zig не допускает использования неинициализированной памяти, все переменные должны быть явно инициализированы. Это требование распространяется и на управляющие конструкции: нельзя объявить переменную внутри if и использовать её вне, если обе ветки не гарантируют её инициализацию.

// Это не скомпилируется:
var x: i32;
if (condition) {
x = 10;
}
// использование x здесь запрещено, если нет else-ветки

Компилятор строго отслеживает поток инициализации, что исключает ошибки использования мусора.


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

Идиоматический код на Zig часто следует определённым паттернам, которые делают его предсказуемым и безопасным.


Паттерн "early return"

Вместо глубокой вложенности условий Zig поощряет досрочный возврат при ошибках:

fn handleRequest(req: Request) !Response {
if (req.method != .GET) {
return error.MethodNotAllowed;
}
if (req.path.len == 0) {
return error.InvalidPath;
}
// основная логика
return buildResponse(req);
}

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


Паттерн "optional unwrapping"

Для работы с опциональными значениями (?T) Zig использует if с деструктуризацией:

if (maybe_value) |value| {
// value имеет тип T
} else {
// значение отсутствует
}

Разбор:

  • maybe_value имеет тип ?T (опциональное значение).
  • В ветке |value| компилятор знает, что value точно присутствует.
  • Ветка else обрабатывает null без риска разыменования пустого значения.

Это заменяет собой проверки на null и исключает разыменование пустых указателей.


Паттерн "error union handling"

Аналогично, ошибки обрабатываются через if или switch:

const result = riskyOperation();
if (result) |value| {
use(value);
} else |err| {
logError(err);
}

Разбор:

  • Для error union !T ветка |value| — успех, ветка |err| — ошибка.
  • Один if заменяет связку "проверить код возврата + отдельно обработать ошибку".
  • Компилятор требует обе ветки, что снижает риск забыть обработку сбоя.

Такой подход делает обработку ошибок явной и обязательной.


Сравнение с другими языками

В отличие от C, Zig не допускает "проваливания" в switch, не требует break и обеспечивает полный охват вариантов. В отличие от Rust, Zig не использует шаблоны сопоставления с образцом, но достигает аналогичной выразительности через if и switch с деструктуризацией. В отличие от Go, Zig не имеет goto, но предоставляет более мощные метки с break и continue.

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


Частые ошибки и быстрые исправления

ОшибкаПочему возникаетКак исправить
Использование числа как условияВ Zig условие только boolЯвно сравнивайте: if (x != 0)
Смешение i32 и u32 в операцииНет неявных приведенийПриводите тип через @intCast
Ожидание fallthrough в switchВ Zig его нетГруппируйте кейсы явно
Игнорирование !TError union требует обработкиИспользуйте try/catch/if

Связанные статьи

Содержание