Перечисления и объединения

Разбираем enum, union и их безопасную комбинацию — tagged union.

Tagged union — объединение, которое помнит, какой именно вариант сейчас активен; благодаря тегу его можно безопасно разбирать через switch, не рискуя прочитать не тот тип.

Перечисление (enum) задаёт тип с фиксированным набором именованных значений. Объединение (union) хранит одно из нескольких полей в одной области памяти. По отдельности они полезны, но вместе образуют tagged union — один из самых выразительных инструментов Zig.

Перечисление

const Direction = enum { north, east, south, west };

const d = Direction.north;
const name = switch (d) {
    .north => "север",
    .east => "восток",
    .south => "юг",
    .west => "запад",
};

Значения enum пишут с точкой: .north. switch по перечислению обязан покрыть все варианты — если вы добавите пятое направление, компилятор заставит обработать и его. Это страховка от забытых случаев.

Объединение

Простой union хранит ровно одно из полей в общей памяти — как union в C. Но читать «не то» поле опасно. Поэтому в Zig чистые union применяют редко; почти всегда берут их безопасную версию.

Tagged union — безопасный вариантный тип

const std = @import("std");

const Value = union(enum) {
    integer: i64,
    float: f64,
    text: []const u8,
};

pub fn main() void {
    const v = Value{ .integer = 42 };
    switch (v) {
        .integer => |n| std.debug.print("целое: {d}\n", .{n}),
        .float => |f| std.debug.print("дробное: {d}\n", .{f}),
        .text => |s| std.debug.print("текст: {s}\n", .{s}),
    }
}

Вывод:

целое: 42

Запись union(enum) создаёт tagged union: к данным автоматически прикрепляется тег-перечисление, отмечающий активный вариант. switch по нему не только выбирает ветку, но и захватывает данные нужного типа через |n|. Прочитать неактивный вариант невозможно — компилятор и рантайм этого не позволят.

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

Tagged union в памяти — это тег (маленькое целое перечисления) плюс область, достаточная для самого большого варианта. При switch сначала проверяется тег, и только потом читается соответствующее поле. Это даёт безопасность чистого алгебраического типа из функциональных языков, но без сборщика мусора и с предсказуемой раскладкой памяти.

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

Первая — использовать «голый» union там, где нужен union(enum), и читать неактивное поле — это неопределённое поведение. Вторая — забыть, что switch по enum или tagged union обязан быть исчерпывающим. Третья — путать тег и значение: тег говорит, какой вариант, а захват |x| даёт само значение.

Итог

  • enum — фиксированный набор именованных значений; switch по нему исчерпывающий.
  • union хранит одно из полей в общей памяти, но небезопасен при чтении не того поля.
  • union(enum) — tagged union: тег запоминает активный вариант.
  • switch по tagged union безопасно разбирает варианты и захватывает данные.
Проверьте себя
1. Что добавляет union(enum) по сравнению с обычным union?
AНичего, это синонимы
BТег, который запоминает активный вариант и позволяет безопасный switch
CАвтоматическое выделение памяти
DПоддержку наследования
2. Что делает switch по tagged union с захватом |n|?
AТолько выбирает ветку
BВыбирает ветку и захватывает данные активного варианта нужного типа
CКопирует весь union
DМеняет активный вариант