Кортежи

2025-03-21 1506 8

В прошлой главе мы рассмотрели основной тип используемых структур в Zig - структуры с именованными полями. Однако в Zig довольно часто можно встретить еще два типа структур - структуры в виде кортежей и пустые структуры.

Кортежи

Кортежи - это структуры, которые содержат только значения, но не имеют именованных полей. В Zig нет встроенного типа “кортеж”, как в Python или Rust, но их можно легко реализовать с помощью анонимных структур. Кортежи довольно часто используются для передачи или возвращения нескольких значений из функций.

Для того чтобы создать кортеж в языке Zig вам нужно создать анонимную структуру, которая содержит только значения. Например:

const std = @import("std");

pub fn main() !void {
    const tuple: struct { u8, bool } = .{ 42, true };

    std.debug.print("{any} {}\n", .{ tuple, @TypeOf(tuple) });
}

Этот код выведет следующее:

{ 42, true } struct { u8, bool }

Как мы видим из вывода программы тип нашей структуры определяется как struct { u8, bool }, т.е. это анонимная структура с двумя полями: первое поле типа u8 и второе поле типа bool.

Доступ к элементам кортежа

Для доступа к элементам кортежа в Zig есть два способа. Первый и самый простой способ - это использовать оператор [], который мы использовали при работе с массивами или срезами:

const std = @import("std");

pub fn main() !void {
    const tuple: struct { u8, bool } = .{ 42, true };

    std.debug.print("Number {}\n", .{tuple[0]});
    std.debug.print("Bool {}\n", .{tuple[1]});

    std.debug.print("Tuple len {}\n", .{tuple.len});
}

Все поля внутри кортежа доступны по индексам, так же, как в массивах. Кроме того, кортежи в Zig, как и массивы, имеют свойство len, которое возвращает количество элементов. В целом, кортежи можно рассматривать как массивы, в которых элементы могут иметь разные типы.

Второй способ доступа к элементам кортежа - с помощью комбинации операторов .@. В этом случае мы обращаемся к элементу кортежа используя не числовое индексирование, а используя имя числового индекса в кортеже:

const std = @import("std");

pub fn main() !void {
    const tuple: struct { u8, bool } = .{ 42, true };

    std.debug.print("Number {}\n", .{tuple.@"0"});
    std.debug.print("Bool {}\n", .{tuple.@"1"});
}

Конечно, второй вариант выглядит менее естественно, и обычно для обращения к полям кортежа используют числовые индексы.

Для доступа к элементам кортежа мы также можем использовать цикл inline for или деструктуризацию кортежа. Использовать обычный цикл for с кортежами нельзя, так как для доступа к кортежам используется механизм рефлексии на этапе компиляции:

const std = @import("std");

pub fn main() !void {
    const tuple: struct { u8, bool } = .{ 42, true };

    inline for (tuple) |item| {
        std.debug.print("Item {}\n", .{item});
    }

    const number, const boolean = tuple;
    std.debug.print("Number {}\n", .{number});
    std.debug.print("Bool {}\n", .{boolean});
}

Этот код выведет:

Item 42
Item true
Number 42
Bool true

Обращение к элементам через деструктуризацию наверно самый популярный способ использование кортежей и чаще всего вы будете встречать именно его в коде.

Если у вас есть указатель на кортеж, то как и в случае массивов вы можете использовать стандартный метод доступа к элементам кортежа через [] используя указатель:

const std = @import("std");

pub fn main() !void {
    const tuple: struct { u8, bool } = .{ 42, true };
    const ptr = &tuple;

    std.debug.print("Number {}\n", .{ptr[0]});
    std.debug.print("Bool {}\n", .{ptr[1]});
}

Выведет:

Number 42
Bool true

Объединение кортежей

Так как кортежи похожи на массивы мы также как и массивы можем объединять их с помощью оператора ++:

const std = @import("std");

pub fn main() !void {
    const tuple1: struct { u8, bool } = .{ 42, true };
    const tuple2: struct { u8, bool } = .{ 24, false };

    const combined = tuple1 ++ tuple2;

    std.debug.print("Combined tuple {any}\n", .{combined});
}

Выведет:

Combined tuple { 42, true, 24, false }

Использование кортежей в функциях

Чаще всего кортежи используются для передачи или возвращения нескольких значений из функций. Например, если у вас есть функция, которая возвращает два значения, кортежи позволяют удобно работать с результатом.

Вы можете либо деструктурировать кортеж, присваивая его значения отдельным переменным, либо использовать его как единое целое, если все элементы логически связаны между собой:

const std = @import("std");

pub fn main() !void {
    const point = getPoint();

    std.debug.print("X {}\n", .{point[0]});
    std.debug.print("Y {}\n", .{point[1]});
}

fn getPoint() struct { u8, u8 } {
    return .{ 42, 33 };
}

Выведет:

Number 42
Bool true

Использование кортежей при передаче значений в функцию может быть также полезно, если параметры кортежа имеют одну общую логическую связь. Например, если функция принимает координаты точки в виде кортежа (x, y), то использование кортежей позволяет избежать необходимости передавать два отдельных аргумента:

const std = @import("std");

pub fn main() !void {
  const point = .{42, 33};

  moveToPoint(point);
}

fn moveToPoint(point: struct { u8, u8 }) void {
  ...
}

