# Глава 4. Поток управления

# Поток управления, циклы и другие формы цифрового доминирования

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

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

К концу этой главы даже опытный программист почувствует, что владение механизмами потока управления в Zig выводит мастерство на новый уровень. Вы сможете принимать решения с точностью, повторять действия эффективно и встречать неопределённость с уверенностью человека, у которого всегда есть план Б (или В, или Г).

В этой главе мы рассмотрим:

  • Принятие решений с помощью if, else и switch (моральный компас вашего кода).
  • Циклы: повторение действий с for и while (ведь делать что-то один раз — скучно).
  • Исчерпывающие switch: элегантная логика с помощью switch (станьте свахой, которую заслуживает ваш код).
  • Необязательные типы: работа с неопределённостью жизни (и кода).

Приготовьтесь! Даже если у вас большой опыт, Zig предлагает интересные повороты в потоках управления, которые обогатят ваш арсенал программиста. Давайте отправимся в путь к мастерству управления судьбой вашего кода.

# Технические требования

Весь код из этой главы можно найти в каталоге Chapter04 нашего репозитория GitHub.

# Принятие решений с помощью if, else и switch (моральный компас вашего кода)

Вы на распутье, дорогой программист. Пора вашему коду принимать решения — важные. Пойдёт ли он по пути «Привет, мир» или по пути ошибки? Условные операторы — это моральный компас, который направляет выполнение программы, принимая решения на основе данных, обстоятельств и капризов пользователя. Давайте разберёмся, как делать выбор в Zig.

В Zig условные операторы — простой способ управлять выполнением программы. Если вы знакомы с C, Go или Python, вам будет легко, но у Zig есть свои особенности.

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

const std = @import("std");

pub fn main() void {
    const number = 10;
    if (number > 5) {
        std.debug.print("Число больше 5.\n", .{});
    } else {
        std.debug.print("Число 5 или меньше.\n", .{});
    }
}

Вот так вы даёте программе власть выбирать свою судьбу. Если бы только жизнь была такой простой!

А что если нужно больше двух вариантов? Здесь на помощь приходит else if. Можно выстроить цепочку условий любой сложности:

const std = @import("std");
pub fn main() void {
    const number = 7;
    if (number > 10) {
        std.debug.print("Число больше 10.\n", .{});
    } else if (number > 5) {
        std.debug.print("Число между 6 и 10.\n", .{});
    } else {
        std.debug.print("Число 5 или меньше.\n", .{});
    }
}

Казалось бы, вы уже освоили скромный if, но Zig умеет удивлять. Почему бы if не быть не только управляющим оператором, но и выражением? В Zig if возвращает значение — это как коллега, который не только делает свою работу, но и берёт чужую.

# if как выражение

Во многих языках для этого используют тернарный оператор. Но Zig не засоряет код символами вроде ? и :. Вместо этого можно использовать if как выражение.

Пример:

const std = @import("std");
pub fn main() void {
    const a: u32 = 5;
    const b: u32 = 4;
    const result = if (a != b) 47 else 3089;
    std.debug.print("Результат: {}\n", .{result});
}

Разберём:

  • Объявляем две константы a и b.
  • Присваиваем result значение с помощью выражения if: if (a != b) 47 else 3089.
  • Условие истинно, результат — 47.
  • Выводим результат.

Такой подход позволяет присваивать значения по условию без загадочной пунктуации. Zig как бы говорит: «Зачем быть кратким, если можно быть понятным?»

# if с булевыми условиями: классика не стареет

Иногда хочется использовать if по старинке — для выполнения блоков кода по условию. В Zig всё работает так, как ожидаешь.

Пример:

const std = @import("std");
pub fn main() void {
    const a: u32 = 5;
    const b: u32 = 4;
    if (a != b) {
        std.debug.print("a не равно b.\n", .{});
    } else if (a == 9) {
        std.debug.print("a почему-то равно 9.\n", .{});
    } else {
        std.debug.print("Ни одно из условий не выполнено.\n", .{});
    }
}

Это тот самый if, который вы знаете и любите. Иногда предсказуемость — благо.

# if с объединениями (union)

if в Zig умеет работать и с объединениями (union types), что позволяет элегантно управлять разными типами значений. Например, с ошибочными объединениями (error unions) можно отличать успешный результат от ошибки. Но это тема для главы 8 «Обработка ошибок». Сейчас важно знать: if в Zig универсален и позволяет писать выразительный код.

Сила и гибкость

Хотя соблазнительно увлечься мощью выражений if, не усложняйте код ради красоты. Читаемость важнее. Будьте последовательны: используйте один стиль управления потоком во всей кодовой базе.

# Элегантный switch: новый уровень принятия решений

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

Вот простой пример использования switch:

