Error unions и try

Знакомимся с обработкой ошибок Zig — без исключений и без скрытой раскрутки стека.

Error union !T — тип значения, которое либо успешно (типа T), либо является ошибкой; функция явно объявляет, что может вернуть ошибку, и вызывающий обязан её обработать.

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

Объявление функции, которая может ошибиться

const DivError = error{ DivisionByZero };

fn divide(a: i32, b: i32) DivError!i32 {
    if (b == 0) return error.DivisionByZero;
    return @divTrunc(a, b);
}

Тип возврата DivError!i32 означает: «либо i32, либо ошибка из набора DivError». Ошибку возвращают как обычное значение: return error.DivisionByZero. Набор ошибок объявляют через error{...} — это перечисление возможных провалов.

Оператор try — проброс ошибки наверх

fn compute() DivError!i32 {
    const x = try divide(10, 2); // если ошибка — вернуть её из compute
    return x + 1;
}

Оператор try — это сокращение: «попробуй; если получилось — дай значение, если ошибка — немедленно верни её из текущей функции». Он заменяет громоздкую проверку if (err) return err из C. Поток управления при этом полностью явный: try видно в коде на каждом месте, где ошибка может «выпрыгнуть».

Вывод ошибки в наборе

// !i32 без явного набора — Zig сам выведет множество ошибок
fn parse(s: []const u8) !i32 {
    if (s.len == 0) return error.Empty;
    return 0;
}

Если написать просто !i32, Zig сам выведет, какие ошибки может вернуть функция, проанализировав её тело. Это удобно: не нужно вручную перечислять набор, но при желании его можно зафиксировать явно для документации.

Как работает под капотом

Error union физически — это значение плюс компактный код ошибки (целое число). Никакой раскрутки стека, никаких таблиц исключений. Возврат ошибки — это обычный return, такой же дешёвый, как возврат числа. Поэтому в Zig обработка ошибок не вредит производительности и предсказуема — это критично для системного кода.

Частые ошибки

Первая — проигнорировать возможную ошибку: компилятор не даст просто присвоить результат !T переменной типа T без обработки. Вторая — путать try и catch: try пробрасывает ошибку наверх, а catch (следующий урок) обрабатывает на месте. Третья — забыть, что ошибки в Zig — это значения, а не объекты с данными: код ошибки несёт только своё имя.

Итог

  • В Zig нет исключений; ошибка — это значение в типе !T.
  • Набор ошибок объявляют через error{...}, возвращают как error.Name.
  • try expr пробрасывает ошибку из текущей функции, если она возникла.
  • Тип !T без явного набора заставляет компилятор вывести множество ошибок сам.
Проверьте себя
1. Что означает тип возврата функции !i32 в Zig?
AУказатель на i32
BЛибо i32, либо ошибка
CОпциональный i32
DОтрицание числа
2. Что делает оператор try перед вызовом функции?
AЛогирует вызов
BЕсли функция вернула ошибку — немедленно возвращает её из текущей функции
CИгнорирует ошибку
DПовторяет вызов при ошибке