Дженерики через comptime-типы

Реализуем обобщённый код через типы-параметры — без шаблонов и без отдельного синтаксиса.

Дженерик в Zig — это обычная функция или структура, принимающая тип как comptime-параметр; никакого специального синтаксиса шаблонов нет — обобщённость выражается тем же языком.

В C++ дженерики — это шаблоны с отдельным синтаксисом template<...>. В Java и C# — параметры типов в угловых скобках. В Zig обобщённый код не требует никаких новых конструкций: раз типы — это значения, функция просто принимает тип comptime-параметром и возвращает... другой тип.

Обобщённая функция

const std = @import("std");

// T — тип, известный при компиляции; max работает для любого числового типа
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

pub fn main() void {
    std.debug.print("{d} {d}\n", .{ max(i32, 3, 7), max(f64, 2.5, 1.1) });
}

Вывод:

7 2.5

Функция max принимает первым параметром comptime T: type — сам тип. Остальные параметры и возврат используют T. При каждом вызове компилятор подставляет конкретный тип и порождает специализированную версию. Это монооморфизация, как у шаблонов C++, но выраженная обычным кодом.

Обобщённая структура — функция, возвращающая тип

// функция, которая ВОЗВРАЩАЕТ новый тип — это и есть generic-контейнер
fn Stack(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,

        fn top(self: @This()) T {
            return self.items[self.len - 1];
        }
    };
}

// использование: Stack(i32) — это конкретный тип
const IntStack = Stack(i32);

Вот ключевой приём: обобщённый контейнер — это функция, принимающая тип и возвращающая тип (struct). Вызов Stack(i32) исполняется при компиляции и порождает конкретную структуру стека для i32. @This() внутри ссылается на текущий тип-структуру. Именно так в стандартной библиотеке устроены ArrayList и HashMap.

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

Поскольку Stack — comptime-функция, компилятор исполняет её при каждом уникальном аргументе-типе и кеширует результат. Stack(i32) и Stack(f64) дадут два разных типа, но Stack(i32) дважды — один и тот же. Никакого стирания типов, как в Java: каждая специализация — это отдельный, полностью типизированный код, оптимизируемый компилятором.

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

Первая — искать синтаксис <T>: его в Zig нет, тип передают обычным аргументом. Вторая — забыть comptime у параметра-типа: тип обязан быть известен при компиляции. Третья — путать Stack (функция) и Stack(i32) (результат — конкретный тип): экземпляры создают именно от второго.

Итог

  • Дженерики в Zig — обычные функции с comptime T: type, без особого синтаксиса.
  • Обобщённый контейнер — это функция, принимающая тип и возвращающая struct.
  • @This() ссылается на текущий тип-структуру внутри неё.
  • Каждая специализация — отдельный типизированный код; стирания типов нет.
Проверьте себя
1. Как в Zig реализуется обобщённая структура-контейнер?
AЧерез синтаксис template<T>
BФункцией, принимающей comptime T: type и возвращающей struct
CЧерез наследование
DЧерез макросы препроцессора
2. Чем отличается дженерик Zig от стирания типов в Java?
AНичем
BКаждая специализация — отдельный полностью типизированный код, без стирания
CZig вообще не поддерживает дженерики
DZig стирает типы сильнее