const std = @import("std");
pub fn main() void {
    const day = 3;
    switch (day) {
         1 => std.debug.print("Это понедельник.\n", .{}),
         2 => std.debug.print("Это вторник.\n", .{}),
         3 => std.debug.print("Это среда.\n", .{}),
         else => std.debug.print("Это какой-то другой день.\n", .{}),
    }
}

Разберём этот пример:

  • switch оценивает переменную day.
  • Каждое число соответствует дню недели: 1 — понедельник, 2 — вторник и так далее.
  • Блок else выступает в роли универсального обработчика. Если ни один из предыдущих случаев не совпал, он обрабатывает всё остальное — ведь никогда не знаешь, что может произойти.

Если вы закомментируете блок else, компилятор Zig выдаст ошибку:

error: switch must handle all possibilities

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

  • Гарантированная исчерпаемость: в Zig оператор switch обязывает обработать все возможные значения выражения. Если вы не охватите какой-то случай, Zig обнаружит это на этапе компиляции. Многие языки (например, C и Go) этого не требуют, что позволяет оставлять пробелы в логике и приводит к ошибкам. В Zig switch гарантирует, что все варианты учтены — либо явно, либо через блок else, который служит страховкой. Это особенно полезно при работе с перечислениями (enum), где нужно обработать каждое возможное значение.
  • Нет неявного «проваливания» (fallthrough): в C, C++ и даже JavaScript после выполнения одного case выполнение продолжается в следующем, если нет оператора break. Это частая причина ошибок, когда разработчик забывает поставить break. В Zig «проваливания» по умолчанию нет — каждый case изолирован, и вы не выполните следующий блок случайно. Это делает switch в Zig гораздо менее подверженным ошибкам и более предсказуемым.
  • Явное объединение случаев: если в Zig нужно обработать несколько случаев одним блоком кода, вы явно группируете их вместе. Это делает поток управления более прозрачным и предотвращает непреднамеренные последствия, которые могут возникнуть из-за неявного «проваливания» в других языках.

Оператор switch в Zig не только делает код чище, но и безопаснее. Благодаря обязательной исчерпаемости и отсутствию «проваливания» он помогает выявлять потенциальные ошибки на этапе компиляции, а не в 3 часа ночи, когда вы этого меньше всего ожидаете. В Zig switch — это не просто инструмент выбора, а мощный механизм для написания надёжного и предсказуемого кода.

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

Овладев искусством выбора — где ваш код притворяется, что у него есть свобода воли с помощью if, else и всёобъемлющего switch — пора принять неизбежное: повторение одного и того же действия. Ведь, честно говоря, если бы вам не нравились хорошие циклы, вы бы, вероятно, не занимались программированием. Так что стряхните пыль с терпения и приготовьтесь погрузиться в мир циклов в Zig.

# Необязательные типы: потому что жизнь (и код) не всегда определённа

В какой-то момент каждый программист сталкивается с ситуацией, когда значение может быть, а может и не быть. Это может быть отсутствующий ввод пользователя, не пришедшие по сети данные или незавершённое чтение файла. Работа с неопределённостью неизбежна. В Zig для этого используются необязательные типы, которые заставляют явно учитывать возможность отсутствия значения и гарантировать обработку таких случаев. Но Zig идёт дальше — он предлагает дополнительные механизмы, такие как объединения ошибок и паттерны try-catch, чтобы сделать обработку ошибок и безопасность кода ещё надёжнее.

Необязательные типы в Zig — это не просто удобство, а часть философии языка, направленной на безопасность и надёжность. Правильное их использование предотвращает разыменование нулевых указателей и позволяет писать устойчивый, предсказуемый код. Давайте разберёмся, как работают необязательные типы, как они взаимодействуют с обработкой ошибок и как их эффективно применять в крупных проектах.

# Что такое тип ?T?

В Zig необязательный тип (?T) используется для представления значения, которое может быть либо присутствовать, либо быть null. Это предотвращает распространённую в других языках проблему, когда переменная неожиданно оказывается равной null, что приводит к ошибкам разыменования указателя во время выполнения. В Zig обработка возможного отсутствия значения делается явно: вы обязаны признать и обработать этот случай.

Вот базовый пример:

const std = @import("std");
pub fn main() void {
    const maybeValue: ?i32 = 10;
    if (maybeValue) |value| {
        std.debug.print("Число: {}\n", .{value});
    } else {
        std.debug.print("Числа нет.\n", .{});
    }
}

Разберём этот пример:

  • maybeValue — необязательный i32, то есть может содержать число или быть null.
  • Мы явно проверяем наличие значения перед доступом к нему.

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

# Проверка нескольких необязательных значений

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

Zig позволяет элегантно решить эту задачу с помощью логического оператора and для необязательных типов. Это даёт возможность одновременно проверить несколько необязательных переменных и продолжить только если все они содержат значения.

Пример:

