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 улетят к вызывающему сами
}