LEARN X · ЗА 17 МИН

Zig

Zig за 17 минут: весь язык на одной странице в комментариях кода — типы, опционалы, ошибки !T, allocator, defer, comptime, указатели.

Zig — современный системный язык, претендующий на замену C: без скрытого потока управления, без скрытых аллокаций, без макросов. Память — ваша забота (явный allocator), ошибки — часть типа (!T), а магия метапрограммирования делается обычным кодом на этапе компиляции (comptime). Ниже — весь язык одной страницей: всё объяснение живёт в комментариях рабочего кода (Zig 0.13+).

1. Структура программы

// Однострочный комментарий начинается с //
// Многострочных комментариев в Zig НЕТ — только // на каждой строке.

// Подключаем стандартную библиотеку как обычную константу.
const std = @import("std");

// pub fn main — точка входа. void = ничего не возвращает.
pub fn main() void {
    // std.debug.print(формат, .{аргументы}) — печать в stderr.
    // {s} — строка, {d} — число; .{...} — кортеж аргументов (anonymous struct).
    std.debug.print("Привет, {s}!\n", .{"Zig"}); // => Привет, Zig!
    std.debug.print("2 + 2 = {d}\n", .{2 + 2});   // => 2 + 2 = 4
}

2. Переменные и типы

const неизменяема, var изменяема. Неиспользуемые переменные — ошибка компиляции.

const std = @import("std");

pub fn main() void {
    // const — значение нельзя переприсвоить.
    const pi: f64 = 3.14159;

    // var — можно менять; тип выводится из значения.
    var count: i32 = 10;
    count += 5; // 15

    // Целые: i8/i16/i32/i64/i128, беззнаковые u8/u16/.../usize.
    const byte: u8 = 255;          // 0..255
    const big: i64 = -9000000000;  // знаковое 64-битное

    // Размер под платформу: usize (индексы), isize.
    const idx: usize = 0;

    // bool и числа с плавающей точкой.
    const ready: bool = true;
    const ratio: f32 = 0.5;

    // _ = x; — подавить "unused" для демонстрации.
    _ = .{ pi, byte, big, idx, ready, ratio };
    std.debug.print("count = {d}\n", .{count}); // => count = 15
}

3. Операторы и условия

const std = @import("std");

pub fn main() void {
    // Арифметика: + - * / %  (целочисленное переполнение = паника в debug).
    const a = 7;
    const b = 3;
    std.debug.print("{d} {d} {d}\n", .{ a / b, a % b, a * b }); // => 2 1 21

    // Сравнения: == != < > <= >= ; логика: and / or / !
    const ok = (a > b) and (b != 0);
    std.debug.print("ok = {}\n", .{ok}); // => ok = true

    // if — это ВЫРАЖЕНИЕ, возвращает значение (аналог тернарного оператора).
    const max = if (a > b) a else b;
    std.debug.print("max = {d}\n", .{max}); // => max = 7

    // switch — без проваливания (no fallthrough), должен покрыть все случаи.
    const grade: u8 = 4;
    const text = switch (grade) {
        5 => "отлично",
        4 => "хорошо",
        3 => "удовлетворительно",
        else => "неуд", // else обязателен, если перечислены не все значения
    };
    std.debug.print("оценка: {s}\n", .{text}); // => оценка: хорошо

    // В switch можно объединять и задавать диапазоны.
    const n = 42;
    const size = switch (n) {
        0 => "ноль",
        1...9 => "маленькое",      // диапазон ...
        10, 20, 30 => "круглое",   // несколько значений
        else => "большое",
    };
    std.debug.print("{s}\n", .{size}); // => большое
}

4. Циклы

const std = @import("std");

pub fn main() void {
    // while с условием и опциональным шагом : (i += 1)
    var i: usize = 0;
    var sum: usize = 0;
    while (i < 5) : (i += 1) {
        sum += i;
    }
    std.debug.print("sum = {d}\n", .{sum}); // => sum = 10

    // for проходит по срезам/массивам; |item| — захват значения.
    const nums = [_]i32{ 10, 20, 30 };
    var total: i32 = 0;
    for (nums) |x| {
        total += x;
    }
    std.debug.print("total = {d}\n", .{total}); // => total = 60

    // for с индексом: второй "диапазон" 0.. даёт индекс.
    for (nums, 0..) |x, idx| {
        std.debug.print("nums[{d}] = {d}\n", .{ idx, x });
    }

    // break / continue с МЕТКАМИ — управление вложенными циклами.
    outer: for (0..3) |r| {
        for (0..3) |c| {
            if (r + c == 3) break :outer; // выйти сразу из внешнего цикла
            std.debug.print("({d},{d}) ", .{ r, c });
        }
    }
    std.debug.print("\n", .{});

    // while тоже умеет возвращать значение через break value.
    var k: usize = 0;
    const found = while (k < 100) : (k += 1) {
        if (k * k > 50) break k; // вернуть k из цикла
    } else 0; // else — если цикл завершился без break
    std.debug.print("found = {d}\n", .{found}); // => found = 8
}

