Массивы
Массивы и срезы — это ключевые структуры данных в 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 размер массива является частью его типа, а проверки границ выполняются как во время компиляции, так и во время выполнения. Это помогает избежать распространённых ошибок доступа к памяти, свойственных языкам более низкого уровня, при сохранении высокой производительности.
Массивы особенно удобны в следующих случаях:
- Когда данные необходимо разместить в стеке, а не в куче. Про стек и кучу мы поговорим позже.
- Когда требуется фиксированное количество элементов
- Когда размер коллекции известен на этапе компиляции
- Когда важна производительность и нужно избежать динамического выделения памяти