var maybeX: ?i32 = 10;
var maybeY: ?i32 = null;
if (maybeX and maybeY) |x, y| {
    const sum = x + y;
    std.debug.print("Сумма: {}\n", .{sum});
} else {
    std.debug.print("Нельзя вычислить сумму: одно из значений отсутствует.\n", .{});
}

Ключевые моменты:

  • Одновременная проверка на null: if (maybeX and maybeY) позволяет проверить сразу несколько необязательных переменных.
  • Безопасное извлечение: если условие истинно, Zig безопасно извлекает оба значения и присваивает их x и y.
  • Эффективный поток управления: операция выполняется только если все необходимые данные есть.

Необязательные типы — это не просто удобство, а инструмент обеспечения безопасности. Явно делая null отдельным типом, Zig предотвращает ошибки, характерные для языков, где null может появиться незаметно.

# Почему это важно для безопасности

  • Нет разыменования нулевого указателя: вы обязаны проверить наличие значения перед доступом.
  • Чёткое намерение: если переменная объявлена как ?T, сразу видно, что она может отсутствовать.
  • Гарантии на этапе компиляции: если вы забудете обработать случай null, компилятор выдаст ошибку.

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

  • Используйте orelse для значений по умолчанию:

    const defaultNumber = maybeNumber orelse 0;
    std.debug.print("Число: {}\n", .{defaultNumber});
  • Обрабатывайте null как можно раньше.
  • Документируйте использование необязательных типов.

# Необязательные типы в циклах

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

Пример:

const std = @import("std");
pub fn main() void {
    const items = [_]?i32{ 1, null, 3, null, 5 };
    var sum: i32 = 0;
    for (items) |item| {
        if (item) |value| {
            sum += value;
        } else {
            std.debug.print("Встретилось значение null.\n", .{});
        }
    }
    std.debug.print("Сумма ненулевых значений: {}\n", .{sum});
}

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

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

# Помеченные switch: когда обычного switch недостаточно

Вы уже знакомы с операторами switch и тем, как они элегантно позволяют вашему коду принимать решения. Но что, если нужно добавить гибкости в управление состояниями? Встречайте — помеченные switch. Эта возможность усиливает обычные switch, позволяя им делать больше: теперь switch можно помечать и переходить между состояниями с помощью continue, как в циклах. Это как switch с характером — больше не нужно возвращаться через громоздкие циклы, можно сразу прыгнуть к следующему состоянию!

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

Вот как это реализуется:

const std = @import("std");
pub fn main() void {
    light: switch (@as(u8, 1)) {
        1 => {
            std.debug.print("Свет красный. Машины стоят.\n", .{});
            continue :light 2;  // Переход к жёлтому
        },
        2 => {
            std.debug.print("Свет жёлтый. Готовьтесь остановиться.\n", .{});
            continue :light 3;  // Переход к зелёному
        },
        3 => {
            std.debug.print("Свет зелёный. Машины едут.\n", .{});
            continue :light 4;  // Переход к пешеходному переходу
        },
        4 => {
            std.debug.print("Пешеходный переход. Машины стоят, люди идут.\n", .{});
            return;  // Конец цикла
        },
    }
}

Этот помеченный switch моделирует простой цикл светофора с явными переходами между состояниями:

  • Состояние 1 (красный): машины стоят, затем переход к жёлтому.
  • Состояние 2 (жёлтый): предупреждение остановиться, затем переход к зелёному.
  • Состояние 3 (зелёный): машины едут, затем переход к пешеходному переходу.
  • Состояние 4 (пешеходный переход): машины стоят, пешеходы идут, затем завершение.

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

Рассмотрим другой пример — торговый автомат. Он принимает деньги, позволяет выбрать напиток и выдаёт его. Помеченный switch позволяет описать все этапы процесса:

const std = @import("std");
pub fn main() void {
    vending: switch (@as(u8, 1)) {
        1 => {
            std.debug.print("Внесите монеты.\n", .{});
            continue :vending 2;  // Переход: монеты внесены
        },
        2 => {
            std.debug.print("Выберите напиток.\n", .{});
            continue :vending 3;  // Переход: напиток выбран
        },
        3 => {
            std.debug.print("Выдача напитка. Приятного!\n", .{});
            return;  // Конец транзакции
        },
        4 => {
            std.debug.print("Транзакция отменена. Возврат монет.\n", .{});
            return;  // Конец транзакции
        },
    }
}

Здесь:

  • Состояние 1: внесение монет → переход ко второму.
  • Состояние 2: выбор напитка → переход к третьему.
  • Состояние 3: выдача напитка → завершение.
  • Состояние 4: отмена транзакции → возврат монет и завершение.

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

Конечный автомат

Если вы когда-либо строили конечный автомат или работали с переходами между состояниями, эта конструкция покажется вам настоящим подарком. С помощью continue :метка новое_состояние вы можете элегантно управлять переходами, делая код читаемым и надёжным.

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

