Массивы

2025-02-22 1674 8

Массивы и срезы — это ключевые структуры данных в Zig, позволяющие эффективно работать с последовательностями элементов. В отличие от языков с автоматическим управлением памятью, Zig даёт программисту полный контроль над размещением данных, что требует осознания различий между массивами и срезами. Массивы полезны, если вы хотите, чтобы данные размещались в стеке, а не в куче (мы рассмотрим стек и кучу подробнее в следующей главе). О срезах мы поговорим позже, после того как рассмотрим указатели и как работает стек и куча, а сейчас давайте рассмотрим как устроены массивы в Zig.

Обьявление массива

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

const arr: [5]i32 = [_]i32{ 1, 2, 3, 4, 5 };

Здесь создаётся массив из 5 элементов типа i32. При этом символ _ указывает компилятору, что размер массива должен быть выведен автоматически из количества элементов в инициализаторе. Также можно использовать фигурные скобки для явного указания значений:

const numbers = [3]i32{ 10, 20, 30 };

Обращение к элементам массива

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

const arr = [_]i32{ 1, 2, 3, 4, 5 };

// Чтение элементов по индексу
const first = arr[0];   // 1 - первый элемент
const third = arr[2];   // 3 - третий элемент
const last = arr[4];    // 5 - последний элемент

// Изменение элементов (только для изменяемых массивов)
var mutable_arr = [_]i32{ 1, 2, 3 };
mutable_arr[1] = 10;    // Изменение второго элемента

// Попытка доступа за пределами массива вызовет ошибку
const invalid = arr[5];  // Ошибка: индекс выходит за границы

Zig автоматически проверяет границы массива как во время компиляции (для константных индексов), так и во время выполнения (для динамических индексов), что помогает избежать ошибок доступа к памяти.

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

const zeros = [5]i32{ 0 ** 5 };

Каждый массив содержит в себе информацию о своей длине, которую легко получить, обратившись к свойству len:

const length = zeros.len;

Хранение длины в структуре массива позволяет Zig выполнять проверки границ массива на этапе компиляции и во время выполнения, что защищает от распространённых ошибок доступа к памяти.

Массивы в Zig поддерживают многомерность, что бывает полезно в задачах работы с матрицами. Вы можете создавать двумерные и более массивы:

const matrix = [3][3]i32{
    [_]i32{ 1, 2, 3 },
    [_]i32{ 4, 5, 6 },
    [_]i32{ 7, 8, 9 },
};

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

var arr: [3]i32 = undefined;
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

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

Деструктуризация массивов

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

В Zig можно комбинировать константы и переменные в одной деструктуризации, используя ключевые слова const и var перед отдельными идентификаторами. При деструктуризации в константы значения нельзя будет изменить после присваивания:

const arr = [_]i32{ 1, 2, 3 };
const head, var middle, const tail = arr;
// head - константа со значением 1
// middle - переменная со значением 2
// tail - константа со значением 3

middle += 10; // можно изменить, так как это переменная
head += 1;  // ошибка компиляции: нельзя изменить константу

Zig не поддерживает частичную деструктуризацию массивов - вы не можете из массива содержащего 5 элементов взять только первые 3. Следующий код приведет к ошибке компиляции:

const arr = [_]i32{ 1, 2, 3, 4, 5 };
const [head, middle, tail] = arr;
// head = 1, middle = 2, tail = 3

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

const arr = [_]i32{ 1, 2, 3, 4, 5 };
const [first, _, third, _, last] = arr;
// first = 1, third = 3, last = 5

Sentinel-массивы

Sentinel-массивы (массивы с часовым) - это особый вид массивов в Zig, которые имеют дополнительный элемент в конце, называемый “часовым” (sentinel). Этот элемент служит маркером конца массива и может быть полезен при работе со строками или при взаимодействии с C-кодом.

Sentinel-массивы объявляются с указанием значения часового после размера массива:

const sentinel_array: [5:0]u8 = [_:0]u8{ 'h', 'e', 'l', 'l', 'o' };

В этом примере создаётся массив из 5 элементов с нулевым байтом (0) в качестве часового. Фактически, такой массив занимает в памяти место для 6 элементов, где последний элемент - это часовой.

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

const c_string: [:0]const u8 = "hello";  // строковый литерал автоматически создаёт sentinel-массив
const sentinel = c_string[c_string.len];  // получаем значение часового (0)

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

Объединение массивов

В Zig существует несколько способов объединения массивов. Основной метод - использование оператора ++ для конкатенации массивов во время компиляции:

