#
Глава 10: Стандартная библиотека
Думаете, стандартная библиотека — это просто пыльный ящик со старыми инструментами? Подумайте ещё раз. std Zig — это сокровищница подземелья, набитая клинками, зельями и таинственными артефактами — всё это создано для того, чтобы вы прорубались сквозь системное программирование как варвар с учёной степенью по эффективности. К концу этой главы вы будете владеть стандартной библиотекой Zig не как робкий писец, а как закалённый в боях инженер, который обращается с памятью, файлами и потоками так же, как DM (мастер игры) обращается с книгами правил: с безжалостной точностью.
В этой главе мы рассмотрим следующие темы:
- Распространённые структуры данных: ArrayList, HashMap и другие забавные штуки.
- Ввод-вывод файлов: чтение, запись и укрощение файлов как босс.
- Форматирование и печать: придание вашему выводу красивого вида (или хотя бы разборчивости).
- Другие утилиты: случайные числа, время и прочие забавные вещи.
Стандартная библиотека — это бьющееся сердце Zig. Забудьте о раздутых фреймворках или магических заклинаниях рантайма — здесь всё стройно, явно и абсолютно бескомпромиссно. Пишете ли вы ядро, которое никогда не должно падать, игровой сервер, которому плевать на паузы сборщика мусора, или модуль WebAssembly размером меньше эго гоблина, std — ваш билет к коду, который быстр, надёжен и принадлежит вам. Пропустите это, и вы будете отлаживать сегфолты в Вальхалле.
Готовы обменять сборщики мусора на монтировку и фонарик? Тогда ныряем.
#
Технические требования
Весь код, показанный в этой главе, можно найти в каталоге Chapter10 нашего репозитория Git: https://github.com/PacktPublishing/Learning-Zig/tree/main/Chapter10.
#
Распространённые структуры данных: ArrayLists, HashMaps и другие забавные штуки
#
Участники вечеринки: ArrayLists в Zig — искусство контролируемого хаоса
Представьте, что вы командуете батальоном байтов. Они шумные, непредсказуемые и поднимут мятеж при малейшей ошибке. ArrayList в Zig — это ваша полевая инструкция для этого бунта: динамический массив, который позволяет увеличивать численность ваших войск именно тогда, когда это необходимо, но требует хоронить павших. Здесь нет сборщиков мусора, играющих роль могильщиков. Вы либо оставите за собой след утечек памяти, как программист на C после бурной ночи, либо возвыситесь до мастера управления памятью. Давайте копать окопы.
Примечание переводчика
Не забывайте, что с версии Zig 0.15.2 произошли существенные изменения в структуре списков стандартной библиотеки. Теперь ArrayList — функция, создающая std.array_list.Aligned, которому в каждый метод, работающий с данными нужно передавать аллокатор. Максимально близка к ArrayList из предыдущих версий структура std.array_list.Managed. Подробнее смотрите документацию стандартной библиотеки вашей версии Zig, которую можно получить по команде zig std.
#
init: поднятие знамени
Каждый ArrayList начинается как пустое обещание. Вы объявляете его с помощью init, передавая ему аллокатор — наёмника, который будет сражаться за память от вашего имени. Забудьте об убаюкивающем new ArrayList<>() из Java или литералах срезов из Go. В Zig вы зарабатываете свои динамические массивы:
var list = ArrayList(u8).init(allocator);
// Поздравляю. Теперь у вас есть список, который не может вместить ни одного байта.
// Это как купить меч без лезвия. Но подождите — в этом есть потенциал.
Это не лень, это аскетизм. Zig не выделит ни одного байта, пока вы не отдадите явный приказ. Почему? Потому что разработчики встраиваемых систем ненавидят скрытые выделения памяти так же сильно, как кошки ненавидят воду, а системным программистам нужно точно знать, где заложены их гранаты памяти.
#
append: иллюзия безопасности
Ах, append — функция, за которую цепляется каждый новый программист на Zig, как за защитное одеяло. «Смотри!» — говорят они, — «Я могу динамически добавлять элементы!». Но вот в чём загвоздка:
try list.append(42);
Это невинное try — ваша первая подсказка. В Zig даже добавление жалкого целого числа может завершиться неудачей. Не хватает памяти? Жёстко. Аллокатор взбунтовался? Обработайте это или упадите. Это не Python, где списки растут как сорняки. Это контракт: вы управляете кавалерией, а Zig просто предоставляет лошадей.
И когда вы становитесь самоуверенным — когда вы «обеспечили» ёмкость и думаете, что находитесь в безопасности — Zig вручает вам appendAssumeCapacity. Это функциональный эквивалент вызова на слабо: «Давай, добавь этот элемент. Я дважды посмею тебе переполнить буфер». Один неверный шаг — и вы смотрите на ошибку сегментации (segmentation fault) — версию почётной грамоты от Zig.
#
Ёмкость: шахматная партия
Память в Zig — это не шведский стол, а шахматный матч. ensureTotalCapacity — ваш первый ход — гамбит для резервирования пространства без обязательств:
try list.ensureTotalCapacity(1000); // "Мне это может понадобиться позже."
Но Zig насмехается над вашей нерешительностью. Зарезервированная память — это потраченная впустую память во встраиваемых системах. Поэтому вы переходите к ensureTotalCapacityPrecise, дотошному кузену. Он выделяет ровно столько, сколько вы просите, ни байтом больше — идеально, когда вы продеваете нитку в игольное ушко микроконтроллера.
А когда вы закончили? shrinkAndFree срезает мёртвый груз. Это Мари Кондо управления памятью — если бы метод КонМари включал ручное освобождение каждого байта и проверку на слёзы.
#
Неконтролируемая орда
Затем есть ArrayListUnmanaged — режим Zig «подержи моё пиво». Здесь вы избавляетесь от няни-аллокатора и идёте вразнос:
var list = ArrayListUnmanaged(u8){};
// Вы: "Я сам справлюсь с аллокатором, спасибо."
// Также вы: [Позже, в поту] "Почему в продакшене происходит переполнение буфера?"
Неконтролируемые списки предназначены для случаев, когда вы хотите передавать аллокаторы как контрабанду — полезно для разработчиков ядра, которые жонглируют зонами памяти, или модулей WebAssembly, которые относятся к памяти браузера как к взятому напрокат плащу. Но забыли передать аллокатор в append? Наслаждайтесь неопределённым поведением. Zig — не ваша няня, он ваш сержант-инструктор.
#
Вставки и удаления: танец смерти
Давайте поговорим о вставке элементов. В JavaScript вы используете splice. В Zig вы используете insert, но с грацией бензопилы:
try list.insert(0, 99); // Добавляет 99 в начало. Сложность: O(n)
Этот 0 — смертный приговор для больших списков. Каждый элемент сдвигается вправо, как пассажиры, теснящиеся на сиденье в метро. Для маленьких списков? Нормально. Для 10 000 элементов? Вы только что изобрели новую форму задержки (latency).
И удаления! orderedRemove поддерживает порядок, как вежливая очередь. swapRemove — альтернатива в стиле драки в баре — затаскивает последний элемент в образовавшуюся брешь. Это быстро, хаотично и идеально подходит для систем реального времени, где скорость важнее приличий.
#
Гамбит с терминальным символом
Нужен дружественный к C срез с завершающим нулём? toOwnedSliceSentinel вам поможет:
const c_str = try list.toOwnedSliceSentinel(0);
defer allocator.free(c_str);
// Вот он: срез Zig, маскирующийся под строку C.
// Один неверный шаг — и это валентинка переполнения буфера.
Это способ Zig поиздеваться над совместимостью с C. «О, тебе нужны терминальные символы? Держи. Но ты управляешь временем жизни». Почтение ручному контролю почти поэтично — если бы поэзия включала разыменование нулевых указателей.
#
Философия боли
ArrayList в Zig — это не просто структура данных, а моральная позиция. Он говорит: «Память — это ресурс, а не право». Хочешь динамический рост? Хорошо, но ты будешь обрабатывать ошибки перераспределения. Хочешь безопасность? Используй defer list.deinit() так, будто твоя работа зависит от этого (потому что так оно и есть).
Для разработчика на Go это кажется варварством — где твой сборщик мусора для среза? Для инженера C++ это ностальгия — std::vector, который не притворяется безопасным. Но для программиста встраиваемых систем это свобода: никаких скрытых выделений, никаких сюрпризов во время выполнения, только сырой, бескомпромиссный контроль.
#
Эпилог: зачем вообще этим заниматься?
Потому что в окопах системного программирования важен каждый байт. Потому что модули WebAssembly не могут позволить себе паузу сборщика мусора. Потому что системы реального времени требуют детерминированного поведения. ArrayList в Zig — это инструмент для тех, кто видит память не как абстракцию, а как бухгалтерскую книгу — каждое выделение является долгом, каждое освобождение — погашением.
Он не для всех. Но если вы когда-нибудь смотрели на ошибку проверки заимствований Rust или исключение Java OutOfMemoryException и думали: «Я мог бы сделать лучше», ArrayList в Zig — ваш Экскалибур. Владейте им мудро, и Святой Грааль системного программирования будет вашим. Злоупотребляйте им, и пусть боги сегфолтов смилостивятся над вашей душой.
#
HashMaps: драчуны из таверн среди структур данных
Хеш-карта (HashMap) Zig похожа на шумную таверну: ключи проталкиваются внутрь, значения пьют эль в углу, и если вы не уберёте после закрытия (кашель deinit кашель), вы проснётесь от криков системного администратора об утечках памяти. Вот в чём загвоздка: Zig даёт вам ключи от таверны. Никакого вышибалы, никакой магии. Только вы, ваша смекалка и очень явный свод правил.
#
Ключевые операции (каламбур)
Давайте разберём хаос:
init: Призвать новую HashMap. Думайте об этом как о высечении свежей комнаты в подземелье.
var map = AutoHashMap(u32, u32).init(allocator);
// Поздравляю! Вы только что выделили пустоту. Теперь наполните её добычей (или ключами).
deinit: Сжечь таверну дотла. Метафорически.
defer map.deinit(); // Освободи её, иначе вернутся космические ужасы ОЗУ.
Совет профессионала: забудьте об этом, и ваша программа станет драконом, копящим память. Никто не любит драконов.
put: Бросить пару ключ-значение в свалку. Перезаписать существующие ключи? Тебе решать.
try map.put(42, 9001); // Ключ: 42. Значение: Более. Девяти. Тысяч.
get: Извлечь значение. Возвращает null, если ключ скрывается в эфирном плане.
if (map.get(42)) |val| {
// Нашёл его! Теперь не урони его как горячую картошку.
}
remove: Удалить ключ. Оставляет надгробие (не крутую рок-группу).
_ = map.remove(42); // Надгробие установлено. Производительность падает!
#
Тёмные искусства хеширования
AutoHashMap в Zig похож на сварливого волшебника — он автоматически захеширует ваши ключи, но потребует от вас определения двух заклинаний:
hash: Превращает ключи в числа. Номера не должны совпадать ни с одним из ключей (если только вам не нравятся драки в тавернах).eql: Проверяет, идентичны ли два ключа. Здесь нет места самозванцам.
Вот пример для u32:
const Context = struct {
pub fn hash(self: @This(), key: u32) u64 { ... }
pub fn eql(self: @This(), a: u32, b: u32) bool { ... }
};
// Ваши заклинания, сэр. Применяйте их с умом.
Вот в чём загвоздка: если вы используете []const u8 (строки) в качестве ключей, Zip выдаёт ошибку компиляции. Почему? Потому что строки хитрые — они могут быть захешированы по значению (содержимому) или по идентификатору (адресу памяти). Zig говорит: «Выбирай, трус».
Решение — используйте StringHashMap для строковых ключей. Это как библиотекарь — он хеширует содержимое строки, а не адрес.
#
Надгробия: призраки прошлых хешей
Когда вы удаляете ключ, Zig его не стирает. Он оставляет надгробие — призрачное напоминание о том, что здесь когда-то что-то было. Зачем? Потому что линейное пробирование (алгоритм, который использует Zig) нуждается в этих маркерах, чтобы избежать преждевременного завершения поиска.
Но надгробия накапливаются. Их становится слишком много, и ваша HashMap превращается в кишащее привидениями кладбище. На помощь приходит rehash:
map.rehash(); // Изгнать призраков. Ваша HashMap теперь на 50% менее жуткая.
Rehash против кэша CPU
Периодически выполняйте рехеш, если у вас происходят интенсивные циклы вставки/удаления. Ваш кэш процессора скажет вам спасибо.
#
Адаптируйся или погибни: контексты и кастомизация
Хеш-карты (HashMaps) Zig позволяют менять логику хеширования так же легко, как персонаж RPG меняет снаряжение. Используйте getOrPutAdapted для пользовательских типов ключей:
const AdaptedKey = struct {
name: []const u8
};
const AdaptedContext = struct {
fn hash(ctx: @This(), key: AdaptedKey) u64 { ... }
fn eql(ctx: @This(), a: AdaptedKey, b: AdaptedKey) bool { ... }
};
Теперь швыряйтесь адаптированными ключами, как некромант швыряется проклятиями:
try map.getOrPutAdapted(AdaptedKey{.name="Zig"}, AdaptedContext{});
#
Zig против всего мира
- Rust: Проверка заимствований бормочет: «Не будет у тебя висячих указателей». Zig отвечает: «Вот клинок. Постарайся не зарезаться сам».
- JavaScript: «LOL, просто используй объекты!» Zig парирует: «Ваши 'хеш-таблицы' на 80% состоят из клея и надежды».
- C++: «std::unordered_map безопасен!» Zig смеётся. «Твой ход, -fsanitize=undefined».
#
Бой с боссом: проверь своё снаряжение
var map = AutoHashMap(u32, u32).init(allocator);
defer map.deinit();
// Квест: Вставить 3 ключа, удалить один, затем клонировать карту.
// Награда: Вечная слава (и никаких утечек памяти).
Подсказка: Используйте ensureTotalCapacity перед боем с боссом (то есть при тяжёлой вставке). Никому не нравятся падения частоты кадров посреди битвы.
Хеш-карты Zig грубые, явные и великолепно быстрые. Освойте их, и вы сможете подчинять память своей воле. Забыли вызвать deinit? Что ж... скажем так, космические ужасы ОЗУ всегда голодны.
#
Ввод-вывод файлов: читайте, записывайте и укрощайте файлы как босс
«Там, где байты встречаются с отвагой, а ошибки сегментации боятся ступить».
Примечание переводчика
В версии Zig 0.16 существенно изменилась работа с файлами, поскольку появился новый std.Io. Подробнее смотрите в документации стандартной библиотеки и примечаниях к выпуску 0.16.0.
#
Открывающая сцена: темница файловой системы
Чувствуете себя подавленным из-за файлов? Не волнуйтесь, вы будете рассекать операции с файлами, как разбойник с кинжалом +5. Ввод-вывод файлов в Zig проще, чем вы могли бы ожидать. Давайте погрузимся.
#
Открытие файлов: переговоры с вышибалой файловой системы
Представьте файлы как сварливых библиотекарей, охраняющих древние фолианты. Чтобы читать или писать, вам нужны правильные разрешения, твёрдая рука и здоровый страх перед кодами ошибок. В Zig открытие файла похоже на то, чтобы убедить дракона одолжить вам его сокровища — возможно, но только если вы следуете правилам.
const std = @import("std");
pub fn main() !void {
// Открыть "secret_plans.txt" для чтения. Если его нет? Жёсткая удача.
const file = try std.fs.cwd().openFile("secret_plans.txt", .{ .read = true });
defer file.close(); // Закройте его, иначе гоблин дескриптора файла будет преследовать вашу оперативную память.
}
Примечание: Zig заставляет вас явно обрабатывать ошибки. Помните ключевое слово try? Это ваш щит от неопределённого поведения. Забудете проверить наличие ошибок, и компилятор поджарит вас сильнее, чем тролля при дневном свете.
Теперь, когда вы прорвались через ворота, давайте разграбим сундук с сокровищами...
#
Чтение файлов: от байтов до права на хвастовство
Чтение файлов в Zig похоже на разграбление сундука с сокровищами — вы решаете, сколько золота (данных) унести, и вам лучше иметь достаточно карманов (памяти). Давайте совершим набег:
// Выделить буфер для хранения содержимого файла.
const allocator = std.heap.page_allocator;
const data = try file.readToEndAlloc(allocator, 1024);
defer allocator.free(data); // Освободите его, иначе столкнётесь с кракеном утечки памяти.
std.debug.print("File contents: {s}\n", .{data});
Функция readToEndAlloc — ваш верный скакун здесь, но она жадная — она будет поглощать байты, пока файл не закончится или ваш буфер не взорвётся. Для больших файлов читайте по частям (chunks), если только вы не наслаждаетесь экзистенциальным кризисом, вызванным RAM.
Добыча получена? Пора высечь своё наследие на диске…
#
Запись файлов: высекая своё наследие на диске
Запись файлов — это то место, где вы переходите от ученика к архимагу. Хотите нацарапать «Hello, World» в бытие? Zig прикроет тебя, но это не избавит от чернильных пятен (смотрим на вас, висящие дескрипторы файлов).
const new_file = try std.fs.cwd().createFile("achievements.txt", .{});
defer new_file.close();
const message = "Defeated the Boss Monster in 3.2s";
_ = try new_file.writeAll(message); // Запишите это, иначе ваше право на хвастовство исчезнет.
Внезапный опрос, умник: Что произойдёт, если вы забудете defer new_file.close()?
- Zig вежливо закроет его за вас.
- Дескриптор файла протечёт как решето.
- Компилятор отправит вам пассивно-агрессивную ошибку.
(Ответ: b. Всегда. Закрывайте. Свои. Файлы.)
Но зачем останавливаться на каракулях текста? Давайте высечем данные в саму душу диска…
#
Бинарные файлы: телепатия для машин
Текстовые файлы — это вежливые беседы. Бинарные файлы? Это телепатические дампы данных — сырые, эффективные и абсолютно безжалостные. Давайте закодируем характеристики персонажа RPG, не будучи сожранными ошибками выравнивания:
const Player = extern struct {
health: u32,
mana: u32,
level: u16,
};
// Сохраняем состояние игрока
var player = Player{ .health = 100, .mana = 50, .level = 42 };
try file.writeAll(std.mem.asBytes(&player));
// Загружаем обратно
const saved_data = try file.readToEndAlloc(allocator, @sizeOf(Player));
const loaded_player = std.mem.bytesToValue(Player, saved_data);
Предупреждение: бинарный ввод-вывод похож на путешествие во времени — измените макет структуры, и ваше прошлое «я» (данные) станет повреждённым.
Теперь, когда вы освоили необработанные байты, давайте увеличим скорость вашего ввода-вывода…
#
Буферизованный ввод-вывод: молниеносный вариант
Зачем ковыряться в файлах по одному байту, когда можно проноситься сквозь них? Буферизованный ввод-вывод — это ваш скакун с турбонаддувом, пожирающий данные кусками.
var buffer: [4096]u8 = undefined;
var reader = file.reader(); // Буферизированный ридер, летит со свистом (BRRR)
while (try reader.readUntilDelimiterOrEof(&buffer, '\n')) |line| {
std.debug.print("Line: {s}\n", .{line});
}
Буферизованное чтение похоже на разграбление подземелья оптом — меньше походов, больше сокровищ. Но запись данных небезопасна до тех пор, пока они не будут выгравированы на поверхности диска…
#
Синхронизация файла: ритуал сброса буфера
Запись в файл похожа на каракули на пергаменте — это не настоящее, пока чернила не высохнут. sync — ваше священное заклинание для изгнания демонов буфера.
try file.writeAll("Critical quest data: DO NOT CORRUPT");
try file.sync(); // Изыди, гремлины буфера!
// Сбросить только данные (метаданные могут подождать)
try file.syncDataOnly();
Используйте sync после критических записей. Ваши пользователи скажут вам спасибо, когда в таверне отключится электричество посреди сохранения. Готовы изменить само время? Давайте отмотаем его назад…
#
Перемещение по файлу: путешествие во времени для байтов
Файлы — это свитки, а seek — ваше заклинание для путешествия во времени. Отмотайте назад, перемотайте вперёд или заблудитесь в пустоте.
try file.seekTo(0); // Перематываем в начало (как точка сохранения)
const pos = try file.getPos(); // «Где я?»
try file.seekBy(-10); // Шаг назад на 10 байт (остерегайтесь переполнения снизу!)
Распространённый сценарий: Парсинг бинарных форматов? seek позволяет вам танцевать вокруг заголовков, как бард, уклоняющийся от стрел. Но будьте осторожны — манипулирование временем имеет последствия. Даже файлы помнят, когда вы вмешивались…
#
Обработка ошибок: политика Zig «Без BS»
Zig относится к ошибкам как к хардкорной RPG — никаких возрождений, никаких контрольных точек. Вы либо обрабатываете их, либо игра окончена.
const maybe_file = std.fs.cwd().openFile("config.json", .{}) catch |err| {
std.debug.print("Failed to open file: {s}\n", .{@errorName(err)});
return; // Выйти изящно, как разбойник, растворившийся в тенях.
};
Теперь давайте поговорим о правилах этого подземелья…
#
Права доступа к файлам: заклинания стражи
«Скажи "друг", и войди». За исключением того, что здесь дверь (файл) требует большего, чем пароль — она требует заклинаний. Zig позволяет вам переписывать правила доступа, как волшебник, вносящий правки в книгу заклинаний.
// Наложение защитного заклинания (чтение/запись для тебя, только чтение для простолюдинов)
try file.chmod(0o644); // 0o644 = «Это моё, но ты можешь смотреть».
// Призовите статистику стража файла
const stats = try file.stat();
if (stats.mode & 0o200 == 0) {
std.debug.print("File is write-locked. Not today, hacker.\n", .{});
}
Права доступа — это ров вокруг вашей крепости с данными. Забудьте о них, и ваши файлы превратятся во всеобщую драку в таверне.
Но что, если вам нужна копия вашего драгоценного файла, чтобы разделить нагрузку? Введите…
#
Жесткие ссылки: файлы-двойники
Жёсткие ссылки — это не просто ярлыки, это близнецы с общей душой. Удалить оригинал? Клон продолжает жить, ухмыляясь из тени.
// Выковать жёсткую ссылку (два файла, одна душа)
try std.fs.cwd().link("original_quest.txt", "backup_quest.txt", .{});
// Разорвать связь (данные выживают в Вальхалле)
try std.fs.cwd().deleteFile("original_quest.txt");
Используйте жёсткие ссылки для систем резервного копирования. Они дешевле клонов и вдвое лояльнее.
#
Финальный босс: атомарный логгер
Вот ваш квест:
- Создать временный файл.
- Записать в него данные журнала (log data).
- Атомарно переименовать его в app.log при успехе.
- Удалить временный файл при сбое.
Остерегайтесь следующего:
- Права доступа (вышибала может сказать «нет»).
- Частичные записи (не оставляйте наполовину испечённые файлы).
- Ошибки (проверяйте каждый код возврата).
Награда: Благословение системного администратора (и отсутствие повреждённых журналов).
#
Символ веры файловой системы Zig
Zig не держит вас за руку. Он вручает вам меч и карту и говорит: «Вы командуете байтами. Вы ведёте переговоры об ошибках. Забыли вызвать close() для файла? Компилятор не будет вас ругать — но ваша оперативная память заплачет. Овладейте этими инструментами, и вы сможете подчинять файловые системы своей воле, открывая по одному файлу за раз.».
Теперь идите вперёд, и пусть ваши приключения со std.fs будут без утечек.
Вы всё ещё здесь? Подземелье файловой системы ждёт. Ваш следующий квест: man 2 open.
#
Нетронутые территории в операциях с файлами Zig: утерянные свитки
Вы сразили драконов базового ввода-вывода — теперь давайте совершим набег на хранилища нетронутых секретов std.fs.
#
Режимы и флаги файлов: мелкий шрифт при создании файла
Открытие файла — это не просто «чтение» или «запись». Функции openFile и createFile в Zig принимают флаги, которые превращают вас в юриста файловой системы.
// Открыть файл в режиме "только для добавления" (как гроссбух для логирования)
const log_file = try dir.createFile("combat.log", .{ .append = true });
// Создать файл эксклюзивно (завершается ошибкой, если он существует)
const vault = try dir.createFile("loot.dat", .{ .exclusive = true });
// Обрезать файл до нуля при открытии (удалить его содержимое)
const clean_slate = try dir.openFile("save.txt", .{ .truncate = true });
Эти флаги — ваш контракт с файловой системой. .exclusive — это как заявить «это моё» на сундук с сокровищами.
Запомните: Комбинируйте флаги, такие как .{ .read = true, .write = true }, для режима чтения-записи — идеально подходит для сохранений в RPG.
#
Файлы, отображаемые в память: некромантия ввода-вывода
Помещайте файлы в оперативную память с помощью темной магии. Хотя этого нет в std.fs, модуль std.os.mmap в Zig позволяет вам обращаться с файлами как с изменяемой памятью.
const data = try std.os.mmap(
null,
file_size,
std.os.PROT.READ | std.os.PROT.WRITE,
std.os.MAP.SHARED,
file.handle,
0 // Смещение
);
defer std.os.munmap(data);
// Редактировать файл ПРЯМО В ПАМЯТИ (обращаться осторожно)
data[0] = 'Z'; // Повредить первый байт? Ты монстр.
Предупреждение: Это всё равно что жонглировать боевыми гранатами. Один неверный шаг — и ваша программа превратится в фонтан ошибок сегментации.
#
Разведданные о дисковом пространстве: отчёт разведчика
Запрашивайте статистику файловой системы, чтобы избежать записи на полный диск (тихий убийца встраиваемых систем).
const stats = try std.fs.FileSystemStats.get(dir.fd);
std.debug.print("Free space: {d} MB\n", .{stats.blocks_available * stats.block_size / 1024 / 1024});
Это критически важно для устройств IoT — потому что исчерпание места посреди обновления прошивки — это очень плохое время.
#
Создание символических ссылок: ловушки для неосторожных
Создавайте символьные ссылки, которые ведут в никуда (или повсюду).
// Создать символьную ссылку
try dir.symLink("innocent.txt", "suspicious.link", .{});
// Проверить, существует ли цель символьной ссылки
const target_stats = std.fs.File.stat("suspicious.link") catch |err| {
if (err == error.FileNotFound) std.debug.print("It's a trap!\n", .{});
};
Ловушка с симлинком
Симлинки могут «висеть» (dangle). Всегда проверяйте, существует ли цель, прежде чем доверять им.
#
Расширенное восстановление после ошибок: протокол Феникса
Обрабатывайте временные ошибки (например, EINTR) путём повторения попытки — потому что даже системные вызовы иногда пугаются сцены.
fn writeWithRetry(file: std.fs.File, data: []const u8) !void {
while (true) {
file.writeAll(data) catch |err| {
if (err == error.Interrupted) continue; // Повторить попытку
return err; // Сдаться перед другими ошибками
};
break;
}
}
Используйте аналогичную стратегию для сетевых файловых систем или нестабильного оборудования. Ваши пользователи никогда не узнают о хаосе, который вы предотвратили.
Неписаное правило Zig: файловая система — это дикая местность. Zig даёт вам мачете и компас и говорит: «Удачи». Освойте эти тайные искусства, и вы будете кодировать как полубог. Забудете их? Наслаждайтесь отладкой ENOENT в солнечный день.
Теперь идите вперёд — и пусть ваши вызовы openFile никогда не вернут ошибку error.AccessDenied.
#
Форматирование и печать: придание вашему выводу красивого вида (или хотя бы разборчивости)
Вы когда-нибудь смотрели на вывод программы и чувствовали себя так, будто расшифровываете загадочную головоломку? Модуль std.fmt в Zig — ваш надёжный проводник, превращающий хаос в ясность, по одному спецификатору формата за раз. Вы будете создавать вывод, который будет не просто функциональным, а осмысленным, независимо от того, отлаживаете ли вы драйвер или регистрируете данные для высоконагруженного модуля WebAssembly.
#
Почему std.fmt? Точность встречается с гибкостью
Форматирование строк в Zig - это не игра в угадайку. В то время как в некоторых языках приоритет отдается краткости, в Zig предпочтение отдается ясности: каждый заполнитель имеет определенный тип, каждое выравнивание задано заранее, и каждый байт учтен. Никаких скрытых преобразований, никаких сюрпризов во время выполнения — только вы и компилятор, работающие как одна команда.
Вот в чём прелесть этого: форматтеры Zig прозрачны. Если вы не совпадаете по типу, компилятор помечает это на ранней стадии, как второй пилот, мягко возвращающий вас на курс. Готовы погрузиться в набор инструментов? Давайте разберём основы форматирования Zig — считайте это своей дорожной картой к просветлению вывода.
#
Основные инструменты: создание строк с определенной целью
std.fmt вручает вам три надёжных инструмента. Давайте исследуем их, как повар точит свои ножи.
#
format — архитектор
Создавайте строки программно, вшивайте переменные в шаблоны и отправляйте их любому писателю (файлы, буферы, сети).
const name = "Zig";
const power_level = 9001;
try std.fmt.format(writer, "{s}'s power level is {d}.", .{name, power_level});
// Вывод: Zig's power level is 9001.
Обратите внимание на {s} для строк и {d} для целых чисел. Zig требует ясности — никаких неоднозначных плейсхолдеров %v здесь нет.
#
print — разведчик
Отправляйте отформатированный текст прямо в stdout. Это идеально подходит для отладки или быстрых экспериментов.
std.debug.print("Temperature: {d}°C\n", .{98});
// Вывод: Temperature: 98°C
Совет: std.debug.print — ваш инструмент разведки. Используйте его, чтобы заглядывать в значения посреди битвы, а затем убирайте его перед развёртыванием.
#
allocPrint — библиотекарь
Нужна отформатированная строка для повторного использования? Динамически выделите её, а затем верните на полку, когда закончите.
const message = try std.fmt.allocPrint(allocator, "Errors: {d}", .{42});
defer allocator.free(message); // Вернуть память — это хорошая гигиена!
Теперь, когда вы освоили основы, давайте перейдём на новый уровень. Форматирование в Zig — это не просто функциональность, это выразительность.
#
Продвинутые техники: точность, отступы и индивидуальность
Форматирование в Zig похоже на проектирование пользовательского интерфейса: важна каждая деталь. Давайте добавим изюминку, сохраняя при этом твёрдую основу.
#
Выравнивание и отступы
std.debug.print("Health: {d:0>4}\n", .{5}); // Health: 0005
std.debug.print("Mana: {d:*<4}\n", .{8}); // Mana: 8***
Управляйте выравниванием с помощью > (вправо) или < (влево) и заполняйте любым символом. Это идеально подходит для таблиц или отображения статуса.
#
Шестнадцатеричный формат, двоичный код и друзья
std.debug.print("Hex: 0x{x}\n", .{255}); // 0xff
std.debug.print("Binary: 0b{b}\n", .{42}); // 0b101010
Это необходимо для низкоуровневой отладки. Используйте {X} для шестнадцатеричного формата в верхнем регистре (например, 0xDEADBEEF), чтобы соответствовать спецификациям протокола.
#
Управление числами с плавающей запятой
std.debug.print("PI: {d:.3}\n", .{3.1415926535}); // PI: 3.142
Укажите десятичную точность, чтобы избежать двусмысленности чисел с плавающей запятой. Больше никаких сюрпризов «0.1 + 0.2 = 0.30000000000000004».
Но как насчет ваших собственных типов данных? Zig не оставит вас равнодушными — пользовательское форматирование - это первоклассная функция.
#
Пользовательское форматирование: заставьте ваши типы сиять
Есть структура? Научите её элегантно форматировать саму себя. Превратите это:
В это:
Lifetime Leak (HP: 9999)
const Monster = struct {
name: []const u8,
hp: u32,
pub fn format(
self: Monster,
comptime _: []const u8,
_: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.print("{s} (HP: {d})", .{self.name, self.hp});
}
};
// ...
const boss = Monster{ .name = "Lifetime Leak", .hp = 9999 };
std.debug.print("Boss: {}\n", .{boss}); // Вывод: Boss: Lifetime Leak (HP: 9999)
Определите метод format, и Zig бесшовно интегрирует ваш тип в любую строку формата. Это как дать вашей структуре голос.
Готовы к испытанию? Давайте проверим ваши новообретённые навыки.
#
Время викторины: найдите ошибку
Что не так с этим кодом?
std.debug.print("User {s} has {d} unread messages.\n", .{"Ziggy", "5"});
(Подсказка: проверка типов в Zig скрупулёзна. Что здесь не совпадает?)
#
Ответ:
Спецификатор {d} ожидает целое число, но "5" — это строка. Zig требует явных типов — никаких неявных преобразований. Исправьте это на 5 (целое число) вместо "5".
Относитесь к строкам формата как к чертежам. Дважды проверьте свои спецификаторы и типы перед компиляцией.
Чем же система форматирования Zig выделяется:
- Проверки во время компиляции: отлавливайте ошибки на ранней стадии, не дожидаясь взрывов во время выполнения.
- Ноль скрытых выделений памяти: идеально подходит для встраиваемых систем или WebAssembly, где важен каждый байт.
- Прозрачный контроль: выравнивайте, дополняйте и форматируйте именно так, как вы хотите — никакой магии, никаких догадок.
std.fmt в Zig похож на прецизионный инструмент — простой в освоении, глубоко мощный на практике. Независимо от того, настраиваете ли вы встроенное устройство или оптимизируете модуль WASM, чёткий вывод начинается здесь. Теперь идите и сделайте эти логи осмысленными.
Но чёткий вывод — это только начало. Готовы добавить непредсказуемости, точности и толику утилитарности в свой набор инструментов? Давайте исследуем стандартную библиотеку Zig за пределами форматирования: там, где случайные числа, хронометраж и другие основы превращают код в ремесло.
#
Другие утилиты: случайные числа, время и другие забавные штуки
Представьте, что вы кодируете процедурно генерируемое подземелье. Вам нужна случайность, которая кажется честной, отслеживание времени с точностью, достаточной для измерения чиха дракона, и сортировка, которая не прогнётся под 10 000 сундуков с сокровищами. Стандартная библиотека Zig — это не просто инструменты, это философия «никаких сюрпризов, никаких оправданий». Давайте повысим уровень.
#
Случайность: выбирайте оружие с умом
std.rand.DefaultPrng
- Использование: Игры, симуляции, всё, где скорость важнее паранойи.
- Как это работает: Детерминированный генератор псевдослучайных чисел. Посейте его один раз, и он будет выдавать одну и ту же последовательность каждый раз. Отлично подходит для отладки или воспроизводимого игрового процесса.
Ловушка: Если ваше начальное число (seed) предсказуемо, то и ваша «случайность» тоже.
var rng = std.Random.DefaultPrng.init(42); // Ответ на главный вопрос жизни, вселенной и плохого ГПСЧ const damage = rng.random().intRangeAtMost(u16, 1, 100); // Бросок на инициативу!
std.crypto.random
- Использование: Пароли, шифрование, всё, где предсказуемость означает конец игры.
- Как это работает: Использует энтропию уровня ОС (например, /dev/urandom). Это медленнее, но так же безопасно, как пароль от Wi-Fi в Форт-Ноксе.
Ловушка: Избыточное решение для большинства приложений. Не призывайте монстра, чтобы лишь открыть банку с огурцами.
var buffer: [16]u8 = undefined; std.crypto.randomBytes(&buffer); // Ваши данные теперь защищены (наверное).
Случайность — это весело, пока вам не нужен порядок. Поговорим о чём-то более предсказуемом: времени.
#
Время: сырое, нефильтрованное и бескомпромиссное
API времени в Zig похож на ящик механика: никаких излишеств, только точные инструменты. Нужна временная метка? Вот счётчик наносекунд. Хотите календарную дату? Соберите её сами.
Примечание переводчика:
В Zig 0.16 произошло упразднение пространства имён std.time. Теперь std.time.Timer стало std.Io.Timestamp. Подробнее смотрите в примечаниях к выпуску 0.16.0
#
Почему нет DateTime?
Философия Zig — «нет скрытого кода, нет раздутых API». Даты включают часовые пояса, високосные годы и политический багаж (смотрю на вас, летнее время). Вместо того чтобы навязывать универсальное решение, Zig даёт вам следующее:
std.time.timestamp(): Секунды с 1970-01-01 (эпоха Unix)std.time.nanoTimestamp(): Наносекунды с произвольной точки (идеально для бенчмарков)std.time.Timer: Измерение выполнения кода как лабораторный эксперимент// Как рассчитать 30-дневный пробный период без DateTime: const now = std.time.timestamp(); const trial_end = now + (30 * 24 * 60 * 60); // извиняюсь, февраль, ты особенный!
Предупреждение об переполнении!
Добавление больших интервалов может превысить целочисленные пределы. Используйте насыщающую математику (+|), чтобы ограничить значение максимальным значением типа вместо сбоя (в режиме отладки) или циклического перехода (в режиме релиза). Это критически важно для таймеров, датчиков и расчётов ресурсов.
#
Нужна библиотека для работы с датами?
Пакет datetime от сообщества Zig находится в разработке. А пока — пишите свой собственный или примите хаос.
Время — это лишь одно измерение контроля. Давайте разрежем данные на части с помощью итераторов.
#
Итераторы: скальпели против бензопил
Итераторы в Zig — это инструменты, а не магия. Вот два тяжеловеса:
std.mem.SplitIterator- Использование: Разделение среза по фиксированному разделителю (например, парсинг CSV).
Поведение: Безжалостно эффективен. Никаких выделений памяти. Просто дайте ему разделитель и смотрите, как он рубит.
const data = "Zig,C,Rust,Go"; var iter = std.mem.split(u8, data, ","); while (iter.next()) |lang| { // lang = "Zig", "C", "Rust", "Go" }
std.mem.TokenIterator- Использование: Разделение по пробельным символам или пользовательским наборам токенов (например, парсинг аргументов командной строки).
Поведение: По умолчанию пропускает пустые токены и обрезает пробелы. Менее жестокий, более вежливый.
const cli_args = " zig build -Doptimize=ReleaseFast "; var iter = std.mem.tokenize(u8, cli_args, " "); while (iter.next()) |arg| { // arg = "zig", "build", "-Doptimize=ReleaseFast" }
Вот эмпирические правила:
SplitIterator: когда нужна хирургическая точность (например, бинарные протоколы).TokenIterator: когда нужен удобный для человека парсинг (например, текстовые файлы).
Как только ваши данные будут разделены, вам может понадобиться их отсортировать. У Zig есть своё мнение и на этот счёт.
#
Сортировка: никаких тренировок, никаких сожалений
Большинство языков скрывают сортировку за универсальной функцией sort(). Zig говорит: «Скажи мне точно, что ты хочешь».
const enemies = [_]u8{ 'Z', 'D', 'A', 'G' };
std.sort.block(u8, &enemies, {}, comptime std.sort.asc(u8));
// Теперь: ['A', 'D', 'G', 'Z'] — алфавитный порядок, потому что хаос переоценён.
Пользовательские компараторы: хотите отсортировать структуры по полю? Zig заставляет вас писать логику — никаких чёрных ящиков.
const Player = struct {
name: []const u8,
score: u32
};
var leaderboard = [_]Player{
.{ .name = "Ziggy", .score = 99 },
.{ .name = "Rusty", .score = 42 },
};
// Сортировка по очкам по убыванию:
std.sort.block(Player, &leaderboard, {}, struct {
pub fn lessThan(_: void, a: Player, b: Player) bool {
return a.score > b.score; // > для убывания, < для возрастания
}
}.lessThan);
Обратите внимание, что здесь нет выделений памяти, сортировка происходит на месте. Кроме того, она детерминирована. Одинаковый ввод? Тот же самый вывод. Каждый. Раз.
#
Отличие Zig: никакой магии, никаких компромиссов
- WebAssembly: Модули std.time и std.rand компилируются в WASM без перетаскивания рантайма. Ваш код остаётся стройнее, чем парсер JSON на C++.
- Встраиваемые системы (Embedded): Нет системных вызовов, нет сюрпризов. Таймер std.time.Timer использует аппаратные счётчики напрямую.
- Беженцы из C/C++: API Zig — это то, о чём стандартная библиотека C мечтает после терапии. Да, больше никакой «рулетки сегфолтов».
#
Финальное испытание
Создайте инструмент, который делает следующее:
- Генерирует безопасный пароль с помощью std.crypto.random.
- Замеряет время, необходимое для сортировки 1000 случайных целых чисел.
- Записывает результат в файл без использования std.log (используйте
std.fsиstd.fmt).
Застряли? Документация Zig — это ваш журнал квестов. Теперь идите взламывать планету!
#
Утилитам std.mem: исчерпывающее руководство
Вот разбивка функций std.mem от Zig, организованная по практическим сценариям использования и технической глубине. Здесь нет философии — только то, что вам нужно для эффективного управления памятью.
#
Основные операции
#
copy
Безопасно копирует байты между двумя срезами. В отличие от memcpy из C, Zig требует, чтобы исходный и целевой срезы имели одинаковую длину, предотвращая переполнения буфера.
Вы можете использовать его для копирования сетевых пакетов фиксированного размера в буфер.
Вот пример:
const source = "Hello";
var dest: [5]u8 = undefined;
std.mem.copy(u8, &dest, source); // теперь dest содержит "Hello"
Примечание переводчика: В Zig 0.16 используйте встроенную функцию @memcpy.
#
set
Эта функция заполняет срез определённым значением байта. Она полезна для инициализации массивов или очистки конфиденциальных данных (например, обнуления буфера пароля после использования).
var buffer: [128]u8 = undefined;
std.mem.set(u8, &buffer, 0); // Все байты установлены в 0
Примечание переводчика: В Zig 0.16 используйте встроенную функцию @memset.
#
eql
Сравнивает два среза побайтно. Возвращает true, только если длины и содержимое совпадают. Может использоваться для проверки криптографических хэшей.
const hash1 = "a1b2c3";
const hash2 = "a1b2c3";
if (std.mem.eql(u8, hash1, hash2)) {
// Хэши совпадают
}
#
Манипуляции с памятью
#
replace
Эта функция заменяет все вхождения подстроки на другую. Она возвращает новый срез, выделенный в куче.
Вы можете использовать её для очистки пользовательского ввода (например, замены нецензурных слов).
const input = "Zag is better than Zig";
const output = try std.mem.replace(
allocator,
u8,
input,
"Zag",
"Zig"
);
defer allocator.free(output); // Вывод: "Zig is better than Zig"
#
sliceAsBytes / bytesAsSlice
Преобразует срезы любого типа в/из байтовых срезов. Это безопасная альтернатива приведению типов (type punning) в C. Особенно полезно для сериализации целых чисел для сетевой передачи.
const numbers = [_]u32{ 0xDEADBEEF, 0xCAFEBABE };
const bytes = std.mem.sliceAsBytes(&numbers); // Преобразование в []u8
const restored = std.mem.bytesAsSlice(u32, bytes); // Преобразование обратно в []u32
#
Расширенные утилиты
#
alignForward / alignBackward
Корректирует указатель или смещение до ближайшей границы выравнивания (например, 16 байт). Обычно используется для подготовки данных к операциям SIMD.
const ptr: *u8 = ...; // Какой-то невыровненный указатель
const aligned_ptr = std.mem.alignForward(ptr, 16);
#
readInt / writeInt
Читает/записывает целые числа с явным порядком байтов (прямой порядок — little-endian или обратный — big-endian). Используется для парсинга бинарных файлов (например, заголовков PNG).
const bytes = [_]u8{ 0x12, 0x34, 0x56, 0x78 };
const value = std.mem.readInt(u32, &bytes, .Big); // 0x12345678
#
Итерация и разделение
#
split (итератор)
Разделяет срез по фиксированному разделителю. Включает пустые срезы, если разделитель встречается последовательно. Может использоваться для парсинга CSV-файлов.
const data = "Zig,Rust,,C";
var iter = std.mem.split(u8, data, ",");
while (iter.next()) |lang| {
// Выдаёт "Zig", "Rust", "", "C"
}
#
tokenize (итератор)
Разделяет срез по разделителю, пропуская пустые токены и обрезая пробельные символы. Лучше подходит для парсинга аргументов командной строки.
const args = " zig build -Doptimize=ReleaseFast ";
var iter = std.mem.tokenize(u8, args, " ");
while (iter.next()) |arg| {
// Выдаёт "zig", "build", "-Doptimize=ReleaseFast"
}
#
Решение задачи: обработчик 64-байтовых блоков
Задача: Разделить бинарный срез на блоки по 64 байта, заменить 0xFF на 0x00 и замерить время выполнения.
fn processChunks(input: []u8) !void {
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < input.len) {
const end = @min(i + 64, input.len);
const chunk = input[i..end];
// Заменяем 0xFF на 0x00 (на месте)
for (chunk) |*byte| {
if (byte.* == 0xFF) byte.* = 0x00;
}
i += 64;
}
const elapsed_ns = timer.read();
std.debug.print("Processed in {d} ns\n", .{elapsed_ns});
}
Давайте разберём, что здесь происходит:
- Разделение на блоки: Используется арифметика указателей для разделения входных данных на сегменты по 64 байта.
- Замена на месте: Модифицируются исходные данные (без выделения памяти).
- Замер времени: Используется std.time.Timer с точностью до наносекунд.
#
Когда какую функцию использовать
Теперь, когда мы изучили все тонкости std.mem и то, как его можно использовать для эффективного управления памятью, давайте сделаем шаг назад и подведём итоги ключевых моментов, которые мы рассмотрели в этой главе.
#
Итоги
Вы прошли через Стандартную библиотеку Zig, от низкоуровневых файловых операций до эффективного форматирования строк и распространённых структур данных. Эти инструменты нужны не просто чтобы выполнить работу — они нужны для того, чтобы работа выполнялась правильно: с ясностью, точностью и полной ответственностью за каждый ресурс.
Вот ключевые выводы:
- Файлы и ввод-вывод: Zig заставляет вас обрабатывать ошибки и очистку ресурсов заранее, предотвращая утечки.
- Форматирование строк: Проверки во время компиляции устраняют загадочные сбои во время выполнения.
- Утилиты для работы с памятью: Такие функции, как std.mem.copy и std.mem.split, предлагают предсказуемые операции без выделения памяти.
- Детерминизм: API для хронометража и генерации случайных чисел отдают приоритет воспроизводимости и контролю.
Но какой смысл создавать структуры данных божественного уровня, если вы ничего не можете с ними сделать? Далее: Система сборки Zig — обучение компиляции проектов на разных платформах, управлению зависимостями и точной настройке бинарных файлов для скорости или размера. Именно здесь сияет философия Zig «никаких скрытых шагов», позволяя вам описывать процесс сборки так же тщательно, как вы пишете код.
Готовы превратить свои знания в исполняемые рабочие процессы? Подготовьте свой терминал. Фестиваль сборки ждёт.