Преимущества:

  • Управление состояниями: идеально подходит для сценариев с чётко определёнными состояниями и переходами.
  • Читаемость: continue :метка делает поток управления прозрачным.
  • Производительность: явные переходы позволяют компилятору Zig оптимизировать выполнение, особенно в горячих циклах или конечных автоматах.

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

Пример традиционного подхода:

const std = @import("std");
pub fn main() void {
    var op: u8 = 1;
    while (true) {
        switch (op) {
            1 => {
                op = 2;
                continue;
            },
            2 => {
                op = 3;
                continue;
            },
            3 => {
                std.debug.print("Операция выполнена успешно.\n", .{});
                return;
            },
            else => {
                std.debug.print("Неизвестная операция.\n", .{});
                break;
            },
        }
    }
    std.debug.print("Выход из цикла.\n", .{});
}

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

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

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

Когда использовать помеченные switch:

  • Для реализации конечных автоматов и диспетчеризации команд.
  • В сложных логиках с многоуровневыми переходами.

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

# Итоги

Посмотрите на себя — вы покорили циклы, условные операторы и switch как настоящий цифровой властелин! Вы научились принимать решения, повторять действия и справляться с неопределённостью жизни (и кода) с помощью необязательных типов. Теперь у вас есть инструменты, чтобы ваш код подчинялся каждой вашей команде.

Но давайте будем честны: хотя управление потоком выполнения — это основа, оно может завести вас только до определённого момента, пока вы не начнёте повторять один и тот же код снова и снова. А это — большое «нет» в мире программирования. Не волнуйтесь: в следующей главе мы поднимемся на новый уровень с помощью функций!

Так что сделайте глубокий вдох, разомнёте пальцы и приготовьтесь создавать ещё более мощные программы. Функции уже ждут!

# Проверка знаний

  1. Какова основная цель оператора if в Zig?
    1. Выполнять код многократно, пока не выполнится условие
    2. Принимать решения на основе условий и выполнять соответствующий код
    3. Перебирать коллекции
    4. Обрабатывать ошибки и исключения

Ответ: b

  1. В чём ключевое отличие оператора switch от цепочки if-else в Zig?
    1. Операторы switch не являются исчерпывающими, а if-else — являются
    2. В switch разрешено «проваливание» (fallthrough) по умолчанию
    3. Оператор switch требует обработать все возможные случаи исчерпывающе
    4. if-else может сравнивать только на равенство, а switch — диапазоны

Ответ: c

  1. Как перебрать массив items в Zig с помощью цикла for?
    1. for i in items
    2. for (items) |item|
    3. foreach item in items
    4. while (items)

Ответ: b

  1. Какой будет вывод следующего кода на Zig?

    const std = @import("std");
    pub fn main() void {
        const number = 7;
        if (number > 10) {
            std.debug.print("Greater than 10\n", .{});
        } else if (number > 5) {
            std.debug.print("Between 6 and 10\n", .{});
        } else {
            std.debug.print("5 or less\n", .{});
        }
    }
    1. "Greater than 10"
    2. "Between 6 and 10"
    3. "5 or less"
    4. Нет вывода

Ответ: b

  1. Как в Zig объявить необязательное целое число, которое может быть null?
    1. var maybeNumber: int = null;
    2. var maybeNumber: ?i32 = null;
    3. var maybeNumber: optional i32 = null;
    4. var maybeNumber: i32? = null;

Ответ: b

  1. Какой оператор в Zig позволяет выполнить выражение в конце каждой итерации цикла while?
    1. break
    2. continue
    3. else
    4. : (expression)

Ответ: d

  1. Что демонстрирует следующий код на Zig? const result = if (a != b) 47 else 3089;
    1. Использование if как оператора
    2. Использование if как выражения, возвращающего значение
    3. Синтаксическая ошибка в Zig
    4. Объявление переменной внутри условия if

Ответ: b

  1. Как в цикле for Zig получить доступ и к значению, и к индексу при переборе массива?
    1. for (items) |index, value|
    2. for (items) |value| и использовать отдельный счётчик
    3. for (items) |value, index|
    4. В for нельзя получить индекс

Ответ: c

  1. Для чего нужен блок else в цикле for Zig?
    1. Выполнить код, если цикл прошёл все итерации без break
    2. Выполнить код, если тело цикла не выполнилось ни разу
    3. Обработать ошибки внутри цикла
    4. Задать действие по умолчанию для несовпавших условий

Ответ: b

  1. Каким образом Zig обеспечивает безопасность в операторах switch?
    1. Разрешая «проваливание» между case
    2. Требуя наличия default
    3. Обязывая обрабатывать все возможные варианты (исчерпаемость)
    4. Ограничивая количество case до трёх

Ответ: c