const first = [_]i32{ 1, 2, 3 };
const second = [_]i32{ 4, 5, 6 };

// Объединение двух массивов
const combined = first ++ second;  // [6]i32{ 1, 2, 3, 4, 5, 6 }

// Можно объединять более двух массивов
const third = [_]i32{ 7, 8, 9 };
const all = first ++ second ++ third;  // [9]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }

Также можно объединять массивы с отдельными элементами:

const arr = [_]i32{ 1, 2, 3 };
const with_prefix = [_]i32{0} ++ arr;     // [4]i32{ 0, 1, 2, 3 }
const with_suffix = arr ++ [_]i32{4};     // [4]i32{ 1, 2, 3, 4 }

Важные особенности объединения массивов:

При работе с sentinel-массивами нужно учитывать значение часового:

const hello = [_:0]u8{ 'h', 'e', 'l' };
const world = [_:0]u8{ 'l', 'o' };

// При объединении sentinel-массивов нужно явно указывать новое значение часового
const combined = hello[0..3] ++ world[0..2] ++ [1]u8{0};  // "hello\0"

Использование функций для инициализации массивов

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

Инициализация с помощью анонимных функций

const std = @import("std");

pub fn main() void {
    // Создание массива с использованием анонимной функции
    const numbers = init: {
        var arr: [5]i32 = undefined;
        for (&arr, 0..) |*item, i| {
            item.* = @intCast(i * 2);
        }
        break :init arr;
    };
    // numbers = [5]i32{ 0, 2, 4, 6, 8 }
}

Функции-инициализаторы

const std = @import("std");

// Функция для создания массива, заполненного степенями двойки
fn powersOfTwo(comptime size: usize) [size]i32 {
    var arr: [size]i32 = undefined;
    var i: usize = 0;
    while (i < size) : (i += 1) {
        arr[i] = std.math.pow(i32, 2, @intCast(i));
    }
    return arr;
}

// Функция для создания массива с последовательностью чисел
fn sequence(comptime size: usize, start: i32, step: i32) [size]i32 {
    var arr: [size]i32 = undefined;
    var value = start;
    for (&arr) |*item| {
        item.* = value;
        value += step;
    }
    return arr;
}

pub fn main() void {
    // Использование функций-инициализаторов
    const powers = powersOfTwo(5);     // [5]i32{ 1, 2, 4, 8, 16 }
    const seq = sequence(4, 1, 3);     // [4]i32{ 1, 4, 7, 10 }
}

Инициализация с помощью функций этапа компиляции

const std = @import("std");

// Функция для создания массива факториалов
fn factorials(comptime size: usize) [size]u64 {
    comptime {
        var arr: [size]u64 = undefined;
        var i: usize = 0;
        while (i < size) : (i += 1) {
            var factorial: u64 = 1;
            var j: u64 = 1;
            while (j <= i + 1) : (j += 1) {
                factorial *= j;
            }
            arr[i] = factorial;
        }
        return arr;
    }
}

pub fn main() void {
    // Массив вычисляется на этапе компиляции
    const facts = factorials(5);  // [5]u64{ 1, 2, 6, 24, 120 }
}

Функции инициализации особенно полезны когда:

Копирование массивов

В Zig массивы передаются по значению, поэтому при присваивании одного массива другому создаётся полная копия всех элементов:

const std = @import("std");

pub fn main() void {
    // Исходный массив
    const original = [_]i32{ 1, 2, 3, 4, 5 };

    // Создание копии массива через присваивание
    var copy = original;

    // Изменение копии не влияет на оригинал
    copy[0] = 10;
    copy[1] = 20;

    // Выведет разные значения для оригинала и копии
    std.debug.print("Оригинал: {any}\n", .{original}); // [1, 2, 3, 4, 5]
    std.debug.print("Копия: {any}\n", .{copy});        // [10, 20, 3, 4, 5]
}

Заключение

В этой главе мы рассмотрели основные аспекты работы с массивами в Zig: объявление, инициализацию, доступ к элементам, деструктуризацию, особые типы массивов с часовыми (sentinel arrays), а также операции объединения и копирования. Массивы в Zig предоставляют мощные средства для работы с последовательностями данных при сохранении строгих гарантий безопасности памяти.

Важно помнить, что в Zig размер массива является частью его типа, а проверки границ выполняются как во время компиляции, так и во время выполнения. Это помогает избежать распространённых ошибок доступа к памяти, свойственных языкам более низкого уровня, при сохранении высокой производительности.

Массивы особенно удобны в следующих случаях:

#array #zig #zigbook

0%