Опциональные типы вместо null

Учимся выражать «значения может не быть» через ?T и избегать разыменования null.

Опциональный тип ?T — это либо значение типа T, либо специальное null; компилятор не даст использовать значение, пока вы явно не проверили, что оно есть.

«Ошибка на миллиард долларов» — так Тони Хоар назвал изобретение null. В C любой указатель может оказаться NULL, и разыменование роняет программу. Zig устраняет это: обычный указатель *T никогда не бывает null. Если значение может отсутствовать, тип явно помечается вопросом: ?*T или ?i32.

Объявление опционального значения

var maybe: ?i32 = 42;
maybe = null;        // допустимо: тип опциональный
var required: i32 = 42;
// required = null;  // ОШИБКА компиляции: i32 не опционален

Тип ?i32 вмещает либо целое, либо null. А обычный i32 присвоить null нельзя — компилятор запрещает. Это и есть безопасность: невозможность null там, где его не ждут, гарантирована типами.

Распаковка через if-захват

const std = @import("std");

pub fn main() void {
    const maybe: ?i32 = 100;
    if (maybe) |value| {
        std.debug.print("есть значение: {d}\n", .{value});
    } else {
        std.debug.print("значения нет\n", .{});
    }
}

Вывод:

есть значение: 100

Конструкция if (maybe) |value| проверяет, что значение есть, и «захватывает» его в переменную value уже без вопроса — тип i32. Внутри блока вы работаете с гарантированно существующим значением. Если бы было null, выполнилась бы ветка else.

Оператор orelse

const maybe: ?i32 = null;
const value = maybe orelse 0; // если null — берём 0
// value == 0

Оператор orelse подставляет значение по умолчанию, когда опционал пуст. Это компактная альтернатива if/else. А если вы абсолютно уверены, что значение есть, его можно «насильно» распаковать через maybe.? — но при null это паника, поэтому используют осторожно.

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

Для опционального указателя Zig применяет умную оптимизацию: ?*T занимает столько же байт, сколько обычный указатель, и значение null кодируется нулевым адресом. То есть безопасность достаётся бесплатно, без расходов памяти. Для значений вроде ?i32 добавляется один байт-флаг «есть/нет».

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

Главная — привычка из C считать любой указатель потенциально нулевым. В Zig это не так: если хотите «может быть null», объявляйте ?*T явно. Вторая ошибка — злоупотреблять .? для распаковки: при null это аварийное завершение. Безопаснее if-захват или orelse.

Итог

  • Обычный указатель *T никогда не бывает null — это гарантия типа.
  • ?T явно выражает «значение может отсутствовать».
  • Распаковка: if (x) |v| { ... } или x orelse default.
  • ?*T занимает столько же памяти, сколько обычный указатель.
Проверьте себя
1. Может ли обычный указатель *T в Zig быть null?
AДа, как в C
BНет, для возможного null нужен опциональный тип ?*T
CТолько в release-сборке
DТолько если он const
2. Что делает оператор orelse в выражении x orelse 0?
AСкладывает x и 0
BПодставляет 0, если x равен null
CВсегда возвращает 0
DБросает ошибку при null