5. Массивы и срезы

const std = @import("std");

pub fn main() void {
    // Массив фиксированной длины: [N]T. [_] — длину выведет компилятор.
    var arr = [_]i32{ 1, 2, 3, 4, 5 };
    std.debug.print("len = {d}\n", .{arr.len}); // => len = 5

    // Срез (slice) — указатель + длина: []T. Окно поверх массива.
    const mid: []i32 = arr[1..4]; // элементы с 1 по 3 (4 не входит)
    std.debug.print("mid[0] = {d}, len = {d}\n", .{ mid[0], mid.len }); // => 2, 3

    // Срез ссылается на ту же память — меняем через срез, меняется массив.
    mid[0] = 99;
    std.debug.print("arr[1] = {d}\n", .{arr[1]}); // => arr[1] = 99

    // Строки — это срезы байт: []const u8 (UTF-8).
    const name: []const u8 = "Zig";
    std.debug.print("первый байт: {d}\n", .{name[0]}); // => 90 (код 'Z')

    // ** повторяет массив, ++ конкатенирует (на этапе компиляции).
    const zeros = [_]u8{0} ** 3; // {0,0,0}
    std.debug.print("zeros.len = {d}\n", .{zeros.len}); // => 3
}

6. Опциональные типы и ошибки

Фишка Zig: «нет значения» (?T) и «ошибка» (!T) закодированы прямо в типе — никаких null-указателей и исключений.

const std = @import("std");

// ?T — опционал: либо значение T, либо null.
fn firstEven(items: []const i32) ?i32 {
    for (items) |x| {
        if (@mod(x, 2) == 0) return x;
    }
    return null; // не нашли
}

pub fn main() void {
    const data = [_]i32{ 1, 3, 6, 7 };

    // Распаковка опционала через if с захватом |v|.
    if (firstEven(&data)) |v| {
        std.debug.print("чётное: {d}\n", .{v}); // => чётное: 6
    } else {
        std.debug.print("не найдено\n", .{});
    }

    // orelse — значение по умолчанию, если null.
    const odd = [_]i32{ 1, 3, 5 };
    const v = firstEven(&odd) orelse -1;
    std.debug.print("v = {d}\n", .{v}); // => v = -1

    // .? — "развернуть или паника" (если уверены, что не null).
    const sure = firstEven(&data).?;
    std.debug.print("sure = {d}\n", .{sure}); // => sure = 6
}

7. Функции

const std = @import("std");

// fn имя(параметры) ТипВозврата { ... }. Параметры — неизменяемы.
fn add(a: i32, b: i32) i32 {
    return a + b;
}

// "Несколько возвращаемых значений" — вернуть struct (часто анонимный).
fn divmod(a: i32, b: i32) struct { q: i32, r: i32 } {
    return .{ .q = @divTrunc(a, b), .r = @mod(a, b) };
}

// Функции — значения: можно передавать как параметр.
fn apply(f: *const fn (i32, i32) i32, x: i32, y: i32) i32 {
    return f(x, y);
}

pub fn main() void {
    std.debug.print("add = {d}\n", .{add(2, 3)}); // => add = 5

    // Деструктуризация результата-структуры.
    const dm = divmod(17, 5);
    std.debug.print("q={d} r={d}\n", .{ dm.q, dm.r }); // => q=3 r=2

    // Передаём функцию по указателю.
    std.debug.print("apply = {d}\n", .{apply(&add, 10, 20)}); // => apply = 30
}

8. Структуры и перечисления

const std = @import("std");

// struct с полями, значениями по умолчанию и МЕТОДАМИ.
const Point = struct {
    x: f64,
    y: f64 = 0, // значение по умолчанию

    // Метод: первый параметр self — сама структура.
    fn dist(self: Point) f64 {
        return @sqrt(self.x * self.x + self.y * self.y);
    }
};

// enum — перечисление именованных вариантов.
const Color = enum { red, green, blue };

// union(enum) — теговое объединение: хранит ОДИН из вариантов + тип.
const Value = union(enum) {
    int: i64,
    text: []const u8,
};

pub fn main() void {
    const p = Point{ .x = 3, .y = 4 };
    std.debug.print("dist = {d}\n", .{p.dist()}); // => dist = 5

    const c = Color.green;
    std.debug.print("color = {s}\n", .{@tagName(c)}); // => color = green

    // switch по union раскрывает активный вариант.
    const val = Value{ .text = "hi" };
    switch (val) {
        .int => |n| std.debug.print("int {d}\n", .{n}),
        .text => |s| std.debug.print("text {s}\n", .{s}), // => text hi
    }
}

