Типы данных и управление памятью
Дальше: Справочник Zig · Основы языка Zig (базовые структуры и работа с данными)
Типы данных и управление памятью
Этот раздел лучше читать с практической целью — после него вы сможете безопасно передавать данные между функциями, не терять владение памятью и не путаться между массивом, срезом и указателем.
В Zig именно типы и память определяют надёжность программы. Когда эти основы понятны, остальные темы языка воспринимаются заметно легче.
Play ITЗагрузка интерактивного демо…
Переменные — объявление и семантика
В Zig каждая переменная должна быть явно объявлена. Язык не допускает неявного создания переменных при первом использовании. Объявление начинается с ключевого слова var для изменяемых переменных или const для неизменяемых значений. Эта простота отражает философию Zig — всё, что может быть константой, должно быть константой. Изменяемость — это исключение, а не правило.
const x = 42;
var y = 10;
Разбор:
const x = 42;объявляет неизменяемое значение: повторное присваиваниеxкомпилятор запретит.var y = 10;создаёт изменяемую переменную, которую можно обновлять в пределах области видимости.- Оба типа выводятся автоматически (
comptime_intс дальнейшей конкретизацией по контексту), но остаются строго типизированными.
Здесь x — неизменяемое значение, установленное один раз и защищённое от последующих модификаций. y — изменяемая переменная, которую можно переприсваивать в рамках её области видимости.
Zig не требует указания типа при объявлении, если компилятор может вывести его из значения. Однако тип всегда существует и строго проверяется на этапе компиляции. Это статическая типизация без необходимости избыточного синтаксиса.
const message = "Hello, Zig!";
// Тип message — [*:0]const u8 (нуль-терминированная строка)
Разбор:
- Строковый литерал хранится как последовательность байтов UTF-8 с завершающим
\0(sentinel). - Тип
[*:0]const u8полезен для C-совместимых API, ожидающих нуль-терминированную строку. constфиксирует ссылку на литерал и предотвращает его модификацию.
Если требуется явное указание типа, оно записывается после имени переменной через двоеточие:
var counter: u32 = 0;
const pi: f64 = 3.141592653589793;
Разбор:
- Явная аннотация
u32задаёт диапазон беззнакового 32-битного числа. f64выбирают для большей точности вычислений с плавающей точкой.- Пример показывает, как аннотации улучшают читаемость там, где автоматический вывод может быть неочевиден.
Такой подход позволяет сочетать удобство вывода типов с точностью аннотаций, когда это необходимо для читаемости или совместимости.
Play ITЗагрузка интерактивного демо…
Целочисленные типы
Zig предлагает полный набор целочисленных типов, каждый из которых имеет чётко определённый размер и знаковость. Все целочисленные типы в Zig начинаются с буквы i (для знаковых) или u (для беззнаковых), за которой следует количество бит — 8, 16, 32, 64, 128.
Примеры:
u8— беззнаковое целое 8 бит (диапазон от 0 до 255)i32— знаковое целое 32 бита (от -2 147 483 648 до 2 147 483 647)u128— беззнаковое целое 128 бит, способное хранить очень большие значения
Эти типы не являются платформозависимыми. u32 всегда занимает ровно 32 бита, независимо от архитектуры процессора. Это устраняет неопределённость, характерную для таких языков, как C, где int может менять размер в зависимости от системы.
Zig также предоставляет псевдонимы usize и isize, которые соответствуют размеру указателя на текущей платформе. usize используется для индексации, размеров массивов и вычислений, связанных с адресацией памяти.
const buffer_size: usize = 1024;
var index: usize = 0;
Разбор:
usizeсоответствует разрядности платформы и подходит для индексов/длин.- Значение такого типа безопаснее использовать при адресной арифметике и работе со срезами.
- Комбинация
constдля фиксированной длины иvarдля курсора итерации отражает идиоматичный стиль Zig.
Числа с плавающей точкой
Для представления дробных чисел Zig использует стандартные типы IEEE 754 — f16, f32, f64, f128. На практике чаще всего применяются f32 (одинарная точность) и f64 (двойная точность).
const temperature: f32 = -12.5;
const gravity: f64 = 9.80665;
Разбор:
f32занимает 32 бита и подходит, когда важна экономия памяти и допустима меньшая точность.f64— стандартный выбор для научных и инженерных расчётов с двойной точностью.- Оба типа следуют IEEE 754, поэтому поведение переполнения и спецзначений (
inf,nan) предсказуемо на разных платформах.
Zig не поддерживает неявных преобразований между целыми и вещественными типами. Любое приведение должно быть явным, что предотвращает случайные потери точности или неожиданное поведение.
const a: i32 = 10;
const b: f64 = @floatFromInt(a); // Явное преобразование
Разбор:
- Zig запрещает неявное преобразование
i32->f64, поэтому используется встроенная функция@floatFromInt. - Такой явный вызов документирует намерение и исключает "тихие" изменения типа.
- Компилятор проверяет корректность преобразования на этапе сборки/выполнения в зависимости от контекста.
Функция @floatFromInt — одна из встроенных функций времени компиляции, обеспечивающих безопасное и контролируемое преобразование.
Логический тип
Логический тип в Zig называется bool и принимает два значения: true и false. Он используется в условиях, циклах и логических выражениях.
const is_ready = true;
var is_valid: bool = false;
Разбор:
const is_readyфиксирует логическое значение, которое нельзя переприсвоить.var is_validдопускает смену состояния в ходе выполнения программы.- В условиях
ifи циклах требуется именноbool, а не "любое ненулевое число".
Любое значение другого типа нельзя напрямую использовать как логическое. Например, число 0 не эквивалентно false. Это исключает распространённые ошибки, связанные с неявной интерпретацией значений.
Символы и строки
Zig не имеет отдельного типа для одиночного символа вроде char в C. Вместо этого символы представляются как целые числа, соответствующие кодовой точке Unicode. Чаще всего используется тип u21, так как он способен вместить любую кодовую точку UTF-8 (максимум 21 бит).
const euro_sign: u21 = 0x20AC; // €
Разбор:
u21вмещает любую кодовую точку Unicode (до U+10FFFF).- Литерал
0x20AC— шестнадцатеричная запись кода символа €. - В выводе символ кодируется в UTF-8 уже на этапе форматирования или записи в буфер.
Строки в Zig — это неизменяемые последовательности байтов в кодировке UTF-8. Они представлены как указатели на нуль-терминированные массивы ([*:0]const u8) или как срезы ([]const u8). Первый вариант используется для строковых литералов, второй — для динамических строк.
const greeting = "Привет, мир!"; // Тип: [*:0]const u8
const dynamic_text: []const u8 = "временный текст";
Разбор:
greeting— литерал с нуль-терминатором, удобный для C API и констант.dynamic_text— срез фиксированной длины без зависимости от\0в середине данных.- Оба варианта read-only; для записи нужен отдельный буфер
[]u8в выделенной памяти.
Строки в Zig не являются объектами высокого уровня с методами. Работа со строками осуществляется через функции из стандартной библиотеки, такие как std.mem.eql для сравнения или std.fmt.bufPrint для форматирования.
Три представления текста
| Форма | Пример типа | Когда использовать |
|---|---|---|
| Строковый литерал | [*:0]const u8 / [:0]const u8 | Константы, C-совместимые API |
| Срез байт | []const u8 | Аргументы функций, парсинг, I/O |
| Изменяемый буфер | []u8 | Запись после allocator.alloc |
Литерал хранит UTF-8 и завершается \0. Срез — пара "указатель + длина"; он не владеет памятью. Для динамической строки выделяют []u8, заполняют и при необходимости передают как []const u8.
Интерактив ниже — на JavaScript; в Zig те же идеи: массив фиксированного размера (
[N]T) и срез ([]T) как вид на память.
Play ITЗагрузка интерактивного демо…
Массивы и срезы
Массив в Zig — это фиксированная по размеру коллекция элементов одного типа. Размер массива является частью его типа, что делает массивы в Zig строго типизированными.
var numbers: [5]i32 = [_]i32{ 1, 2, 3, 4, 5 };
Разбор:
[5]i32— фиксированный массив, где длина является частью типа.[_]i32{ ... }позволяет вывести длину автоматически по числу элементов.- Такой контейнер хранится непрерывно в памяти и эффективен для предсказуемого доступа по индексу.
Здесь [5]i32 — тип массива из пяти 32-битных целых. Конструкция [_]i32{ ... } позволяет компилятору вывести размер из количества элементов.
Срез (slice) — это динамическое представление части массива или другой последовательности. Он состоит из указателя на начало данных и длины. Срезы не владеют данными, а лишь ссылаются на них.
const slice: []const i32 = &numbers[1..4]; // {2, 3, 4}
Разбор:
numbers[1..4]создаёт диапазон по полуинтервалу: включается индекс1, исключается4.&берёт ссылку на этот диапазон, формируя срез[]const i32(указатель + длина).- Срез не копирует данные и не владеет памятью исходного массива.
Срезы играют центральную роль в передаче данных между функциями, так как они не копируют содержимое и не зависят от исходного размера массива.
Динамический список — std.ArrayList(T) (нужен аллокатор):
| Действие | Метод |
|---|---|
| Добавить в конец | append(allocator, &list, item) или list.append(item) |
| Прочитать | list.items[i] |
| Вставить | insert(i, item) |
| Удалить | orderedRemove(i) / swapRemove(i) |
Фиксированный массив [N]T и срез []T: только slice[i] и итерация; рост — через ArrayList.
const std = @import("std");
pub fn demoList(allocator: std.mem.Allocator) !void {
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
try list.append(10);
try list.append(20);
std.debug.print("Длина: {}, первый: {}\n", .{ list.items.len, list.items[0] });
}
Разбор:
ArrayList(i32)хранит элементы в динамически растущем буфере, управляемом аллокатором.defer list.deinit()освобождает внутренний буфер списка при выходе из функции.appendдобавляет элемент в конец;list.items— срез на текущие данные для чтения и итерации.
Интерактив ниже — на JavaScript; в Zig ассоциативные структуры строят через
std.HashMapи comptime.
Play ITЗагрузка интерактивного демо…
Операции со словарём (std.AutoHashMap)
| Действие | Пример |
|---|---|
| Добавить или заменить | try map.put(key, value) |
| Прочитать | map.get(key) |
| Удалить | _ = map.remove(key) |
Операции с множеством (std.AutoHashMap(T, void) или std.BufSet)
| Действие | Пример |
|---|---|
| Добавить | try set.put(value, {}) |
| Удалить | _ = set.remove(value) |
| Проверить наличие | set.contains(value) |
const std = @import("std");
pub fn demoMap(allocator: std.mem.Allocator) !void {
var map = std.StringHashMap(i32).init(allocator);
defer map.deinit();
try map.put("alice", 1);
try map.put("bob", 2);
if (map.get("alice")) |score| {
std.debug.print("alice = {}\n", .{score});
}
}
Разбор:
StringHashMap(i32)связывает строковые ключи с целочисленными значениями.putдобавляет или заменяет пару ключ–значение.getвозвращает?i32; конструкцияif (map.get(...)) |score|безопасно распаковывает результат.
Структуры
Структуры (struct) — основной способ группировки связанных данных. Каждое поле структуры имеет имя и тип.
const Point = struct {
x: f64,
y: f64,
};
const origin = Point{ .x = 0.0, .y = 0.0 };
Разбор:
structгруппирует связанные поля в единый пользовательский тип.- Инициализация по именам (
.x,.y) повышает устойчивость к будущему изменению порядка полей. originсоздаётся как неизменяемый экземпляр, пригодный для безопасной передачи без мутаций.
Поля инициализируются по именам, что повышает читаемость и устойчивость к изменениям порядка полей. Структуры могут содержать методы, объявленные внутри тела структуры как функции, принимающие self.
const std = @import("std");
const Circle = struct {
center: Point,
radius: f64,
fn area(self: Circle) f64 {
return std.math.pi * self.radius * self.radius;
}
};
const unit_circle = Circle{ .center = origin, .radius = 1.0 };
const area = unit_circle.area();
Разбор:
Circleвложенно используетPointкак полеcenter, демонстрируя композицию структур.- Метод
area(self: Circle)вычисляет площадь по формуле πr² черезstd.math.pi. - Вызов
unit_circle.area()— синтаксический сахар для передачи структуры первым аргументом.
Указатели
Zig предоставляет прямой контроль над памятью через указатели. Указатель объявляется с помощью звёздочки перед типом. Существуют два основных вида указателей:
*T— указатель на одно значение типаT[*]T— указатель на первый элемент массива неизвестной длины
var value: i32 = 100;
var ptr: *i32 = &value; // Адрес переменной
ptr.* = 200; // Разыменование и присваивание
Разбор:
&valueберёт адрес переменной и формирует указатель*i32.ptr.*выполняет разыменование: доступ к реальному значению по адресу.- Присваивание через указатель изменяет исходную переменную
valueв памяти.
Указатель *T не может быть null — для nullable используют ?*T. Компилятор отслеживает инициализацию и область видимости, но не проверяет "висячие" указатели после free так же строго, как borrow checker в Rust: корректность lifetime — ответственность разработчика.
var maybe_ptr: ?*i32 = null;
maybe_ptr = &value;
if (maybe_ptr) |p| {
p.* = 300;
}
Эта система исключает разыменование нулевых указателей на этапе компиляции или делает его явным и контролируемым.
Опциональные типы
Zig не использует null как универсальное значение для всех типов. Вместо этого он вводит опциональные типы, обозначаемые вопросительным знаком: ?T. Значение такого типа либо содержит значение T, либо равно null.
const maybe_number: ?i32 = 42;
const no_number: ?i32 = null;
Работа с опциональными типами требует явной проверки. Это достигается через if с распаковкой:
if (maybe_number) |n| {
std.debug.print("Число: {}\n", .{n});
} else {
std.debug.print("Нет числа\n", .{});
}
Разбор:
- Конструкция
if (maybe_number) |n|распаковывает?Tтолько если внутри есть значение. - В ветке
elseобрабатываетсяnull, поэтому "пустой" сценарий тоже явно покрыт. - Такой паттерн исключает случайное использование отсутствующего значения.
Такой подход гарантирует, что разработчик никогда не забудет обработать отсутствие значения.
Перечисления
Перечисления (enum) в Zig — это типы, состоящие из конечного набора именованных значений. Каждое значение имеет уникальное имя и может быть связано с целочисленным представлением.
const Color = enum {
red,
green,
blue,
};
const background = Color.blue;
По умолчанию значения перечисления имеют тип usize, но можно указать другой целочисленный тип:
const HttpStatus = enum(u16) {
ok = 200,
not_found = 404,
internal_server_error = 500,
};
Разбор:
enum(u16)задаёт базовый целочисленный тип для значений перечисления.- Явные числа (
200,404,500) позволяют напрямую сопоставлять enum с HTTP-кодами. - Имена вариантов остаются типобезопасной альтернативой "магическим числам" в коде.
Перечисления могут содержать методы, что делает их мощным инструментом для моделирования состояний и вариантов.
Объединения и алгебраические типы
Zig предоставляет мощный механизм для представления альтернативных состояний — объединения (union). Объединение может содержать значение одного из нескольких возможных типов, но не более одного одновременно. Это делает его идеальным инструментом для моделирования вариативных данных, таких как результат операции (успех или ошибка), разные виды сообщений в системе или различные формы геометрических фигур.
const Shape = union(enum) {
circle: f64,
rectangle: struct { width: f64, height: f64 },
triangle: struct { a: f64, b: f64, c: f64 },
};
Разбор:
- Каждое поле (
circle,rectangle,triangle) — отдельный вариант с собственным типом данных. - Вложенные анонимные
structпозволяют хранить несколько связанных чисел без отдельного имени типа. - Тег
enumгарантирует, что в памяти одновременно активен только один вариант.
Здесь Shape — это объединение с тегом, заданным через enum. Такой подход называется тегированным объединением и гарантирует, что компилятор всегда знает, какой вариант активен. Это исключает ошибки интерпретации памяти и делает код безопасным без дополнительных проверок.
Для работы с объединениями используется синтаксис switch, который обязан обрабатывать все возможные варианты:
const std = @import("std");
fn area(shape: Shape) f64 {
return switch (shape) {
.circle => |r| std.math.pi * r * r,
.rectangle => |rect| rect.width * rect.height,
.triangle => |t| blk: {
const s = (t.a + t.b + t.c) / 2.0;
break :blk std.math.sqrt(s * (s - t.a) * (s - t.b) * (s - t.c));
},
};
}
Разбор:
union(enum)создаёт тегированное объединение: в каждый момент активен ровно один вариант.switch (shape)обязан покрыть все ветки (.circle,.rectangle,.triangle), что даёт исчерпывающую проверку.- Ветви с
|r|,|rect|,|t|распаковывают полезные данные конкретного варианта. - Блок
blkв треугольнике использует именованныйbreak :blkдля возврата промежуточного результата из локального блока.
Каждый случай в switch распаковывает соответствующее значение, обеспечивая прямой доступ к данным без приведений или проверок времени выполнения. Такой подход сочетает выразительность и производительность, характерные для системных языков.
Пользовательские типы и псевдонимы
Zig позволяет создавать новые имена для существующих типов с помощью ключевого слова const. Это лишь вводит удобное имя, улучшающее читаемость.
const UserId = u64;
const TemperatureCelsius = f32;
var user_id: UserId = 1001;
var temp: TemperatureCelsius = -5.3;
Хотя UserId и u64 взаимозаменяемы, использование семантически значимых имён помогает избежать путаницы между разными сущностями, которые технически имеют одинаковое представление.
Для создания действительно новых типов, несовместимых с исходными, используется struct с одним полем:
const SafeString = struct {
data: []const u8,
};
Такой подход применяется в системах, где важна строгая типизация даже для внешне похожих данных, например, различие между строкой имени пользователя и строкой пароля.
Управление памятью и время компиляции
Одной из отличительных черт Zig является глубокая интеграция времени компиляции и времени выполнения. Многие операции, обычно выполняемые во время выполнения в других языках, могут быть перенесены на этап компиляции. Это достигается с помощью ключевого слова comptime.
Переменные, объявленные в контексте comptime, существуют только во время компиляции и не занимают места в исполняемом файле:
const max_buffer_size = comptime blk: {
var size = 1024;
if (@import("builtin").cpu.arch == .x86_64) {
size *= 2;
}
break :blk size;
};
Здесь значение max_buffer_size вычисляется один раз при сборке, и результат встраивается в код как константа. Это позволяет адаптировать программу под целевую платформу без потери производительности.
Функции также могут быть помечены как comptime, что означает, что они вызываются только во время компиляции:
fn arrayFromComptime(comptime T: type, comptime len: usize) [len]T {
return [_]T{0} ** len;
}
const zeroes = arrayFromComptime(i32, 10); // [0, 0, ..., 0]
Разбор:
comptime T: typeиcomptime len: usizeтребуют, чтобы тип и длина были известны на этапе компиляции.- Возвращаемый тип
[len]Tзависит от параметра длины и формируется компилятором статически. [_]T{0} ** lenсоздаёт массив, повторяя значение0lenраз.- Вызов
arrayFromComptime(i32, 10)генерирует конкретный массив[10]i32без runtime-накладных расходов.
Такой подход лежит в основе метапрограммирования в Zig и заменяет макросы, шаблоны и другие механизмы, используемые в C++ или Rust.
Тип void и отсутствие значения
Тип void в Zig представляет отсутствие значения. Он занимает ноль байт и используется в качестве возвращаемого типа функций, которые ничего не возвращают:
const std = @import("std");
fn log(message: []const u8) void {
std.debug.print("{s}\n", .{message});
}
Разбор:
- Возвращаемый тип
voidозначает "полезного значения нет", функция выполняет только побочный эффект (вывод). - Параметр
[]const u8принимает строку как срез байтов без копирования. - Такие функции удобны для логирования и коротких утилит без результата вычисления.
Переменные типа void допустимы, но редко используются:
const nothing: void = {};
Разбор:
{}— пустой литерал единственного значения типаvoid.- Переменная
nothingзанимает 0 байт в рантайме и используется крайне редко. - Пример показывает, что даже "пустое" значение в Zig остаётся частью строгой типовой модели.
Этот подход унифицирует модель типов: каждая функция возвращает значение, даже если это значение пустое. Это упрощает анализ кода и исключает специальные случаи.
Тип type и рефлексия
В Zig существует встроенный тип type, представляющий сам тип как значение. Это позволяет писать обобщённый код, работающий с произвольными типами:
fn printType(comptime T: type) void {
@compileLog("Обрабатывается тип: ", @typeName(T));
}
printType(i32); // Вывод во время компиляции
Разбор:
- Параметр
comptime T — typeпринимает сам тип, а не значение (i32,f64,[]u8и т.д.). @typeName(T)возвращает строковое имя типа для диагностики.@compileLogпечатает сообщение в лог компилятора, не в runtime-консоль.
Функция @typeName возвращает человекочитаемое имя типа, а @compileLog выводит сообщение в лог компилятора. Такие инструменты позволяют создавать гибкие и отлаживаемые системы без необходимости в отдельных препроцессорах.
Практический пример — безопасная работа с входными данными
Рассмотрим задачу парсинга пользовательского ввода, где число может быть представлено как целое или дробное. Используем объединение и строгую типизацию:
Код ITЗагрузка примера кода…
Разбор:
parseIntпытается разобрать целое; при успехе возвращается вариант.integer.- Если целое не подошло, вызывается
parseFloatи активируется вариант.floating. else |_|игнорирует конкретную ошибку парсера и переходит к следующей стратегии илиerror.InvalidNumber.switch (result)обязан обработать оба варианта union, иначе компилятор выдаст ошибку.
Этот код гарантирует, что результат всегда корректен, а обработка каждого случая обязательна. Ошибки явно выражены через систему ошибок Zig, а не скрыты в неопределённых состояниях.
Заключение
Типы данных и переменные в Zig образуют основу языка, построенную на предсказуемости, контроле и минимализме. Каждый элемент — от простого u8 до сложного тегированного объединения — служит конкретной цели и не скрывает деталей реализации. Переменные чётко разделяются на изменяемые и неизменяемые, преобразования типов всегда явны, а управление памятью остаётся в руках разработчика, но без риска неопределённого поведения.
Эта система не усложняет разработку, а, напротив, снижает когнитивную нагрузку — то, что работает на малом примере, масштабируется на большие проекты без изменения принципов. Zig не стремится скрыть машину за абстракциями — он помогает программисту понимать её лучше.
Связанные статьи
- Основы языка Zig — общая модель языка и базовые примеры.
- Управляющие конструкции и операторы Zig — как типы связаны с потоком выполнения.
- Функции и время компиляции — использование типов в
comptime-обобщениях.