Коллекции: ArrayList и HashMap

Работаем с динамическим массивом и словарём из стандартной библиотеки.

ArrayList — динамический массив из стандартной библиотеки Zig, который растёт по мере добавления элементов; как и всё, что трогает кучу, он требует явного аллокатора.

Раз строки и срезы фиксированы по длине, для растущих данных нужны динамические коллекции. std.ArrayList — это динамический массив (аналог vector в C++), а std.HashMap — словарь ключ-значение. Обе — generic-типы, порождённые через comptime, и обе требуют аллокатор.

ArrayList — динамический массив

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    var list = std.ArrayList(i32).init(alloc); // тип элемента — i32
    defer list.deinit();                       // освободить память списка

    try list.append(10);
    try list.append(20);
    try list.append(30);
    std.debug.print("len={d} sum={d}\n", .{ list.items.len, list.items[0] + list.items[2] });
}

Вывод:

len=3 sum=40

std.ArrayList(i32) — это generic-тип для i32, созданный comptime-функцией. Его инициализируют аллокатором (init(alloc)), а после работы освобождают через deinit() — отсюда обязательный defer list.deinit(). Метод append добавляет элемент и может вернуть ошибку (память кончилась), поэтому try. Доступ к данным — через поле list.items (это срез).

HashMap — словарь

var map = std.StringHashMap(i32).init(alloc); // ключ — строка, значение — i32
defer map.deinit();

try map.put("яблоки", 5);
try map.put("груши", 3);

if (map.get("яблоки")) |count| {
    // count == 5
}

StringHashMap(i32) — словарь со строковыми ключами и значениями i32. put вставляет пару, get возвращает опциональное значение (ключа может не быть — отсюда ? и проверка через if-захват). Это снова безопасность через опциональные типы: отсутствие ключа невозможно проигнорировать.

Как работает под капотом

ArrayList хранит срез-буфер и текущую длину. При append, если буфер полон, он запрашивает у аллокатора буфер вдвое больше, копирует данные и освобождает старый — отсюда амортизированная константная стоимость добавления. Поскольку аллокатор явный, рост виден и контролируем. HashMap внутри держит таблицу с открытой адресацией; и тут все выделения идут через переданный аллокатор.

Частые ошибки

Первая — забыть defer list.deinit() и получить утечку (GPA об этом сообщит). Вторая — обращаться к list напрямую вместо list.items: элементы лежат в поле items. Третья — игнорировать опциональность get: результат нужно распаковать через if или orelse, ключа может не быть.

Итог

  • std.ArrayList(T) — динамический массив; инициализируют аллокатором, освобождают deinit().
  • append может вернуть ошибку (память) — отсюда try; данные в поле items.
  • StringHashMap(V) — словарь; get возвращает опциональное значение.
  • Рост буфера и все выделения идут через явный аллокатор — поведение контролируемо.
Проверьте себя
1. Что обязательно сделать после работы с ArrayList?
AНичего, память освобождается сама
BВызвать deinit() (обычно через defer), иначе утечка
CВызвать gc()
DОбнулить items
2. Что возвращает map.get(key) у HashMap в Zig?
AВсегда значение
BОпциональное значение — ключа может не быть, поэтому результат нужно распаковать
CОшибку при отсутствии ключа
DУказатель на null