9. Управление памятью

Ключевая идея Zig: аллокаций «из ниоткуда» нет. Память выделяется через явный allocator, а defer гарантирует её освобождение.

const std = @import("std");

// !void — функция может вернуть ошибку (например, нехватку памяти).
fn run(allocator: std.mem.Allocator) !void {
    // alloc(T, n) выделяет срез из n элементов. try — пробросить ошибку.
    const buf = try allocator.alloc(u8, 5);
    // defer выполнится при ВЫХОДЕ из функции — гарантия освобождения.
    defer allocator.free(buf);

    for (buf, 0..) |*byte, i| byte.* = @intCast('A' + i);
    std.debug.print("buf = {s}\n", .{buf}); // => buf = ABCDE

    // errdefer — срабатывает ТОЛЬКО при возврате ошибки (откат при сбое).
    const tmp = try allocator.alloc(u8, 3);
    errdefer allocator.free(tmp); // освободим, лишь если ниже будет error
    defer allocator.free(tmp);    // обычное освобождение в норме
    _ = tmp;
}

pub fn main() !void {
    // GeneralPurposeAllocator отслеживает утечки памяти.
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit(); // в конце сообщит об утечках
    try run(gpa.allocator());
}

10. comptime

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

const std = @import("std");

// Тип — это значение времени компиляции. "Дженерики" = функции от типов.
fn Stack(comptime T: type) type {
    return struct {
        items: [16]T = undefined,
        len: usize = 0,

        fn push(self: *@This(), v: T) void {
            self.items[self.len] = v;
            self.len += 1;
        }
    };
}

// comptime-вычисление: factorial посчитается ещё при компиляции.
fn factorial(comptime n: u64) u64 {
    return if (n <= 1) 1 else n * factorial(n - 1);
}

pub fn main() void {
    // Константа вычислена компилятором, в бинарнике уже лежит число 120.
    const f5 = comptime factorial(5);
    std.debug.print("5! = {d}\n", .{f5}); // => 5! = 120

    // Создаём конкретный тип стек-из-i32 во время компиляции.
    var s = Stack(i32){};
    s.push(7);
    s.push(42);
    std.debug.print("top = {d}, len = {d}\n", .{ s.items[s.len - 1], s.len }); // => top = 42, len = 2
}

11. Указатели

const std = @import("std");

pub fn main() void {
    var x: i32 = 10;

    // *T — указатель на ОДИН элемент. &x — взять адрес. p.* — разыменование.
    const p: *i32 = &x;
    p.* = 20; // меняем x через указатель
    std.debug.print("x = {d}\n", .{x}); // => x = 20

    // *const T — указатель только для чтения.
    const ro: *const i32 = &x;
    std.debug.print("ro = {d}\n", .{ro.*}); // => ro = 20

    // [*]T — "many-item" указатель (адрес начала, без длины) для C-интеропа.
    var arr = [_]i32{ 1, 2, 3 };
    const many: [*]i32 = &arr;
    std.debug.print("many[2] = {d}\n", .{many[2]}); // => many[2] = 3

    // ?*T — ОПЦИОНАЛЬНЫЙ указатель: может быть null (так Zig заменяет NULL).
    var maybe: ?*i32 = null;
    std.debug.print("null? {}\n", .{maybe == null}); // => null? true
    maybe = &x;
    if (maybe) |ptr| std.debug.print("*ptr = {d}\n", .{ptr.*}); // => *ptr = 20
}

12. Обработка ошибок

const std = @import("std");

// error set — именованное множество возможных ошибок.
const MathError = error{ DivByZero, Negative };

// !T = error union: либо T, либо одна из ошибок (тут вывод набора по telu).
fn safeDiv(a: i32, b: i32) MathError!i32 {
    if (b == 0) return MathError.DivByZero; // вернуть ошибку
    return @divTrunc(a, b);
}

fn sqrtInt(n: i32) MathError!i32 {
    if (n < 0) return error.Negative;
    var r: i32 = 0;
    while (r * r <= n) : (r += 1) {}
    return r - 1;
}

pub fn main() void {
    // catch — перехват ошибки со значением по умолчанию.
    const a = safeDiv(10, 0) catch -1;
    std.debug.print("a = {d}\n", .{a}); // => a = -1

    // catch |err| — доступ к самой ошибке.
    _ = safeDiv(10, 0) catch |err| {
        std.debug.print("поймали: {s}\n", .{@errorName(err)}); // => поймали: DivByZero
        return;
    };
}

// try внутри функции, что возвращает !T — проброс ошибки наверх.
fn pipeline() MathError!i32 {
    const half = try safeDiv(100, 2); // 50
    const root = try sqrtInt(half);   // 7
    return root; // ошибки safeDiv/sqrtInt улетят к вызывающему сами
}
Поддержать проект