Еще один популярный способ использование кортежей при передачи значений в функцию это имитация возможности передачи произвольного количества аргументов в функцию:

const std = @import("std");

pub fn main() !void {
    print(.{ 42, true, 33 });
}

fn print(values: anytype) void {
    const info = @typeInfo(@TypeOf(values));

    if (info != .@"struct") {
        std.debug.print("Not a tuple\n", .{});
        return;
    }

    if (!info.@"struct".is_tuple) {
        std.debug.print("Not a tuple\n", .{});
        return;
    }

    inline for (values) |value| {
        std.debug.print("{}, ", .{value});
    }

    std.debug.print("\n", .{});
}

В данном примере мы впервые видим тип переменной anytype - это тип, который может быть любым типом данных. По сути это тоже самое что и тип any в языке Go, или тип Any в Rust. При использовании этого типа стирается вся информация о типе при передаче переменной в функцию и нам нужно использовать функции рефлексии чтобы снова восстановить знания о типе переданной переменной, что мы и делаем. Используя функции @TypeOf и @typeInfo мы достаем информацию о типе, а затем проверяем, что нам действительно передали кортеж. После всех необходимых проверок мы используем inline for для перебора значений кортежа и вывода их на экран.

Пустые структуры

Пустая структура - это структура, которая не содержит полей. В Zig это можно создать с помощью ключевого слова struct и пустых фигурных скобок {}. Например:

const EmptyStruct = struct {};

Может показаться, что это довольно бесполезная конструкция, но на самом деле у нее есть несколько практических применений. Один из первых вариантов применения пустой структуры это использования ее в качестве значений в HashMap структуре. Мы еще не рассматривали HashMap структуру данных и детально рассмотрим ее позднее, а сейчас давайте рассмотрим пример использования пустой структуры в качестве значений в HashMap.

Предположим, нам нужно хранить где-то список пользователей, которые приходили к нам на сайт, и быстро проверять, был ли уже пользователь на сайте. Когда я говорю быстро, я имею ввиду за константное время (O(1)), не зависимо от того, сколько пользователей у нас на сайте. Для таких случаев удобно использовать структуру данных HashMap, где ключем будет id нашего пользователя, а значением - некий признак того, что пользователь уже был на сайте. Если решать эту задачу в лоб, первое, что приходит в голову — хранить в HashMap булевый флаг true в качестве значения:

const std = @import("std");

pub fn main() !void {
    const gpa = std.heap.page_allocator;
    var set = std.AutoHashMap(i32, bool).init(gpa);
    defer set.deinit();

    try set.put(42, true);
    try set.put(99, true);

    if (set.contains(42)) {
        std.debug.print("42 найдено!\n", .{});
    }
}

Однако такое использование, с булевой переменной, тратит целый байт памяти, что иногда бывает не очень эффективно с точки зрения использования памяти, если таких пользователей у вас тысячи или сотни тысяч. В этом случае можно использовать пустую структуру {} в качестве значения, чтобы сэкономить память:

const std = @import("std");

pub fn main() !void {
    const gpa = std.heap.page_allocator;
    var set = std.AutoHashMap(i32, struct {}).init(gpa);
    defer set.deinit();

    try set.put(42, .{});
    try set.put(99, .{});

    if (set.contains(42)) {
        std.debug.print("42 найдено!\n", .{});
    }

    std.debug.print("Size of empty struct: {}\n", .{@sizeOf(struct {})});
}

В этом случаем мы храним в значениях HashMap пустые структуры, которые как мы видим занимаю 0 байт в памяти, что более эффективно расходует нашу память.

Второй вариант - использование пустой структуры как маркера типа, когда нам важно различать тип, но не хочется тратить память на хранения дополнительной информации. Давайте рассмотрим пример:

const std = @import("std");

const ConsoleLogger = struct {}; // Маркер для логирования в консоль
const FileLogger = struct {};    // Маркер для логирования в файл

fn log(comptime LoggerType: type) void {
    if (LoggerType == ConsoleLogger) {
        std.debug.print("Лог в консоль\n", .{});
    } else if (LoggerType == FileLogger) {
        std.debug.print("Лог в файл\n", .{});
    }
}

pub fn main() void {
    log(ConsoleLogger);
    log(FileLogger);
}

В этом примере мы используем пустую структуру {} в качестве маркера типа, что позволяет компилятору выбирать нужную стратегию, но при этом не несет никаких накладных расходов по памяти.

И еще один полезный пример использования пустой структуры как маркера типа - это использование их в качестве маркеров для различных состояний в системе на базе union-типов. Например, мы можем использовать пустые структуры для маркеров состояний в системе, таких как “ожидание”, “работа”, “завершение” и т.д.

const std = @import("std");

const State = union(enum) {
    idle: struct {}, // Ожидание (нет данных)
    working: i32, // Идет процесс (с прогрессом)
    err: []const u8, // Ошибка (с сообщением)
};

pub fn main() void {
    var s: State = State{ .idle = .{} }; // Состояние ожидания

    s = State{ .working = 50 }; // Рабочее состояние с прогрессом

    if (s == .idle) {
        std.debug.print("Состояние: ожидание\n", .{});
    }
}

В данном примере состояние idle присутсвует в перечислении, но при этом не занимает память.

#tuple #zig #zigbook

0%