Zig — это универсальный язык программирования и инструментальная среда для разработки надёжного, эффективного и повторно используемого программного обеспечения.
Надёжный
Поведение корректно даже в крайних случаях, таких как недостаток памяти.
Эффективный
Напишите программы наилучшим способом для их поведения и производительности.
Повторно используемый
Один и тот же код работает во многих средах с разными ограничениями.
Поддерживаемый
Точно передайте намерения компилятору и другим программистам. Язык налагает небольшой накладной при чтении кода и устойчив к изменениям требований и среды.
Часто наиболее эффективным способом изучить что-то новое является изучение примеров, поэтому данная документация демонстрирует использование каждого из функций Zig. Всё расположено на одной странице, чтобы вы могли использовать инструмент поиска вашего браузера.
Примеры кода в этом документе компилируются и тестируются как часть основного набора тестов Zig.
Этот HTML-документ не зависит от внешних файлов, поэтому вы можете использовать его автономно.
Стандартная библиотека Zig
Стандартная библиотека Zig имеет свою документацию.
Стандартная библиотека Zig содержит часто используемые алгоритмы, структуры данных и определения, которые помогут вам создавать программы или библиотеки. Вы увидите много примеров использования Стандартной библиотеки Zig в этой документации. Чтобы узнать больше о Стандартной библиотеке Zig, посетите ссылку выше.
Большинство раз, целесообразней писать в stderr, а не в stdout, и то, было ли сообщение успешно записано в поток, не имеет значения. Для этого распространённого случая существует более простой API:
Zig поддерживает 3 типа комментариев. Обычные комментарии игнорируются, но комментарии к документации и комментарии к документации верхнего уровня используются компилятором для генерации документации пакета.
Сгенерированная документация всё ещё находится в стадии разработки и может быть сгенерирована с помощью:
Командная строка
zig test -femit-docs main.zig
comments.zig
const print = @import("std").debug.print;
pub fn main() void {
// Comments in Zig start with "//" and end at the next LF byte (end of line).
// The line below is a comment and won't be executed.
//print("Hello?", .{});
print("Hello, world!\n", .{}); // another comment
}
В Zig нет многострочных комментариев (например, как /* */ комментарии в C). Это позволяет Zig обладать свойством, что каждая строка кода может быть проанализирована вне контекста.
Комментарии к документации
Комментирование документации начинается ровно с трёх слэшей (т.е. ///, но не ////); несколько комментариев к документации подряд объединяются для образования многострочного комментария к документации. Комментарий к документации документирует то, что следует непосредственно за ним.
doc_comments.zig
/// A structure for storing a timestamp, with nanosecond precision (this is a
/// multiline doc comment).
const Timestamp = struct {
/// The number of seconds since the epoch (this is also a doc comment).
seconds: i64, // signed so we can represent pre-1970 (not a doc comment)
/// The number of nanoseconds past the second (doc comment again).
nanos: u32,
/// Returns a `Timestamp` struct representing the Unix epoch; that is, the
/// moment of 1970 Jan 1 00:00:00 UTC (this is a doc comment too).
pub fn unixEpoch() Timestamp {
return Timestamp{
.seconds = 0,
.nanos = 0,
};
}
};
Комментарии к документации разрешены только в определённых местах; наличие комментария к документации в неожиданном месте, например, в середине выражения или непосредственно перед комментарием, не являющимся комментарием к документации, — это ошибка компиляции.
$ zig build-obj invalid_doc-comment.zigdoc/langref/invalid_doc-comment.zig:1:16: error: expected type expression, found 'a document comment'
/// doc-comment
^
unattached_doc-comment.zig
pub fn main() void {}
/// End of file
Командная строка
$ zig build-obj unattached_doc-comment.zigdoc/langref/unattached_doc-comment.zig:3:1: error: unattached documentation comment
/// End of file
^~~~~~~~~~~~~~~
Комментарии к документации могут чередоваться с обычными комментариями. В настоящее время при генерации документации пакета обычные комментарии объединяются с комментариями к документации.
Комментарии к документации верхнего уровня
Комментирование документации верхнего уровня начинается с двух слэшей и восклицательного знака: //!; оно документирует текущий модуль.
Ошибка компиляции, если комментарий к документации верхнего уровня не находится в начале контейнера, до любых выражений.
tldoc_comments.zig
//! This module provides functions for retrieving the current date and
//! time with varying degrees of precision and accuracy. It does not
//! depend on libc, but will use functions from it if available.
const S = struct {
//! Top level comments are allowed inside a container other than a module,
//! but it is not very useful. Currently, when producing the package
//! documentation, these comments are ignored.
};
целое число без знака, размером с указатель. Также см. #5185
c_char
char
для совместимости ABI с C
c_short
short
для совместимости ABI с C
c_ushort
unsigned short
для совместимости ABI с C
c_int
int
для совместимости ABI с C
c_uint
unsigned int
для совместимости ABI с C
c_long
long
для совместимости ABI с C
c_ulong
unsigned long
для совместимости ABI с C
c_longlong
long long
для совместимости ABI с C
c_ulonglong
unsigned long long
для совместимости ABI с C
c_longdouble
long double
для совместимости ABI с C
f16
_Float16
16-битное число с плавающей точкой (10-битный мантисса) IEEE-754-2008 binary16
f32
float
32-битное число с плавающей точкой (23-битный мантисса) IEEE-754-2008 binary32
f64
double
64-битное число с плавающей точкой (52-битный мантисса) IEEE-754-2008 binary64
f80
double
80-битное число с плавающей точкой (64-битный мантисса) IEEE-754-2008 80-битное расширенное точность
f128
_Float128
128-битное число с плавающей точкой (112-битный мантисса) IEEE-754-2008 binary128
bool
bool
true или false
anyopaque
void
Используется для типов указателей без типов.
void
(нет)
Всегда значение void{}
noreturn
(нет)
Тип break, continue, return, unreachable, и while (true) {}
type
(нет)
тип типов
anyerror
(нет)
код ошибки
comptime_int
(нет)
Допускается только для известных во время компиляции значений. Тип целочисленных литералов.
comptime_float
(нет)
Допускается только для известных во время компиляции значений. Тип чисел с плавающей точкой.
В дополнение к вышеперечисленным целочисленным типам, целочисленные типы произвольной разрядности можно ссылаться, используя идентификатор i или u, за которым следуют цифры. Например, идентификатор i7 относится к целому числу со знаком 7 бит. Максимальная разрешенная разрядность целочисленного типа — 65535.
Литералы строк — это постоянные одноэлементные указатели на нуль-терминированные массивы байтов. Тип литералов строк кодирует как длину, так и тот факт, что они нуль-терминированы, и поэтому их можно привести как к фрагментам, так и к указателям с нуль-терминатором. Разъяснение литералов строк преобразует их в массивы.
Поскольку исходный код Zig закодирован в UTF-8, любые байты, отличные от ASCII, которые появляются в строковом литерале в исходном коде, сохраняют своё значение UTF-8 в содержимом строки в программе Zig; байты не изменяются компилятором. Можно встроить байты, не являющиеся UTF-8, в строковый литерал, используя обозначение \xNN.
Индексация в строку, содержащую байты, отличные от ASCII, возвращает отдельные байты, независимо от того, являются ли они допустимым UTF-8 или нет.
Многострочные строковые литералы не имеют последовательностей обратного слэша и могут занимать несколько строк. Чтобы начать многострочный строковый литерал, используйте маркер \\. Как и в случае с комментарием, строковый литерал продолжается до конца строки. Конец строки не включается в строковый литерал. Однако, если следующая строка начинается с \\, добавляется новая строка, и строковый литерал продолжается.
Используйте ключевое слово const для присвоения значения идентификатору:
constant_identifier_cannot_change.zig
const x = 1234;
fn foo() void {
// It works at file scope as well as inside functions.
const y = 5678;
// Once assigned, an identifier cannot be changed.
y += 1;
}
pub fn main() void {
foo();
}
Командная строка
$ zig build-exe constant_identifier_cannot_change.zig/home/andy/src/zig/doc/langref/constant_identifier_cannot_change.zig:8:7: error: cannot assign to constant
y += 1;
~~^~~~
referenced by:
main: /home/andy/src/zig/doc/langref/constant_identifier_cannot_change.zig:12:5
callMain: /home/andy/src/zig/lib/std/start.zig:514:17
remaining reference traces hidden; use '-freference-trace' to see all reference traces
const относится ко всем байтам, которые идентификатор непосредственно адресует. Указатели имеют свою константность.
Если вам нужна переменная, которую можно изменить, используйте ключевое слово var:
mutable_var.zig
const print = @import("std").debug.print;
pub fn main() void {
var y: i32 = 5678;
y += 1;
print("{d}", .{y});
}
undefined можно привести к любому типу. После этого невозможно определить, что значение является undefined. undefined означает, что значение может быть любым, даже не имеющим смысла в соответствии с типом. Переводя на английский, undefined означает "Не имеющее смысла значение. Использование этого значения является ошибкой. Значение не будет использовано или будет перезаписано до использования."
В режиме отладки Zig записывает 0xaa байт в неопределённую память. Это делается для раннего обнаружения ошибок и для помощи в обнаружении использования неопределённой памяти в отладчике. Однако это поведение является только особенностью реализации, а не семантикой языка, поэтому оно не гарантировано будет наблюдаемо кодом.
Тестирование Zig
Код, написанный в одном или нескольких test объявлениях, может использоваться для обеспечения того, что поведение соответствует ожиданиям:
testing_introduction.zig
const std = @import("std");
test "expect addOne adds one to 41" {
// The Standard Library contains useful functions to help create tests.
// `expect` is a function that verifies its argument is true.
// It will return an error if its argument is false to indicate a failure.
// `try` is used to return an error to the test runner to notify it that the test failed.
try std.testing.expect(addOne(41) == 42);
}
test addOne {
// A test name can also be written using an identifier.
// This is a doctest, and serves as documentation for `addOne`.
try std.testing.expect(addOne(41) == 42);
}
/// The function `addOne` adds one to the number given as its argument.
fn addOne(number: i32) i32 {
return number + 1;
}
Командная строка
$ zig test testing_introduction.zig
1/2 testing_introduction.test.expect addOne adds one to 41...OK
2/2 testing_introduction.decltest.addOne...OK
All 2 tests passed.
Пример кода testing_introduction.zig тестирует функцию addOne для обеспечения того, что она возвращает 42 при входном значении 41. С точки зрения этого теста, функция addOne называется тестируемым кодом.
zig test — это инструмент, который создаёт и запускает тестовую сборку. По умолчанию он создаёт и запускает исполняемый файл, используя стандартный тестовый запускер, предоставленный стандартной библиотекой Zig, как основную точку входа. Во время сборки test объявления, найденные при разрешении данного файла Zig, включаются для запуска и отчёта стандартного тестового запускаера.
Вывод командной строки, показанный выше, отображает две строки после команды zig test. Эти строки выводятся в стандартный вывод ошибок стандартным тестовым запускаером:
1/2 testing_introduction.test.expect addOne adds one to 41...
Строки такого вида указывают, какой тест из общего числа тестов выполняется. В данном случае 1/2 означает, что выполняется первый тест из двух. Обратите внимание, что при выводе стандартного вывода ошибок запускаера тестов в терминал, эти строки очищаются при успешном прохождении теста.
2/2 testing_introduction.decltest.addOne...
Когда имя теста — идентификатор, стандартный тестовый запускаер использует текст decltest вместо test.
All 2 tests passed.
Эта строка указывает общее количество пройденных тестов.
Объявления тестов
Объявления тестов содержат ключевое слово test, за которым следует необязательное имя, записанное как строковый литерал или идентификатор, за которым следует блок, содержащий любой допустимый код Zig, разрешённый в функции.
Блоки тестов без имени всегда выполняются во время тестовых сборок и освобождаются от пропуска тестов.
Объявления тестов похожи на функции: они имеют тип возвращаемого значения и блок кода. Неявный тип возвращаемого значения test — это союз типа ошибки anyerror!void, и его нельзя изменить. Когда файл Zig не компилируется с помощью инструмента zig test, объявления тестов исключаются из сборки.
Объявления тестов могут быть записаны в том же файле, где написан тестируемый код, или в отдельном файле Zig. Поскольку объявления тестов являются объявлениями верхнего уровня, они не зависят от порядка и могут быть записаны до или после тестируемого кода.
Объявления тестов, имеющие имя, использующее идентификатор, являются примерами тестов в документации. Идентификатор должен ссылаться на другое объявление в области видимости. Пример теста в документации, как и комментарий к документации, служит документацией к связанному объявлению и будет отображаться в сгенерированной документации для объявления.
Эффективный пример теста в документации должен быть самодостаточным и сосредоточенным на тестируемом объявлении, отвечая на вопросы нового пользователя о его интерфейсе или предполагаемом использовании, избегая ненужных или запутанных деталей. Пример теста в документации не заменяет комментарий к документации, а дополняет и сопровождает его, предоставляя проверяемый, основанный на коде пример, проверенный с помощью zig test.
Ошибка теста
Стандартный тестовый запускаер проверяет ошибку, возвращённую из теста. Когда тест возвращает ошибку, тест считается неудачным, и его трассировка возврата ошибки выводится в стандартный вывод ошибок. Общее количество ошибок будет сообщено после выполнения всех тестов.
testing_failure.zig
const std = @import("std");
test "expect this to fail" {
try std.testing.expect(false);
}
test "expect this to succeed" {
try std.testing.expect(true);
}
Командная строка
$ zig test testing_failure.zig
1/2 testing_failure.test.expect this to fail...FAIL (TestUnexpectedResult)
/home/andy/src/zig/lib/std/testing.zig:540:14: 0x103ce3f in expect (test)
if (!ok) return error.TestUnexpectedResult;
^/home/andy/src/zig/doc/langref/testing_failure.zig:4:5: 0x103cf55 in test.expect this to fail (test)
try std.testing.expect(false);
^
2/2 testing_failure.test.expect this to succeed...OK
1 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/054f0b6f088824f384d1b6c648523593/test
Пропуск тестов
Один из способов пропуска тестов — отфильтровать их с помощью параметра командной строки zig test--test-filter [текст]. Это заставляет тестовую сборку включать только тесты, имена которых содержат указанный текстовый фильтр. Обратите внимание, что тесты без имени запускаются даже при использовании параметра командной строки --test-filter [текст].
Для программирования пропуска теста, сделайте test возвращает ошибку error.SkipZigTest, и стандартный тестовый запускаер будет рассматривать тест как пропущенный. Общее количество пропущенных тестов будет сообщено после выполнения всех тестов.
testing_skip.zig
test "this will be skipped" {
return error.SkipZigTest;
}
Командная строка
$ zig test testing_skip.zig
1/1 testing_skip.test.this will be skipped...SKIP
0 passed; 1 skipped; 0 failed.
Отчёт о утечках памяти
Когда код выделяет память с помощью тестового распределителя стандартной библиотеки Zig, std.testing.allocator, стандартный тестовый запускаер будет сообщать о любых утечках, обнаруженных при использовании тестового распределителя:
testing_detect_leak.zig
const std = @import("std");
test "detect leak" {
var list = std.ArrayList(u21).init(std.testing.allocator);
// missing `defer list.deinit();`
try list.append('☔');
try std.testing.expect(list.items.len == 1);
}
Оболочка
$ zig test testing_detect_leak.zig
1/1 testing_detect_leak.test.detect leak...OK
[gpa] (err): memory address 0x7fdbea69e000 leaked:
/home/andy/src/zig/lib/std/array_list.zig:457:67: 0x104f76e in ensureTotalCapacityPrecise (test)
const new_memory = try self.allocator.alignedAlloc(T, alignment, new_capacity);
^/home/andy/src/zig/lib/std/array_list.zig:434:51: 0x1045610 in ensureTotalCapacity (test)
return self.ensureTotalCapacityPrecise(better_capacity);
^/home/andy/src/zig/lib/std/array_list.zig:483:41: 0x1041fe0 in addOne (test)
try self.ensureTotalCapacity(newlen);
^/home/andy/src/zig/lib/std/array_list.zig:262:49: 0x103ef2d in append (test)
const new_item_ptr = try self.addOne();
^/home/andy/src/zig/doc/langref/testing_detect_leak.zig:6:20: 0x103d172 in test.detect leak (test)
try list.append('☔');
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104c6a0 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x10428bb in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103f429 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103ef91 in _start (test)
asm volatile (switch (native_arch) {
^
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/4a17198138bf81bcfabd5652b0d6be24/test
$ zig test testing_detect_test.zig
1/1 testing_detect_test.test.builtin.is_test...OK
All 1 tests passed.
Вывод и регистрация тестов
По умолчанию, тестовый запуск и пространство имён тестирования Zig Standard Library выводят сообщения в стандартный поток ошибок.
Пространство имён тестирования
Пространство имён testing Zig Standard Library содержит полезные функции для создания тестов. Помимо функции expect, в этом документе используются несколько других функций, как показано ниже:
testing_namespace.zig
const std = @import("std");
test "expectEqual demo" {
const expected: i32 = 42;
const actual = 42;
// The first argument to `expectEqual` is the known, expected, result.
// The second argument is the result of some expression.
// The actual's type is casted to the type of expected.
try std.testing.expectEqual(expected, actual);
}
test "expectError demo" {
const expected_error = error.DemoError;
const actual_error_union: anyerror!void = error.DemoError;
// `expectError` will fail when the actual error is different than
// the expected error.
try std.testing.expectError(expected_error, actual_error_union);
}
Оболочка
$ zig test testing_namespace.zig
1/2 testing_namespace.test.expectEqual demo...OK
2/2 testing_namespace.test.expectError demo...OK
All 2 tests passed.
Zig Standard Library также содержит функции для сравнения фрагментов, строк и многое другое. См. остальную часть пространства имён std.testing в Zig Standard Library для получения дополнительных доступных функций.
Документация по инструменту тестирования
zig test имеет несколько параметров командной строки, которые влияют на компиляцию. См. zig test --help для получения полного списка.
В целом, предпочтительно использовать const вместо var при объявлении переменной. Это снижает нагрузку на людей и компьютеры при чтении кода и создаёт больше возможностей для оптимизации.
Ключевое слово extern или встроенная функция @extern могут использоваться для связи с переменной, экспортированной из другого объекта. Ключевое слово export или встроенная функция @export могут использоваться для обеспечения доступа к переменной другим объектам во время линковки. В обоих случаях тип переменной должен быть совместим с C ABI.
Идентификаторы переменных никогда не могут затенять идентификаторы из внешней области видимости.
Идентификаторы должны начинаться с буквенного символа или подчёркивания и могут быть продолжены любым количеством буквенно-цифровых символов или подчёркиваний. Они не должны совпадать с ключевыми словами. См. Справочник по ключевым словам.
Если необходимо имя, которое не соответствует этим требованиям, например, для связи с внешними библиотеками, может использоваться синтаксис @"".
identifiers.zig
const @"identifier with spaces in it" = 0xff;
const @"1SmallStep4Man" = 112358;
const c = @import("std").c;
pub extern "c" fn @"error"() void;
pub extern "c" fn @"fstat$INODE64"(fd: c.fd_t, buf: *c.Stat) c_int;
const Color = enum {
red,
@"really red",
};
const color: Color = .@"really red";
Переменные уровня контейнера
Переменные уровня контейнера имеют статический жизненный цикл, не зависят от порядка и анализируются лениво. Значение инициализации переменных уровня контейнера неявно comptime.
Если переменная уровня контейнера const, то её значение известно comptime, в противном случае — известно во время выполнения.
$ zig test test_static_local_variable.zig
1/1 test_static_local_variable.test.static local variable...OK
All 1 tests passed.
Переменные, локальные для потока
Переменную можно определить как локальную для потока, используя ключевое слово threadlocal, что приводит к тому, что каждый поток работает с отдельной копией переменной:
Если локальная переменная const, это означает, что после инициализации значение переменной не будет меняться. Если начальное значение const переменной известно на стадии comptime, то переменная также является comptime-известной.
Локальная переменная может быть квалифицирована ключевым словом comptime. Это приводит к тому, что значение переменной будет известно на стадии comptime, а все загрузки и сохранения переменной будут происходить во время семантического анализа программы, а не во время выполнения. Все переменные, объявленные в выражении comptime, неявно являются переменными comptime.
test_comptime_variables.zig
const std = @import("std");
const expect = std.testing.expect;
test "comptime vars" {
var x: i32 = 1;
comptime var y: i32 = 1;
x += 1;
y += 1;
try expect(x == 2);
try expect(y == 2);
if (y != 2) {
// This compile error never triggers because y is a comptime variable,
// and so `y != 2` is a comptime value, and this if is statically evaluated.
@compileError("wrong y value");
}
}
Оболочка
$ zig test test_comptime_variables.zig
1/1 test_comptime_variables.test.comptime vars...OK
All 1 tests passed.
Целые числа
Целочисленные литералы
integer_literals.zig
const decimal_int = 98222;
const hex_int = 0xff;
const another_hex_int = 0xFF;
const octal_int = 0o755;
const binary_int = 0b11110000;
// underscores may be placed between two digits as a visual separator
const one_billion = 1_000_000_000;
const binary_mask = 0b1_1111_1111;
const permissions = 0o7_5_5;
const big_address = 0xFF80_0000_0000_0000;
Значения целых чисел во время выполнения
Целочисленные литералы не имеют ограничения по размеру, и если произойдёт какое-либо неопределённое поведение, компилятор его поймает.
Однако, как только целочисленное значение больше не известно на стадии компиляции, оно должно иметь известный размер и уязвимо для неопределённого поведения.
runtime_vs_comptime.zig
fn divide(a: i32, b: i32) i32 {
return a / b;
}
В этой функции значения a и b известны только во время выполнения и, следовательно, это операция деления уязвима как для переполнения целых чисел, так и для деления на ноль.
Операторы, такие как + и -, вызывают неопределённое поведение при переполнении целых чисел. Для обёртывания и насыщения арифметики на всех целевых платформах предусмотрены альтернативные операторы. +% и -% выполняют обёртывающую арифметику, а +| и -| — насыщающую.
Zig поддерживает целые числа с произвольной разрядностью, к которым обращаются, используя идентификатор i или u и цифры. Например, идентификатор i7 относится к знаковое целое число 7 бит. Максимальная разрешённая разрядность целочисленного типа составляет 65535. Для знаковых целочисленных типов Zig использует дополнение до двух представление.
c_longdouble - соответствует long double для целевого C ABI
Вещественные литералы
Вещественные литералы имеют тип comptime_float, который гарантирует одинаковую точность и операции с самым большим типом с плавающей точкой, который является f128.
Вещественные литералы преобразуются в любой тип с плавающей точкой и в любой целочисленный тип, когда нет дробной части.
float_literals.zig
const floating_point = 123.0E+77;
const another_float = 123.0;
const yet_another = 123.0e+77;
const hex_floating_point = 0x103.70p-5;
const another_hex_float = 0x103.70;
const yet_another_hex_float = 0x103.70P-5;
// underscores may be placed between two digits as a visual separator
const lightspeed = 299_792_458.000_000;
const nanosecond = 0.000_000_001;
const more_hex = 0x1234_5678.9ABC_CDEFp-10;
Нет синтаксиса для NaN, бесконечности или отрицательной бесконечности. Для этих специальных значений необходимо использовать стандартную библиотеку:
По умолчанию операции с плавающей точкой используют режим Strict, но вы можете переключиться на режим Optimized на уровне блока:
float_mode_obj.zig
const std = @import("std");
const big = @as(f64, 1 << 40);
export fn foo_strict(x: f64) f64 {
return x + big - big;
}
export fn foo_optimized(x: f64) f64 {
@setFloatMode(.optimized);
return x + big - big;
}
Оболочка
$ zig build-obj float_mode_obj.zig -O ReleaseFast
Для этого теста необходимо разделить код на два объектных файла — в противном случае оптимизатор вычислит все значения на этапе компиляции, что работает в строгом режиме.
Операнды со знаком должны быть известны во время компиляции и положительными. В других случаях используйте @divTrunc, @divFloor или @divExact вместо этого.
Операнды со знаком или вещественные операнды должны быть известны во время компиляции и положительными. В других случаях используйте @rem или @mod вместо этого.
Если a равно null, возвращает b ("значение по умолчанию"), иначе возвращает значение a после разворачивания. Обратите внимание, что b может быть значением типа noreturn.
Если a является error, возвращает b ("значение по умолчанию"), иначе возвращает значение a после разворачивания. Обратите внимание, что b может быть значением типа noreturn. err — это error и находится в области выражения b.
const expect = @import("std").testing.expect;
const assert = @import("std").debug.assert;
const mem = @import("std").mem;
// array literal
const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
// alternative initialization using result location
const alt_message: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' };
comptime {
assert(mem.eql(u8, &message, &alt_message));
}
// get the size of an array
comptime {
assert(message.len == 5);
}
// A string literal is a single-item pointer to an array.
const same_message = "hello";
comptime {
assert(mem.eql(u8, &message, same_message));
}
test "iterate over an array" {
var sum: usize = 0;
for (message) |byte| {
sum += byte;
}
try expect(sum == 'h' + 'e' + 'l' * 2 + 'o');
}
// modifiable array
var some_integers: [100]i32 = undefined;
test "modify an array" {
for (&some_integers, 0..) |*item, i| {
item.* = @intCast(i);
}
try expect(some_integers[10] == 10);
try expect(some_integers[99] == 99);
}
// array concatenation works if the values are known
// at compile time
const part_one = [_]i32{ 1, 2, 3, 4 };
const part_two = [_]i32{ 5, 6, 7, 8 };
const all_of_it = part_one ++ part_two;
comptime {
assert(mem.eql(i32, &all_of_it, &[_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }));
}
// remember that string literals are arrays
const hello = "hello";
const world = "world";
const hello_world = hello ++ " " ++ world;
comptime {
assert(mem.eql(u8, hello_world, "hello world"));
}
// ** does repeating patterns
const pattern = "ab" ** 3;
comptime {
assert(mem.eql(u8, pattern, "ababab"));
}
// initialize an array to zero
const all_zero = [_]u16{0} ** 10;
comptime {
assert(all_zero.len == 10);
assert(all_zero[5] == 0);
}
// use compile-time code to initialize an array
var fancy_array = init: {
var initial_value: [10]Point = undefined;
for (&initial_value, 0..) |*pt, i| {
pt.* = Point{
.x = @intCast(i),
.y = @intCast(i * 2),
};
}
break :init initial_value;
};
const Point = struct {
x: i32,
y: i32,
};
test "compile-time array initialization" {
try expect(fancy_array[4].x == 4);
try expect(fancy_array[4].y == 8);
}
// call a function to initialize an array
var more_points = [_]Point{makePoint(3)} ** 10;
fn makePoint(x: i32) Point {
return Point{
.x = x,
.y = x * 2,
};
}
test "array initialization with function calls" {
try expect(more_points[4].x == 3);
try expect(more_points[4].y == 6);
try expect(more_points.len == 10);
}
Оболочка
$ zig test test_arrays.zig
1/4 test_arrays.test.iterate over an array...OK
2/4 test_arrays.test.modify an array...OK
3/4 test_arrays.test.compile-time array initialization...OK
4/4 test_arrays.test.array initialization with function calls...OK
All 4 tests passed.
Вектор представляет собой группу значений булевого типа, целых чисел, чисел с плавающей точкой или указателей, которые обрабатываются параллельно, используя инструкции SIMD, если это возможно. Типы векторов создаются с помощью встроенной функции @Вектор.
Векторы поддерживают те же встроенные операторы, что и их базовые типы. Эти операции выполняются поэлементно и возвращают вектор той же длины, что и входные векторы. Это включает:
Запрещается использовать математический оператор с комбинацией скаляров (отдельных чисел) и векторов. Zig предоставляет встроенную функцию @splat для удобного преобразования скаляров в векторы, а также поддерживает @reduce и синтаксис индексирования массивов для преобразования векторов в скаляры. Векторы также поддерживают присваивание из и в массивы фиксированной длины с известной на этапе компиляции длиной.
Для перестановки элементов внутри и между векторами Zig предоставляет функции @shuffle и @select.
Операции над векторами, длина которых короче, чем родной размер SIMD целевой машины, обычно компилируются в одну инструкцию SIMD, а векторы, длина которых длиннее, чем родной размер SIMD целевой машины, компилируются в несколько инструкций SIMD. Если для данной операции нет поддержки SIMD на целевой архитектуре, компилятор по умолчанию будет обрабатывать каждый элемент вектора по одному. Zig поддерживает любые известные на этапе компиляции длины векторов до 2^32-1, хотя наиболее типичными являются небольшие степени двойки (2-64). Обратите внимание, что чрезмерно большие длины векторов (например, 2^20) могут привести к сбоям компилятора в текущих версиях Zig.
test_vector.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "Basic vector usage" {
// Vectors have a compile-time known length and base type.
const a = @Vector(4, i32){ 1, 2, 3, 4 };
const b = @Vector(4, i32){ 5, 6, 7, 8 };
// Math operations take place element-wise.
const c = a + b;
// Individual vector elements can be accessed using array indexing syntax.
try expectEqual(6, c[0]);
try expectEqual(8, c[1]);
try expectEqual(10, c[2]);
try expectEqual(12, c[3]);
}
test "Conversion between vectors, arrays, and slices" {
// Vectors and fixed-length arrays can be automatically assigned back and forth
const arr1: [4]f32 = [_]f32{ 1.1, 3.2, 4.5, 5.6 };
const vec: @Vector(4, f32) = arr1;
const arr2: [4]f32 = vec;
try expectEqual(arr1, arr2);
// You can also assign from a slice with comptime-known length to a vector using .*
const vec2: @Vector(2, f32) = arr1[1..3].*;
const slice: []const f32 = &arr1;
var offset: u32 = 1; // var to make it runtime-known
_ = &offset; // suppress 'var is never mutated' error
// To extract a comptime-known length from a runtime-known offset,
// first extract a new slice from the starting offset, then an array of
// comptime-known length
const vec3: @Vector(2, f32) = slice[offset..][0..2].*;
try expectEqual(slice[offset], vec2[0]);
try expectEqual(slice[offset + 1], vec2[1]);
try expectEqual(vec2, vec3);
}
Оболочка
$ zig test test_vector.zig
1/2 test_vector.test.Basic vector usage...OK
2/2 test_vector.test.Conversion between vectors, arrays, and slices...OK
All 2 tests passed.
TODO обсуждение взаимодействия с C ABI TODO рассмотрение предложения std.MultiArrayList
*[N]T - указатель на N элементов, такой же, как указатель на массив.
Поддерживает синтаксис индексирования: array_ptr[i]
Поддерживает синтаксис срезов: array_ptr[start..end]
Поддерживает свойство длины: array_ptr.len
[]T - представляет собой срез (толстый указатель, который содержит указатель типа [*]T и длину).
Поддерживает синтаксис индексирования: slice[i]
Поддерживает синтаксис срезов: slice[start..end]
Поддерживает свойство длины: slice.len
Используйте &x для получения указателя на один элемент:
test_single_item_pointer.zig
const expect = @import("std").testing.expect;
test "address of syntax" {
// Get the address of a variable:
const x: i32 = 1234;
const x_ptr = &x;
// Dereference a pointer:
try expect(x_ptr.* == 1234);
// When you get the address of a const variable, you get a const single-item pointer.
try expect(@TypeOf(x_ptr) == *const i32);
// If you want to mutate the value, you'd need an address of a mutable variable:
var y: i32 = 5678;
const y_ptr = &y;
try expect(@TypeOf(y_ptr) == *i32);
y_ptr.* += 1;
try expect(y_ptr.* == 5679);
}
test "pointer array access" {
// Taking an address of an individual element gives a
// single-item pointer. This kind of pointer
// does not support pointer arithmetic.
var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
const ptr = &array[2];
try expect(@TypeOf(ptr) == *u8);
try expect(array[2] == 3);
ptr.* += 1;
try expect(array[2] == 4);
}
Оболочка
$ zig test test_single_item_pointer.zig
1/2 test_single_item_pointer.test.address of syntax...OK
2/2 test_single_item_pointer.test.pointer array access...OK
All 2 tests passed.
Zig поддерживает арифметику указателей. Лучше присвоить указатель переменной [*]T и инкрементировать эту переменную. Например, непосредственное инкрементирование указателя из среза может привести к его повреждению.
test_pointer_arithmetic.zig
const expect = @import("std").testing.expect;
test "pointer arithmetic with many-item pointer" {
const array = [_]i32{ 1, 2, 3, 4 };
var ptr: [*]const i32 = &array;
try expect(ptr[0] == 1);
ptr += 1;
try expect(ptr[0] == 2);
// slicing a many-item pointer without an end is equivalent to
// pointer arithmetic: `ptr[start..] == ptr + start`
try expect(ptr[1..] == ptr + 1);
}
test "pointer arithmetic with slices" {
var array = [_]i32{ 1, 2, 3, 4 };
var length: usize = 0; // var to make it runtime-known
_ = &length; // suppress 'var is never mutated' error
var slice = array[length..array.len];
try expect(slice[0] == 1);
try expect(slice.len == 4);
slice.ptr += 1;
// now the slice is in an bad state since len has not been updated
try expect(slice[0] == 2);
try expect(slice.len == 4);
}
Оболочка
$ zig test test_pointer_arithmetic.zig
1/2 test_pointer_arithmetic.test.pointer arithmetic with many-item pointer...OK
2/2 test_pointer_arithmetic.test.pointer arithmetic with slices...OK
All 2 tests passed.
В Zig, мы обычно предпочитаем срезы вместо указателей_с_маркером_окончания. Вы можете преобразовать массив или указатель в срез, используя синтаксис срезов.
Срезы имеют проверку границ и поэтому защищены от этого вида неопределенного поведения. Это одна из причин, по которой мы предпочитаем срезы указателям.
test_slice_bounds.zig
const expect = @import("std").testing.expect;
test "pointer slicing" {
var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var start: usize = 2; // var to make it runtime-known
_ = &start; // suppress 'var is never mutated' error
const slice = array[start..4];
try expect(slice.len == 2);
try expect(array[3] == 4);
slice[1] += 1;
try expect(array[3] == 5);
}
Оболочка
$ zig test test_slice_bounds.zig
1/1 test_slice_bounds.test.pointer slicing...OK
All 1 tests passed.
Указатели также работают на этапе компиляции, пока код не зависит от неопределённой компоновки памяти:
$ zig test test_integer_pointer_conversion.zig
1/1 test_integer_pointer_conversion.test.@intFromPtr and @ptrFromInt...OK
All 1 tests passed.
Zig может сохранить адреса памяти в коде на этапе компиляции, пока указатель не разыменовывается:
test_comptime_pointer_conversion.zig
const expect = @import("std").testing.expect;
test "comptime @ptrFromInt" {
comptime {
// Zig is able to do this at compile-time, as long as
// ptr is never dereferenced.
const ptr: *i32 = @ptrFromInt(0xdeadbee0);
const addr = @intFromPtr(ptr);
try expect(@TypeOf(addr) == usize);
try expect(addr == 0xdeadbee0);
}
}
Оболочка
$ zig test test_comptime_pointer_conversion.zig
1/1 test_comptime_pointer_conversion.test.comptime @ptrFromInt...OK
All 1 tests passed.
Загрузки и сохранения по умолчанию считаются без побочных эффектов. Если для загрузки или сохранения должны быть побочные эффекты, например, ввода-вывода памяти с отображением ввода-вывода (MMIO), используйте volatile. В следующем коде гарантируется, что все операции загрузки и сохранения с mmio_ptr произойдут и в том же порядке, как в исходном коде:
$ zig test test_volatile.zig
1/1 test_volatile.test.volatile...OK
All 1 tests passed.
Обратите внимание, что volatile не связано с конкурентностью и атомарными операциями. Если вы видите код, использующий volatile для чего-то помимо отображения ввода/вывода в память, это, вероятно, ошибка.
@ptrCast преобразует тип элемента указателя в другой. Это создает новый указатель, который может вызывать не обнаруживаемые ошибки в зависимости от операций чтения и записи, проходящих через него. В общем случае, другие типы преобразований типов предпочтительнее @ptrCast, если это возможно.
test_pointer_casting.zig
const std = @import("std");
const expect = std.testing.expect;
test "pointer casting" {
const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x12, 0x12, 0x12 };
const u32_ptr: *const u32 = @ptrCast(&bytes);
try expect(u32_ptr.* == 0x12121212);
// Even this example is contrived - there are better ways to do the above than
// pointer casting. For example, using a slice narrowing cast:
const u32_value = std.mem.bytesAsSlice(u32, bytes[0..])[0];
try expect(u32_value == 0x12121212);
// And even another way, the most straightforward way to do it:
try expect(@as(u32, @bitCast(bytes)) == 0x12121212);
}
test "pointer child type" {
// pointer types have a `child` field which tells you the type they point to.
try expect(@typeInfo(*u32).Pointer.child == u32);
}
Оболочка
$ zig test test_pointer_casting.zig
1/2 test_pointer_casting.test.pointer casting...OK
2/2 test_pointer_casting.test.pointer child type...OK
All 2 tests passed.
Выравнивание
Каждый тип имеет выравнивание - количество байтов такое, что при загрузке или сохранении значения типа в память адрес памяти должен быть кратен этому числу. Вы можете использовать @alignOf, чтобы узнать это значение для любого типа.
Выравнивание зависит от архитектуры процессора, но всегда является степенью двойки и меньше, чем 1 << 29.
В Zig тип указателя имеет значение выравнивания. Если значение равно выравниванию базового типа, его можно опустить из типа:
$ zig test test_variable_alignment.zig
1/1 test_variable_alignment.test.variable alignment...OK
All 1 tests passed.
Так же, как *i32 может быть преобразован в *consti32, указатель с большим выравниванием может быть неявно преобразован в указатель с меньшим выравниванием, но не наоборот.
Вы можете указать выравнивание для переменных и функций. Если вы это сделаете, то указатели на них получат указанное выравнивание:
$ zig test test_variable_func_alignment.zig
1/2 test_variable_func_alignment.test.global variable alignment...OK
2/2 test_variable_func_alignment.test.function alignment...OK
All 2 tests passed.
Если у вас есть указатель или срез с небольшим выравниванием, но вы знаете, что фактически он имеет большее выравнивание, используйте @alignCast, чтобы изменить указатель на указатель с большим выравниванием. Это бесполезная операция во время выполнения, но вставляет проверку безопасности:
$ zig test test_incorrect_pointer_alignment.zig
1/1 test_incorrect_pointer_alignment.test.pointer alignment safety...thread 3568823 panic: incorrect alignment
/home/andy/src/zig/doc/langref/test_incorrect_pointer_alignment.zig:10:68: 0x103d13a in foo (test)
const int_slice = std.mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
^/home/andy/src/zig/doc/langref/test_incorrect_pointer_alignment.zig:6:31: 0x103cfd7 in test.pointer alignment safety (test)
try std.testing.expect(foo(bytes) == 0x11111111);
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x1047f10 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x103e28b in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103d609 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103d171 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/c771705677c0d2df24e00269a9189f97/test
allowzero
Этот атрибут указателя позволяет указателю иметь адрес ноль. Это необходимо только для целевой системы без ОС, где адрес ноль может быть отображён. Если вы хотите представить нулевые указатели, используйте Дополнительные указатели вместо этого. Дополнительные указатели с allowzero имеют размер, отличный от размеров указателей. В этом примере кода, если у указателя не было атрибута allowzero, это была бы ошибка Преобразование указателя в недействительный нулевой:
test_allowzero.zig
const std = @import("std");
const expect = std.testing.expect;
test "allowzero" {
var zero: usize = 0; // var to make to runtime-known
_ = &zero; // suppress 'var is never mutated' error
const ptr: *allowzero i32 = @ptrFromInt(zero);
try expect(@intFromPtr(ptr) == 0);
}
Оболочка
$ zig test test_allowzero.zig
1/1 test_allowzero.test.allowzero...OK
All 1 tests passed.
Указатели, завершённые стоп-значением
Синтаксис [*:x]T описывает указатель, длина которого определяется значением стоп-значения. Это обеспечивает защиту от переполнения буфера и считывания за его пределами.
sentinel-terminated_pointer.zig
const std = @import("std");
// This is also available as `std.c.printf`.
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
pub fn main() anyerror!void {
_ = printf("Hello, world!\n"); // OK
const msg = "Hello, world!\n";
const non_null_terminated_msg: [msg.len]u8 = msg.*;
_ = printf(&non_null_terminated_msg);
}
Оболочка
$ zig build-exe sentinel-terminated_pointer.zig -lc/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: error: expected type '[*:0]const u8', found '*const [14]u8'
_ = printf(&non_null_terminated_msg);
^~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: note: destination pointer requires '0' sentinel
/home/andy/src/zig/doc/langref/sentinel-terminated_pointer.zig:4:35: note: parameter type declared here
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
~^~~~~~~~~~~~
referenced by:
callMain: /home/andy/src/zig/lib/std/start.zig:524:32
callMainWithArgs: /home/andy/src/zig/lib/std/start.zig:482:12
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Срез — это указатель и длина. Разница между массивом и срезом заключается в том, что длина массива является частью типа и известна во время компиляции, а длина среза известна во время выполнения. Оба могут быть обработаны с помощью поля `len`.
test_basic_slices.zig
const expect = @import("std").testing.expect;
const expectEqualSlices = @import("std").testing.expectEqualSlices;
test "basic slices" {
var array = [_]i32{ 1, 2, 3, 4 };
var known_at_runtime_zero: usize = 0;
_ = &known_at_runtime_zero;
const slice = array[known_at_runtime_zero..array.len];
// alternative initialization using result location
const alt_slice: []const i32 = &.{ 1, 2, 3, 4 };
try expectEqualSlices(i32, slice, alt_slice);
try expect(@TypeOf(slice) == []i32);
try expect(&slice[0] == &array[0]);
try expect(slice.len == array.len);
// If you slice with comptime-known start and end positions, the result is
// a pointer to an array, rather than a slice.
const array_ptr = array[0..array.len];
try expect(@TypeOf(array_ptr) == *[array.len]i32);
// You can perform a slice-by-length by slicing twice. This allows the compiler
// to perform some optimisations like recognising a comptime-known length when
// the start position is only known at runtime.
var runtime_start: usize = 1;
_ = &runtime_start;
const length = 2;
const array_ptr_len = array[runtime_start..][0..length];
try expect(@TypeOf(array_ptr_len) == *[length]i32);
// Using the address-of operator on a slice gives a single-item pointer.
try expect(@TypeOf(&slice[0]) == *i32);
// Using the `ptr` field gives a many-item pointer.
try expect(@TypeOf(slice.ptr) == [*]i32);
try expect(@intFromPtr(slice.ptr) == @intFromPtr(&slice[0]));
// Slices have array bounds checking. If you try to access something out
// of bounds, you'll get a safety check failure:
slice[10] += 1;
// Note that `slice.ptr` does not invoke safety checking, while `&slice[0]`
// asserts that the slice has len > 0.
}
Оболочка
$ zig test test_basic_slices.zig
1/1 test_basic_slices.test.basic slices...thread 3571722 panic: index out of bounds: index 10, len 4
/home/andy/src/zig/doc/langref/test_basic_slices.zig:41:10: 0x103f955 in test.basic slices (test)
slice[10] += 1;
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104c800 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x104210b in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103fe49 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103f9b1 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/3c391d75c939ce98356b98dd812503b1/test
Это одна из причин, по которой мы предпочитаем срезы указателям.
test_slices.zig
const std = @import("std");
const expect = std.testing.expect;
const mem = std.mem;
const fmt = std.fmt;
test "using slices for strings" {
// Zig has no concept of strings. String literals are const pointers
// to null-terminated arrays of u8, and by convention parameters
// that are "strings" are expected to be UTF-8 encoded slices of u8.
// Here we coerce *const [5:0]u8 and *const [6:0]u8 to []const u8
const hello: []const u8 = "hello";
const world: []const u8 = "世界";
var all_together: [100]u8 = undefined;
// You can use slice syntax with at least one runtime-known index on an
// array to convert an array into a slice.
var start: usize = 0;
_ = &start;
const all_together_slice = all_together[start..];
// String concatenation example.
const hello_world = try fmt.bufPrint(all_together_slice, "{s} {s}", .{ hello, world });
// Generally, you can use UTF-8 and not worry about whether something is a
// string. If you don't need to deal with individual characters, no need
// to decode.
try expect(mem.eql(u8, hello_world, "hello 世界"));
}
test "slice pointer" {
var array: [10]u8 = undefined;
const ptr = &array;
try expect(@TypeOf(ptr) == *[10]u8);
// A pointer to an array can be sliced just like an array:
var start: usize = 0;
var end: usize = 5;
_ = .{ &start, &end };
const slice = ptr[start..end];
// The slice is mutable because we sliced a mutable pointer.
try expect(@TypeOf(slice) == []u8);
slice[2] = 3;
try expect(array[2] == 3);
// Again, slicing with comptime-known indexes will produce another pointer
// to an array:
const ptr2 = slice[2..3];
try expect(ptr2.len == 1);
try expect(ptr2[0] == 3);
try expect(@TypeOf(ptr2) == *[1]u8);
}
Оболочка
$ zig test test_slices.zig
1/2 test_slices.test.using slices for strings...OK
2/2 test_slices.test.slice pointer...OK
All 2 tests passed.
Синтаксис [:x]T представляет собой срез, у которого длина известна во время выполнения, а также гарантируется значение стоп-значения в элементе, индексируемом длиной. Тип не гарантирует, что до него нет элементов стоп-значения. Срезы, завершённые стоп-значением, позволяют получить доступ к элементу по индексу len.
$ zig test test_null_terminated_slice.zig
1/1 test_null_terminated_slice.test.0-terminated slice...OK
All 1 tests passed.
Срезы, завершённые стоп-значением, также можно создать, используя разновидность синтаксиса срезов data[start..end :x], где len — указатель, массив или срез, содержащий множество элементов, и x — значение стоп-значения.
$ zig test test_null_terminated_slicing.zig
1/1 test_null_terminated_slicing.test.0-terminated slicing...OK
All 1 tests passed.
Срезы, завершённые стоп-значением, утверждают, что элемент в позиции стоп-значения в базовых данных фактически является значением стоп-значения. Если это не так, возникает защищённая от ошибок ошибка неопределённого поведения.
test_sentinel_mismatch.zig
const std = @import("std");
const expect = std.testing.expect;
test "sentinel mismatch" {
var array = [_]u8{ 3, 2, 1, 0 };
// Creating a sentinel-terminated slice from the array with a length of 2
// will result in the value `1` occupying the sentinel element position.
// This does not match the indicated sentinel value of `0` and will lead
// to a runtime panic.
var runtime_length: usize = 2;
_ = &runtime_length;
const slice = array[0..runtime_length :0];
_ = slice;
}
Оболочка
$ zig test test_sentinel_mismatch.zig
1/1 test_sentinel_mismatch.test.sentinel mismatch...thread 3579807 panic: sentinel mismatch: expected 0, found 1
/home/andy/src/zig/doc/langref/test_sentinel_mismatch.zig:13:24: 0x103cf16 in test.sentinel mismatch (test)
const slice = array[0..runtime_length :0];
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x1048aa0 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x103eabb in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103d4f9 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103d061 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/2af8da0d34d396fbb50fa515cef10c72/test
// Declare a struct.
// Zig gives no guarantees about the order of fields and the size of
// the struct but the fields are guaranteed to be ABI-aligned.
const Point = struct {
x: f32,
y: f32,
};
// Maybe we want to pass it to OpenGL so we want to be particular about
// how the bytes are arranged.
const Point2 = packed struct {
x: f32,
y: f32,
};
// Declare an instance of a struct.
const p = Point{
.x = 0.12,
.y = 0.34,
};
// Maybe we're not ready to fill out some of the fields.
var p2 = Point{
.x = 0.12,
.y = undefined,
};
// Structs can have methods
// Struct methods are not special, they are only namespaced
// functions that you can call with dot syntax.
const Vec3 = struct {
x: f32,
y: f32,
z: f32,
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3{
.x = x,
.y = y,
.z = z,
};
}
pub fn dot(self: Vec3, other: Vec3) f32 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
};
const expect = @import("std").testing.expect;
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
try expect(v1.dot(v2) == 0.0);
// Other than being available to call with dot syntax, struct methods are
// not special. You can reference them as any other declaration inside
// the struct:
try expect(Vec3.dot(v1, v2) == 0.0);
}
// Structs can have declarations.
// Structs can have 0 fields.
const Empty = struct {
pub const PI = 3.14;
};
test "struct namespaced variable" {
try expect(Empty.PI == 3.14);
try expect(@sizeOf(Empty) == 0);
// you can still instantiate an empty struct
const does_nothing = Empty{};
_ = does_nothing;
}
// struct field order is determined by the compiler for optimal performance.
// however, you can still calculate a struct base pointer given a field pointer:
fn setYBasedOnX(x: *f32, y: f32) void {
const point: *Point = @fieldParentPtr("x", x);
point.y = y;
}
test "field parent pointer" {
var point = Point{
.x = 0.1234,
.y = 0.5678,
};
setYBasedOnX(&point.x, 0.9);
try expect(point.y == 0.9);
}
// You can return a struct from a function. This is how we do generics
// in Zig:
fn LinkedList(comptime T: type) type {
return struct {
pub const Node = struct {
prev: ?*Node,
next: ?*Node,
data: T,
};
first: ?*Node,
last: ?*Node,
len: usize,
};
}
test "linked list" {
// Functions called at compile-time are memoized. This means you can
// do this:
try expect(LinkedList(i32) == LinkedList(i32));
const list = LinkedList(i32){
.first = null,
.last = null,
.len = 0,
};
try expect(list.len == 0);
// Since types are first class values you can instantiate the type
// by assigning it to a variable:
const ListOfInts = LinkedList(i32);
try expect(ListOfInts == LinkedList(i32));
var node = ListOfInts.Node{
.prev = null,
.next = null,
.data = 1234,
};
const list2 = LinkedList(i32){
.first = &node,
.last = &node,
.len = 1,
};
// When using a pointer to a struct, fields can be accessed directly,
// without explicitly dereferencing the pointer.
// So you can do
try expect(list2.first.?.data == 1234);
// instead of try expect(list2.first.?.*.data == 1234);
}
Каждое поле структуры может иметь выражение, указывающее значение поля по умолчанию. Такие выражения выполняются в период компиляции и позволяют опустить поле в выражении литерала структуры:
Если для инициализации значения структуры необходимо знать значение во время выполнения, без нарушения инвариантов данных, используйте метод инициализации, принимающий эти значения во время выполнения, и заполняйте оставшиеся поля.
extern struct
У externstruct есть внутреннее представление в памяти, соответствующее C ABI для целевой платформы.
Если внутреннее представление в памяти не требуется, struct — лучший выбор, поскольку он накладывает меньше ограничений на компилятор.
См. packed struct для структуры с ABI своего базового целого числа, что может быть полезно для моделирования флагов.
В отличие от обычных структур, packed структуры гарантируют представление в памяти:
Поля остаются в порядке объявления, от наименее к наиболее значимым.
Между полями нет заполнения.
Zig поддерживает целые числа произвольной длины, и хотя обычно целые числа с менее чем 8 битами все равно используют 1 байт памяти, в упакованных структурах они используют точно свою ширину в битах.
bool поля используют ровно 1 бит.
Поле перечисления использует точно ширину в битах своего целочисленного типа тега.
Поле упакованного объединения использует точно ширину в битах поля объединения с наибольшей шириной в битах.
Это означает, что packedstruct может участвовать в @bitCast или @ptrCast для повторной интерпретации памяти. Это работает даже в период компиляции:
$ zig test test_missized_packed_struct.zigdoc/langref/test_missized_packed_struct.zig:2:29: error: backing integer type 'u32' has bit size 32 but the struct fields have a total bit size of 24
const S = packed struct(u32) { a: u16, b: u8 };
^~~
Zig позволяет получить адрес поля, не выровненного по байтам:
$ zig test test_pointer_to_non-byte_aligned_field.zig
1/1 test_pointer_to_non-byte_aligned_field.test.pointer to non-byte-aligned field...OK
All 1 tests passed.
Однако указатель на поле, не выровненное по байтам, имеет особые свойства и не может быть передан, когда ожидается обычный указатель:
$ zig test test_misaligned_pointer.zigdoc/langref/test_misaligned_pointer.zig:17:20: error: expected type '*const u3', found '*align(1:3:1) u3'
try expect(bar(&bit_field.b) == 2);
^~~~~~~~~~~~
doc/langref/test_misaligned_pointer.zig:17:20: note: pointer host size '1' cannot cast into pointer host size '0'
doc/langref/test_misaligned_pointer.zig:17:20: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
doc/langref/test_misaligned_pointer.zig:20:11: note: parameter type declared here
fn bar(x: *const u3) u3 {
^~~~~~~~~
В этом случае функция bar не может быть вызвана, потому что указатель на поле, не выровненное по ABI, указывает на битовый смещение, а функция ожидает ABI-выровненный указатель.
Указатели на поля, не выровненные по ABI, разделяют один и тот же адрес, что и другие поля внутри их базового целого числа:
$ zig test test_packed_struct_field_address.zig
1/1 test_packed_struct_field_address.test.pointers of sub-byte-aligned fields share addresses...OK
All 1 tests passed.
$ zig test test_bitOffsetOf_offsetOf.zig
1/1 test_bitOffsetOf_offsetOf.test.offsets of non-byte-aligned fields...OK
All 1 tests passed.
Упакованные структуры имеют тот же выравнивание, что и их базовые целые числа, однако, сверхвыровненные указатели на упакованные структуры могут переопределять это:
test_overaligned_packed_struct.zig
const std = @import("std");
const expect = std.testing.expect;
const S = packed struct {
a: u32,
b: u32,
};
test "overaligned pointer to packed struct" {
var foo: S align(4) = .{ .a = 1, .b = 2 };
const ptr: *align(4) S = &foo;
const ptr_to_b: *u32 = &ptr.b;
try expect(ptr_to_b.* == 2);
}
Оболочка
$ zig test test_overaligned_packed_struct.zig
1/1 test_overaligned_packed_struct.test.overaligned pointer to packed struct...OK
All 1 tests passed.
$ zig test test_aligned_struct_fields.zig
1/1 test_aligned_struct_fields.test.aligned struct fields...OK
All 1 tests passed.
Использование упакованных структур с volatile проблематично и может стать ошибкой компиляции в будущем. Подробнее об этом см. данную проблему. TODO обновить эту документацию с рекомендацией по использованию упакованных структур с MMIO (случаи использования volatile упакованных структур), как только эта проблема будет решена. Не волнуйтесь, для этого случая в Zig будет хорошее решение.
Именование структур
Поскольку все структуры анонимные, Zig выводит имя типа на основе нескольких правил.
Если структура находится в выражении инициализации переменной, она получает имя этой переменной.
Если структура находится в return выражении, она получает имя функции, из которой она возвращается, со сериализованными значениями параметров.
В противном случае структура получает имя, например, (filename.funcname.__struct_ID).
Если структура объявлена внутри другой структуры, она получает имя, состоящее из имени родительской структуры и имени, выведенного по предыдущим правилам, разделенных точкой.
Zig позволяет опустить тип структуры литерала. Когда результат преобразуется, литерал структуры напрямую создаст местоположение результата, без копирования:
$ zig test test_anonymous_struct.zig
1/1 test_anonymous_struct.test.fully anonymous struct...OK
All 1 tests passed.
Кортежи
Анонимные структуры могут быть созданы без указания имён полей и называются "кортежами".
Поля неявно называются числами, начиная с 0. Поскольку их имена — целые числа, к ним нельзя получить доступ с помощью . синтаксиса без обертывания их в @"". Имена внутри @"" всегда распознаются как идентификаторы.
Как и массивы, кортежи имеют поле .len, могут быть индексированы (при условии, что индекс известен во время компиляции) и работают с операторами ++ и **. Они также могут быть перебираемы с помощью inline for.
const expect = @import("std").testing.expect;
const mem = @import("std").mem;
// Declare an enum.
const Type = enum {
ok,
not_ok,
};
// Declare a specific enum field.
const c = Type.ok;
// If you want access to the ordinal value of an enum, you
// can specify the tag type.
const Value = enum(u2) {
zero,
one,
two,
};
// Now you can cast between u2 and Value.
// The ordinal value starts from 0, counting up by 1 from the previous member.
test "enum ordinal value" {
try expect(@intFromEnum(Value.zero) == 0);
try expect(@intFromEnum(Value.one) == 1);
try expect(@intFromEnum(Value.two) == 2);
}
// You can override the ordinal value for an enum.
const Value2 = enum(u32) {
hundred = 100,
thousand = 1000,
million = 1000000,
};
test "set enum ordinal value" {
try expect(@intFromEnum(Value2.hundred) == 100);
try expect(@intFromEnum(Value2.thousand) == 1000);
try expect(@intFromEnum(Value2.million) == 1000000);
}
// You can also override only some values.
const Value3 = enum(u4) {
a,
b = 8,
c,
d = 4,
e,
};
test "enum implicit ordinal values and overridden values" {
try expect(@intFromEnum(Value3.a) == 0);
try expect(@intFromEnum(Value3.b) == 8);
try expect(@intFromEnum(Value3.c) == 9);
try expect(@intFromEnum(Value3.d) == 4);
try expect(@intFromEnum(Value3.e) == 5);
}
// Enums can have methods, the same as structs and unions.
// Enum methods are not special, they are only namespaced
// functions that you can call with dot syntax.
const Suit = enum {
clubs,
spades,
diamonds,
hearts,
pub fn isClubs(self: Suit) bool {
return self == Suit.clubs;
}
};
test "enum method" {
const p = Suit.spades;
try expect(!p.isClubs());
}
// An enum can be switched upon.
const Foo = enum {
string,
number,
none,
};
test "enum switch" {
const p = Foo.number;
const what_is_it = switch (p) {
Foo.string => "this is a string",
Foo.number => "this is a number",
Foo.none => "this is a none",
};
try expect(mem.eql(u8, what_is_it, "this is a number"));
}
// @typeInfo can be used to access the integer tag type of an enum.
const Small = enum {
one,
two,
three,
four,
};
test "std.meta.Tag" {
try expect(@typeInfo(Small).Enum.tag_type == u2);
}
// @typeInfo tells us the field count and the fields names:
test "@typeInfo" {
try expect(@typeInfo(Small).Enum.fields.len == 4);
try expect(mem.eql(u8, @typeInfo(Small).Enum.fields[1].name, "two"));
}
// @tagName gives a [:0]const u8 representation of an enum value:
test "@tagName" {
try expect(mem.eql(u8, @tagName(Small.three), "three"));
}
По умолчанию перечисления не гарантируют совместимость с C ABI:
enum_export_error.zig
const Foo = enum { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
Оболочка
$ zig build-obj enum_export_error.zigdoc/langref/enum_export_error.zig:2:17: error: parameter of type 'enum_export_error.Foo' not allowed in function with calling convention 'C'
export fn entry(foo: Foo) void {
^~~~~~~~
doc/langref/enum_export_error.zig:2:17: note: enum tag type 'u2' is not extern compatible
doc/langref/enum_export_error.zig:2:17: note: only integers with 0, 8, 16, 32, 64 and 128 bits are extern compatible
doc/langref/enum_export_error.zig:1:13: note: enum declared here
const Foo = enum { a, b, c };
^~~~~~~~~~~~~~~~
Для совместимости с C ABI укажите явный тип тега для перечисления:
enum_export.zig
const Foo = enum(c_int) { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
Оболочка
$ zig build-obj enum_export.zig
Литералы перечислений
Литералы перечислений позволяют указать имя поля перечисления без указания типа перечисления:
test_enum_literals.zig
const std = @import("std");
const expect = std.testing.expect;
const Color = enum {
auto,
off,
on,
};
test "enum literals" {
const color1: Color = .auto;
const color2 = Color.auto;
try expect(color1 == color2);
}
test "switch using enum literals" {
const color = Color.on;
const result = switch (color) {
.auto => false,
.on => true,
.off => false,
};
try expect(result);
}
Оболочка
$ zig test test_enum_literals.zig
1/2 test_enum_literals.test.enum literals...OK
2/2 test_enum_literals.test.switch using enum literals...OK
All 2 tests passed.
Неполное перечисление
Неполное перечисление можно создать, добавив поле с trailing _. Перечисление должно указать тип тега и не может использовать все значения перечисления.
@enumFromInt для неполного перечисления включает семантику безопасности @intCast для целочисленного типа тега, но в дальнейшем всегда приводит к определенному значению перечисления.
Переключатель для неполного перечисления может включать _ разветвление в качестве альтернативы else разветвлению. С _ разветвлением компилятор выдаёт ошибку, если все известные имена тегов не обрабатываются переключателем.
$ zig test test_switch_non-exhaustive.zig
1/1 test_switch_non-exhaustive.test.switch on non-exhaustive enum...OK
All 1 tests passed.
union
Обычный union определяет набор возможных типов, которые может принимать значение, как список полей. Только одно поле может быть активным в данный момент. Внутреннее представление обычных объединений не гарантируется. Обычные объединения не могут быть использованы для повторной интерпретации памяти. Для этого используйте @ptrCast или extern union или packed union, которые имеют гарантированное расположение в памяти. Доступ к неактивному полю проверяется на безопасность, и считается неопределённым поведением:
test_wrong_union_access.zig
const Payload = union {
int: i64,
float: f64,
boolean: bool,
};
test "simple union" {
var payload = Payload{ .int = 1234 };
payload.float = 12.34;
}
Оболочка
$ zig test test_wrong_union_access.zig
1/1 test_wrong_union_access.test.simple union...thread 3579408 panic: access of union field 'float' while field 'int' is active
/home/andy/src/zig/doc/langref/test_wrong_union_access.zig:8:12: 0x103ce87 in test.simple union (test)
payload.float = 12.34;
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x1048070 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x103e08b in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103d419 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103cf81 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/bb9968225995fac0bbc9f2116e8583c2/test
Вы можете активировать другое поле, присвоив всё объединение:
Для инициализации объединения, когда тег — известное во время компиляции имя, см. @unionInit.
Меченые объединения
Объединения могут быть объявлены с типом тега перечисления. Это превращает объединение в меченное объединение, что делает его пригодным для использования с выражениями switch. Меченые объединения преобразуются в свой тип тега: Преобразование типов: объединения и перечисления.
$ zig test test_tagged_union.zig
1/2 test_tagged_union.test.switch on tagged union...OK
2/2 test_tagged_union.test.get tag type...OK
All 2 tests passed.
Для изменения данных меченного объединения в выражении switch, поместите * перед именем переменной, чтобы сделать её указателем:
У packedunion есть чётко определённое расположение в памяти и оно может быть в упакованной структуре.
Анонимные литералы объединений
Анонимные литералы структур синтаксис может быть использован для инициализации объединений без указания типа:
test_anonymous_union.zig
const std = @import("std");
const expect = std.testing.expect;
const Number = union {
int: i32,
float: f64,
};
test "anonymous union literal syntax" {
const i: Number = .{ .int = 42 };
const f = makeNumber();
try expect(i.int == 42);
try expect(f.float == 12.34);
}
fn makeNumber() Number {
return .{ .float = 12.34 };
}
Оболочка
$ zig test test_anonymous_union.zig
1/1 test_anonymous_union.test.anonymous union literal syntax...OK
All 1 tests passed.
opaque
opaque {} объявляет новый тип с неизвестным (но ненулевым) размером и выравниванием. Он может содержать объявления, аналогичные структурам, объединениям и перечислениям.
Это обычно используется для обеспечения безопасности типов при взаимодействии с кодом C, который не раскрывает подробности структуры. Пример:
$ zig test test_opaque.zigdoc/langref/test_opaque.zig:6:9: error: expected type '*test_opaque.Derp', found '*test_opaque.Wat'
bar(w);
^
doc/langref/test_opaque.zig:6:9: note: pointer type child 'test_opaque.Wat' cannot cast into pointer type child 'test_opaque.Derp'
doc/langref/test_opaque.zig:2:13: note: opaque declared here
const Wat = opaque {};
^~~~~~~~~
doc/langref/test_opaque.zig:1:14: note: opaque declared here
const Derp = opaque {};
^~~~~~~~~
doc/langref/test_opaque.zig:4:18: note: parameter type declared here
extern fn bar(d: *Derp) void;
^~~~~
referenced by:
test.call foo: doc/langref/test_opaque.zig:10:5
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Блоки
Блоки используются для ограничения области видимости объявлений переменных:
test_blocks.zig
test "access variable after block scope" {
{
var x: i32 = 1;
_ = &x;
}
x += 1;
}
Оболочка
$ zig test test_blocks.zigdoc/langref/test_blocks.zig:6:5: error: use of undeclared identifier 'x'
x += 1;
^
Блоки являются выражениями. При маркировке, break можно использовать для возврата значения из блока:
test_labeled_break.zig
const std = @import("std");
const expect = std.testing.expect;
test "labeled break from labeled block expression" {
var y: i32 = 123;
const x = blk: {
y += 1;
break :blk y;
};
try expect(x == 124);
try expect(y == 124);
}
Оболочка
$ zig test test_labeled_break.zig
1/1 test_labeled_break.test.labeled break from labeled block expression...OK
All 1 tests passed.
Идентификаторы никогда не допускается «скрывать» другие идентификаторы, используя то же имя:
test_shadowing.zig
const pi = 3.14;
test "inside test block" {
// Let's even go inside another block
{
var pi: i32 = 1234;
}
}
Оболочка
$ zig test test_shadowing.zigdoc/langref/test_shadowing.zig:6:13: error: local variable shadows declaration of 'pi'
var pi: i32 = 1234;
^~
doc/langref/test_shadowing.zig:1:1: note: declared here
const pi = 3.14;
^~~~~~~~~~~~~~~
Из-за этого, когда вы читаете код Zig, вы всегда можете полагаться на то, что идентификатор будет иметь одно и то же значение в рамках области его определения. Обратите внимание, что вы можете использовать то же имя, если области определения различаются:
test_scopes.zig
test "separate scopes" {
{
const pi = 3.14;
_ = pi;
}
{
var pi: bool = true;
_ = π
}
}
Оболочка
$ zig test test_scopes.zig
1/1 test_scopes.test.separate scopes...OK
All 1 tests passed.
$ zig test test_empty_block.zig
1/1 test_empty_block.test_0...OK
All 1 tests passed.
switch
test_switch.zig
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
test "switch simple" {
const a: u64 = 10;
const zz: u64 = 103;
// All branches of a switch expression must be able to be coerced to a
// common type.
//
// Branches cannot fallthrough. If fallthrough behavior is desired, combine
// the cases and use an if.
const b = switch (a) {
// Multiple cases can be combined via a ','
1, 2, 3 => 0,
// Ranges can be specified using the ... syntax. These are inclusive
// of both ends.
5...100 => 1,
// Branches can be arbitrarily complex.
101 => blk: {
const c: u64 = 5;
break :blk c * 2 + 1;
},
// Switching on arbitrary expressions is allowed as long as the
// expression is known at compile-time.
zz => zz,
blk: {
const d: u32 = 5;
const e: u32 = 100;
break :blk d + e;
} => 107,
// The else branch catches everything not already captured.
// Else branches are mandatory unless the entire range of values
// is handled.
else => 9,
};
try expect(b == 1);
}
// Switch expressions can be used outside a function:
const os_msg = switch (builtin.target.os.tag) {
.linux => "we found a linux user",
else => "not a linux user",
};
// Inside a function, switch statements implicitly are compile-time
// evaluated if the target expression is compile-time known.
test "switch inside function" {
switch (builtin.target.os.tag) {
.fuchsia => {
// On an OS other than fuchsia, block is not even analyzed,
// so this compile error is not triggered.
// On fuchsia this compile error would be triggered.
@compileError("fuchsia not supported");
},
else => {},
}
}
Оболочка
$ zig test test_switch.zig
1/2 test_switch.test.switch simple...OK
2/2 test_switch.test.switch inside function...OK
All 2 tests passed.
switch можно использовать для захвата значений полей меченного объединения. Изменения значений полей можно выполнить, поместив * перед именем переменной захвата, превратив его в указатель.
test_switch_tagged_union.zig
const expect = @import("std").testing.expect;
test "switch on tagged union" {
const Point = struct {
x: u8,
y: u8,
};
const Item = union(enum) {
a: u32,
c: Point,
d,
e: u32,
};
var a = Item{ .c = Point{ .x = 1, .y = 2 } };
// Switching on more complex enums is allowed.
const b = switch (a) {
// A capture group is allowed on a match, and will return the enum
// value matched. If the payload types of both cases are the same
// they can be put into the same switch prong.
Item.a, Item.e => |item| item,
// A reference to the matched value can be obtained using `*` syntax.
Item.c => |*item| blk: {
item.*.x += 1;
break :blk 6;
},
// No else is required if the types cases was exhaustively handled
Item.d => 8,
};
try expect(b == 6);
try expect(a.c.x == 2);
}
Оболочка
$ zig test test_switch_tagged_union.zig
1/1 test_switch_tagged_union.test.switch on tagged union...OK
All 1 tests passed.
Когда выражение switch не имеет else раздела, оно должно исчерпывающе перечислить все возможные значения. Отсутствие этого является ошибкой компиляции:
test_unhandled_enumeration_value.zig
const Color = enum {
auto,
off,
on,
};
test "exhaustive switching" {
const color = Color.off;
switch (color) {
Color.auto => {},
Color.on => {},
}
}
Оболочка
$ zig test test_unhandled_enumeration_value.zigdoc/langref/test_unhandled_enumeration_value.zig:9:5: error: switch must handle all possibilities
switch (color) {
^~~~~~
doc/langref/test_unhandled_enumeration_value.zig:3:5: note: unhandled enumeration value: 'off'
off,
^~~
doc/langref/test_unhandled_enumeration_value.zig:1:15: note: enum 'test_unhandled_enumeration_value.Color' declared here
const Color = enum {
^~~~
inlineelse ветви могут использоваться в качестве безопасной альтернативы inlinefor циклам:
test_inline_else.zig
const std = @import("std");
const expect = std.testing.expect;
const SliceTypeA = extern struct {
len: usize,
ptr: [*]u32,
};
const SliceTypeB = extern struct {
ptr: [*]SliceTypeA,
len: usize,
};
const AnySlice = union(enum) {
a: SliceTypeA,
b: SliceTypeB,
c: []const u8,
d: []AnySlice,
};
fn withFor(any: AnySlice) usize {
const Tag = @typeInfo(AnySlice).Union.tag_type.?;
inline for (@typeInfo(Tag).Enum.fields) |field| {
// With `inline for` the function gets generated as
// a series of `if` statements relying on the optimizer
// to convert it to a switch.
if (field.value == @intFromEnum(any)) {
return @field(any, field.name).len;
}
}
// When using `inline for` the compiler doesn't know that every
// possible case has been handled requiring an explicit `unreachable`.
unreachable;
}
fn withSwitch(any: AnySlice) usize {
return switch (any) {
// With `inline else` the function is explicitly generated
// as the desired switch and the compiler can check that
// every possible case is handled.
inline else => |slice| slice.len,
};
}
test "inline for and inline else similarity" {
const any = AnySlice{ .c = "hello" };
try expect(withFor(any) == 5);
try expect(withSwitch(any) == 5);
}
Оболочка
$ zig test test_inline_else.zig
1/1 test_inline_else.test.inline for and inline else similarity...OK
All 1 tests passed.
При использовании встроенной ветви switch по объединению можно использовать дополнительный захват для получения значения тега перечисления объединения.
test_inline_switch_union_tag.zig
const std = @import("std");
const expect = std.testing.expect;
const U = union(enum) {
a: u32,
b: f32,
};
fn getNum(u: U) u32 {
switch (u) {
// Here `num` is a runtime-known value that is either
// `u.a` or `u.b` and `tag` is `u`'s comptime-known tag value.
inline else => |num, tag| {
if (tag == .b) {
return @intFromFloat(num);
}
return num;
},
}
}
test "test" {
const u = U{ .b = 42 };
try expect(getNum(u) == 42);
}
Оболочка
$ zig test test_inline_switch_union_tag.zig
1/1 test_inline_switch_union_tag.test.test...OK
All 1 tests passed.
Цикл while используется для многократного выполнения выражения до тех пор, пока какое-то условие не станет ложным.
test_while.zig
const expect = @import("std").testing.expect;
test "while basic" {
var i: usize = 0;
while (i < 10) {
i += 1;
}
try expect(i == 10);
}
Оболочка
$ zig test test_while.zig
1/1 test_while.test.while basic...OK
All 1 tests passed.
Используйте break для выхода из цикла while раньше времени.
test_while_break.zig
const expect = @import("std").testing.expect;
test "while break" {
var i: usize = 0;
while (true) {
if (i == 10)
break;
i += 1;
}
try expect(i == 10);
}
Оболочка
$ zig test test_while_break.zig
1/1 test_while_break.test.while break...OK
All 1 tests passed.
Используйте continue для возвращения к началу цикла.
test_while_continue.zig
const expect = @import("std").testing.expect;
test "while continue" {
var i: usize = 0;
while (true) {
i += 1;
if (i < 10)
continue;
break;
}
try expect(i == 10);
}
Оболочка
$ zig test test_while_continue.zig
1/1 test_while_continue.test.while continue...OK
All 1 tests passed.
Циклы while поддерживают выражение continue, которое выполняется при продолжении цикла. Ключевое слово continue учитывает это выражение.
test_while_continue_expression.zig
const expect = @import("std").testing.expect;
test "while loop continue expression" {
var i: usize = 0;
while (i < 10) : (i += 1) {}
try expect(i == 10);
}
test "while loop continue expression, more complicated" {
var i: usize = 1;
var j: usize = 1;
while (i * j < 2000) : ({
i *= 2;
j *= 3;
}) {
const my_ij = i * j;
try expect(my_ij < 2000);
}
}
Оболочка
$ zig test test_while_continue_expression.zig
1/2 test_while_continue_expression.test.while loop continue expression...OK
2/2 test_while_continue_expression.test.while loop continue expression, more complicated...OK
All 2 tests passed.
Циклы while являются выражениями. Результатом выражения является результат else раздела цикла while, который выполняется, когда условие цикла while проверяется как ложное.
break, как и return, принимает параметр значения. Это результат while выражения. Когда вы break из цикла while, else раздел не оценивается.
test_while_else.zig
const expect = @import("std").testing.expect;
test "while else" {
try expect(rangeHasNumber(0, 10, 5));
try expect(!rangeHasNumber(0, 10, 15));
}
fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
var i = begin;
return while (i < end) : (i += 1) {
if (i == number) {
break true;
}
} else false;
}
Оболочка
$ zig test test_while_else.zig
1/1 test_while_else.test.while else...OK
All 1 tests passed.
Цикл while с меткой
Когда цикл while имеет метку, к нему можно обратиться из break или continue внутри вложенного цикла:
test_while_nested_break.zig
test "nested break" {
outer: while (true) {
while (true) {
break :outer;
}
}
}
test "nested continue" {
var i: usize = 0;
outer: while (i < 10) : (i += 1) {
while (true) {
continue :outer;
}
}
}
Оболочка
$ zig test test_while_nested_break.zig
1/2 test_while_nested_break.test.nested break...OK
2/2 test_while_nested_break.test.nested continue...OK
All 2 tests passed.
Цикл while с необязательными значениями
Так же, как выражения if, циклы while могут принимать необязательное значение в качестве условия и захватывать полезную нагрузку. При встрече null цикл завершается.
Когда синтаксис |x| присутствует в while выражении, условие цикла while должно иметь тип необязательного значения.
Раздел else разрешен в итерации с необязательными значениями. В этом случае он будет выполнен при первой встрече null.
test_while_null_capture.zig
const expect = @import("std").testing.expect;
test "while null capture" {
var sum1: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum1 += value;
}
try expect(sum1 == 3);
// null capture with an else block
var sum2: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum2 += value;
} else {
try expect(sum2 == 3);
}
// null capture with a continue expression
var i: u32 = 0;
var sum3: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| : (i += 1) {
sum3 += value;
}
try expect(i == 3);
}
var numbers_left: u32 = undefined;
fn eventuallyNullSequence() ?u32 {
return if (numbers_left == 0) null else blk: {
numbers_left -= 1;
break :blk numbers_left;
};
}
Оболочка
$ zig test test_while_null_capture.zig
1/1 test_while_null_capture.test.while null capture...OK
All 1 tests passed.
Цикл while с объединениями ошибок
Так же, как выражения if, циклы while могут принимать объединение ошибок в качестве условия и захватывать полезную нагрузку или код ошибки. Когда условие приводит к коду ошибки, выполняется раздел else, и цикл завершается.
Когда синтаксис else |x| присутствует в while выражении, условие цикла while должно иметь тип объединения ошибок.
$ zig test test_while_error_capture.zig
1/1 test_while_error_capture.test.while error union capture...OK
All 1 tests passed.
Встроенный цикл while
Циклы while могут быть встроенными. Это приводит к развёртыванию цикла, что позволяет коду выполнять некоторые операции, которые работают только во время компиляции, например, использовать типы как значения первого класса.
test_inline_while.zig
const expect = @import("std").testing.expect;
test "inline while loop" {
comptime var i = 0;
var sum: usize = 0;
inline while (i < 3) : (i += 1) {
const T = switch (i) {
0 => f32,
1 => i8,
2 => bool,
else => unreachable,
};
sum += typeNameLength(T);
}
try expect(sum == 9);
}
fn typeNameLength(comptime T: type) usize {
return @typeName(T).len;
}
Оболочка
$ zig test test_inline_while.zig
1/1 test_inline_while.test.inline while loop...OK
All 1 tests passed.
Рекомендуется использовать встроенные циклы только по одной из следующих причин:
Вам нужен цикл, который выполняется во время comptime для работы семантики.
У вас есть бенчмарк, доказывающий, что принудительное развёртывание цикла таким образом измеряемо быстрее.
const expect = @import("std").testing.expect;
test "for basics" {
const items = [_]i32{ 4, 5, 3, 4, 0 };
var sum: i32 = 0;
// For loops iterate over slices and arrays.
for (items) |value| {
// Break and continue are supported.
if (value == 0) {
continue;
}
sum += value;
}
try expect(sum == 16);
// To iterate over a portion of a slice, reslice.
for (items[0..1]) |value| {
sum += value;
}
try expect(sum == 20);
// To access the index of iteration, specify a second condition as well
// as a second capture value.
var sum2: i32 = 0;
for (items, 0..) |_, i| {
try expect(@TypeOf(i) == usize);
sum2 += @as(i32, @intCast(i));
}
try expect(sum2 == 10);
// To iterate over consecutive integers, use the range syntax.
// Unbounded range is always a compile error.
var sum3: usize = 0;
for (0..5) |i| {
sum3 += i;
}
try expect(sum3 == 10);
}
test "multi object for" {
const items = [_]usize{ 1, 2, 3 };
const items2 = [_]usize{ 4, 5, 6 };
var count: usize = 0;
// Iterate over multiple objects.
// All lengths must be equal at the start of the loop, otherwise detectable
// illegal behavior occurs.
for (items, items2) |i, j| {
count += i + j;
}
try expect(count == 21);
}
test "for reference" {
var items = [_]i32{ 3, 4, 2 };
// Iterate over the slice by reference by
// specifying that the capture value is a pointer.
for (&items) |*value| {
value.* += 1;
}
try expect(items[0] == 4);
try expect(items[1] == 5);
try expect(items[2] == 3);
}
test "for else" {
// For allows an else attached to it, the same as a while loop.
const items = [_]?i32{ 3, 4, null, 5 };
// For loops can also be used as expressions.
// Similar to while loops, when you break from a for loop, the else branch is not evaluated.
var sum: i32 = 0;
const result = for (items) |value| {
if (value != null) {
sum += value.?;
}
} else blk: {
try expect(sum == 12);
break :blk sum;
};
try expect(result == 12);
}
Когда цикл for имеет метку, к нему можно обратиться из break или continue внутри вложенного цикла:
test_for_nested_break.zig
const std = @import("std");
const expect = std.testing.expect;
test "nested break" {
var count: usize = 0;
outer: for (1..6) |_| {
for (1..6) |_| {
count += 1;
break :outer;
}
}
try expect(count == 1);
}
test "nested continue" {
var count: usize = 0;
outer: for (1..9) |_| {
for (1..6) |_| {
count += 1;
continue :outer;
}
}
try expect(count == 8);
}
Оболочка
$ zig test test_for_nested_break.zig
1/2 test_for_nested_break.test.nested break...OK
2/2 test_for_nested_break.test.nested continue...OK
All 2 tests passed.
Встроенный цикл for
Циклы for могут быть встроенными. Это приводит к развёртыванию цикла, что позволяет коду выполнять некоторые операции, которые работают только во время компиляции, например, использовать типы как значения первого класса. Значение захвата и значение итератора в встроенных циклах for известны во время компиляции.
// If expressions have three uses, corresponding to the three types:
// * bool
// * ?T
// * anyerror!T
const expect = @import("std").testing.expect;
test "if expression" {
// If expressions are used instead of a ternary expression.
const a: u32 = 5;
const b: u32 = 4;
const result = if (a != b) 47 else 3089;
try expect(result == 47);
}
test "if boolean" {
// If expressions test boolean conditions.
const a: u32 = 5;
const b: u32 = 4;
if (a != b) {
try expect(true);
} else if (a == 9) {
unreachable;
} else {
unreachable;
}
}
test "if error union" {
// If expressions test for errors.
// Note the |err| capture on the else.
const a: anyerror!u32 = 0;
if (a) |value| {
try expect(value == 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!u32 = error.BadValue;
if (b) |value| {
_ = value;
unreachable;
} else |err| {
try expect(err == error.BadValue);
}
// The else and |err| capture is strictly required.
if (a) |value| {
try expect(value == 0);
} else |_| {}
// To check only the error value, use an empty block expression.
if (b) |_| {} else |err| {
try expect(err == error.BadValue);
}
// Access the value by reference using a pointer capture.
var c: anyerror!u32 = 3;
if (c) |*value| {
value.* = 9;
} else |_| {
unreachable;
}
if (c) |value| {
try expect(value == 9);
} else |_| {
unreachable;
}
}
Оболочка
$ zig test test_if.zig
1/3 test_if.test.if expression...OK
2/3 test_if.test.if boolean...OK
3/3 test_if.test.if error union...OK
All 3 tests passed.
if с необязательными значениями
test_if_optionals.zig
const expect = @import("std").testing.expect;
test "if optional" {
// If expressions test for null.
const a: ?u32 = 0;
if (a) |value| {
try expect(value == 0);
} else {
unreachable;
}
const b: ?u32 = null;
if (b) |_| {
unreachable;
} else {
try expect(true);
}
// The else is not required.
if (a) |value| {
try expect(value == 0);
}
// To test against null only, use the binary equality operator.
if (b == null) {
try expect(true);
}
// Access the value by reference using a pointer capture.
var c: ?u32 = 3;
if (c) |*value| {
value.* = 2;
}
if (c) |value| {
try expect(value == 2);
} else {
unreachable;
}
}
test "if error union with optional" {
// If expressions test for errors before unwrapping optionals.
// The |optional_value| capture's type is ?u32.
const a: anyerror!?u32 = 0;
if (a) |optional_value| {
try expect(optional_value.? == 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!?u32 = null;
if (b) |optional_value| {
try expect(optional_value == null);
} else |_| {
unreachable;
}
const c: anyerror!?u32 = error.BadValue;
if (c) |optional_value| {
_ = optional_value;
unreachable;
} else |err| {
try expect(err == error.BadValue);
}
// Access the value by reference by using a pointer capture each time.
var d: anyerror!?u32 = 3;
if (d) |*optional_value| {
if (optional_value.*) |*value| {
value.* = 9;
}
} else |_| {
unreachable;
}
if (d) |optional_value| {
try expect(optional_value.? == 9);
} else |_| {
unreachable;
}
}
Оболочка
$ zig test test_if_optionals.zig
1/2 test_if_optionals.test.if optional...OK
2/2 test_if_optionals.test.if error union with optional...OK
All 2 tests passed.
В режиме Debug и ReleaseSafeunreachable вызывает panic с сообщением reached unreachable code.
В режиме ReleaseFast и ReleaseSmall оптимизатор использует предположение, что unreachable код никогда не будет достигнут для выполнения оптимизаций.
Основы
test_unreachable.zig
// unreachable is used to assert that control flow will never reach a
// particular location:
test "basic math" {
const x = 1;
const y = 2;
if (x + y != 3) {
unreachable;
}
}
Оболочка
$ zig test test_unreachable.zig
1/1 test_unreachable.test.basic math...OK
All 1 tests passed.
Фактически, вот как реализуется std.debug.assert.
test_assertion_failure.zig
// This is how std.debug.assert is implemented
fn assert(ok: bool) void {
if (!ok) unreachable; // assertion failure
}
// This test will fail because we hit unreachable.
test "this will fail" {
assert(false);
}
Оболочка
$ zig test test_assertion_failure.zig
1/1 test_assertion_failure.test.this will fail...thread 3571599 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_assertion_failure.zig:3:14: 0x103cd9d in assert (test)
if (!ok) unreachable; // assertion failure
^/home/andy/src/zig/doc/langref/test_assertion_failure.zig:8:11: 0x103cd5a in test.this will fail (test)
assert(false);
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x10479a0 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x103dbbb in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103d249 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103cdb1 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/a6b3ce5875a9e285c15739b2a1b30733/test
На этапе компиляции
test_comptime_unreachable.zig
const assert = @import("std").debug.assert;
test "type of unreachable" {
comptime {
// The type of unreachable is noreturn.
// However this assertion will still fail to compile because
// unreachable expressions are compile errors.
assert(@TypeOf(unreachable) == noreturn);
}
}
Оболочка
$ zig test test_comptime_unreachable.zigdoc/langref/test_comptime_unreachable.zig:10:16: error: unreachable code
assert(@TypeOf(unreachable) == noreturn);
^~~~~~~~~~~~~~~~~~~~
doc/langref/test_comptime_unreachable.zig:10:24: note: control flow is diverted here
assert(@TypeOf(unreachable) == noreturn);
^~~~~~~~~~~
$ zig test test_noreturn_from_exit.zig -target x86_64-windows --test-no-exec
Функции
test_functions.zig
const std = @import("std");
const builtin = @import("builtin");
const native_arch = builtin.cpu.arch;
const expect = std.testing.expect;
// Functions are declared like this
fn add(a: i8, b: i8) i8 {
if (a == 0) {
return b;
}
return a + b;
}
// The export specifier makes a function externally visible in the generated
// object file, and makes it use the C ABI.
export fn sub(a: i8, b: i8) i8 {
return a - b;
}
// The extern specifier is used to declare a function that will be resolved
// at link time, when linking statically, or at runtime, when linking
// dynamically. The quoted identifier after the extern keyword specifies
// the library that has the function. (e.g. "c" -> libc.so)
// The callconv specifier changes the calling convention of the function.
const WINAPI: std.builtin.CallingConvention = if (native_arch == .x86) .Stdcall else .C;
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(WINAPI) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;
// The @setCold builtin tells the optimizer that a function is rarely called.
fn abort() noreturn {
@setCold(true);
while (true) {}
}
// The naked calling convention makes a function not have any function prologue or epilogue.
// This can be useful when integrating with assembly.
fn _start() callconv(.Naked) noreturn {
abort();
}
// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
inline fn shiftLeftOne(a: u32) u32 {
return a << 1;
}
// The pub specifier allows the function to be visible when importing.
// Another file can use @import and call sub2
pub fn sub2(a: i8, b: i8) i8 {
return a - b;
}
// Function pointers are prefixed with `*const `.
const Call2Op = *const fn (a: i8, b: i8) i8;
fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
return fnCall(op1, op2);
}
test "function" {
try expect(doOp(add, 5, 6) == 11);
try expect(doOp(sub2, 5, 6) == -1);
}
Оболочка
$ zig test test_functions.zig
1/1 test_functions.test.function...OK
All 1 tests passed.
Существует различие между телом функции и указателем на функцию. Тела функций — это типы только для comptime, в то время как указатели на функции могут быть известны во время выполнения.
Параметры, передаваемые по значению
Примитивные типы, такие как целые числа и числа с плавающей точкой, передаваемые в качестве параметров, копируются, а затем копия доступна в теле функции. Это называется "передачей по значению". Копирование примитивного типа практически бесплатно и обычно включает лишь установку регистра.
Структуры, объединения и массивы иногда могут быть более эффективно переданы по ссылке, так как копия может быть произвольно дорогой, в зависимости от размера. При передаче этих типов в качестве параметров Zig может выбрать копирование и передачу по значению или передачу по ссылке, какой способ будет быстрее. Это частично обусловлено тем, что параметры неизменяемы.
test_pass_by_reference_or_value.zig
const Point = struct {
x: i32,
y: i32,
};
fn foo(point: Point) i32 {
// Here, `point` could be a reference, or a copy. The function body
// can ignore the difference and treat it as a value. Be very careful
// taking the address of the parameter - it should be treated as if
// the address will become invalid when the function returns.
return point.x + point.y;
}
const expect = @import("std").testing.expect;
test "pass struct to function" {
try expect(foo(Point{ .x = 1, .y = 2 }) == 3);
}
Оболочка
$ zig test test_pass_by_reference_or_value.zig
1/1 test_pass_by_reference_or_value.test.pass struct to function...OK
All 1 tests passed.
Для внешних функций Zig следует C ABI для передачи структур и объединений по значению.
Вывод типа параметров функции
Параметры функции могут быть объявлены с anytype вместо типа. В этом случае типы параметров будут выведены при вызове функции. Используйте @TypeOf и @typeInfo для получения информации о выведенном типе.
$ zig test test_fn_type_inference.zig
1/1 test_fn_type_inference.test.fn type inference...OK
All 1 tests passed.
inline fn
Добавление ключевого слова inline к определению функции делает эту функцию семантически встраиваемой в месте вызова. Это не подсказка, которая может быть замечена оптимизирующими пасками, но это имеет последствия для типов и значений, участвующих в вызове функции.
В отличие от обычных вызовов функций, аргументы в месте вызова встроенной функции, которые известны на этапе компиляции, обрабатываются как параметры времени компиляции. Это потенциально может распространиться до возвращаемого значения:
inline_call.zig
test "inline function call" {
if (foo(1200, 34) != 1234) {
@compileError("bad");
}
}
inline fn foo(a: i32, b: i32) i32 {
return a + b;
}
Оболочка
$ zig test inline_call.zig
1/1 inline_call.test.inline function call...OK
All 1 tests passed.
Если inline удалено, тест завершается с ошибкой компиляции вместо прохождения.
В целом, лучше позволять компилятору решать, когда встраивать функцию, за исключением следующих случаев:
Для изменения количества кадров стека вызова в целях отладки.
Для принудительного распространения компиляционно-временного характера аргументов до возвращаемого значения функции, как в приведённом выше примере.
Измерения производительности в реальном мире требуют этого.
Обратите внимание, что inline фактически ограничивает возможности компилятора. Это может ухудшить размер бинарного файла, скорость компиляции и даже производительность во время выполнения.
$ zig test test_fn_reflection.zig
1/1 test_fn_reflection.test.fn reflection...OK
All 1 tests passed.
Ошибки
Тип множества ошибок
Множество ошибок похоже на перечисление. Однако каждому имени ошибки во всей компиляции присваивается целое число без знака, большее 0. Разрешено объявлять одно и то же имя ошибки более одного раза; в этом случае ему присваивается то же значение целого числа.
Тип множества ошибок по умолчанию — u16, хотя если максимальное количество различных значений ошибок задано через параметр командной строки --error-limit [число], будет использоваться целочисленный тип с минимальным числом битов, необходимым для представления всех значений ошибок.
Можно преобразовать ошибку из подмножества в надмножество:
$ zig test test_coerce_error_superset_to_subset.zigdoc/langref/test_coerce_error_superset_to_subset.zig:16:12: error: expected type 'error{OutOfMemory}', found 'error{AccessDenied,OutOfMemory,FileNotFound}'
return err;
^~~
doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.AccessDenied' not a member of destination error set
doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.FileNotFound' not a member of destination error set
doc/langref/test_coerce_error_superset_to_subset.zig:15:28: note: function return type declared here
fn foo(err: FileOpenError) AllocationError {
^~~~~~~~~~~~~~~
referenced by:
test.coerce superset to subset: doc/langref/test_coerce_error_superset_to_subset.zig:12:5
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Существует сокращение для объявления множества ошибок только с 1 значением и получения этого значения:
anyerror относится к глобальному множеству ошибок. Это множество ошибок, содержащее все ошибки во всём блоке компиляции. Оно является надмножеством всех других множеств ошибок и подмножеством ни одного из них.
Можно преобразовать любое множество ошибок в глобальное, и можно явно привести ошибку из глобального множества ошибок к неглобальному. Это вставляет утверждение уровня языка, чтобы убедиться, что значение ошибки фактически находится в целевом множестве ошибок.
Глобальное множество ошибок следует избегать, так как это мешает компилятору знать, какие ошибки возможны на этапе компиляции. Знание множества ошибок на этапе компиляции лучше для сгенерированной документации и полезных сообщений об ошибках, таких как забывание возможного значения ошибки в switch.
Тип объединения ошибок
Тип множества ошибок и обычный тип могут быть объединены с помощью бинарного оператора ! для создания типа объединения ошибок. Тип объединения ошибок вы, скорее всего, будете использовать чаще, чем тип множества ошибок сам по себе.
Вот функция для разбора строки в 64-битное целое число:
error_union_parsing_u64.zig
const std = @import("std");
const maxInt = std.math.maxInt;
pub fn parseU64(buf: []const u8, radix: u8) !u64 {
var x: u64 = 0;
for (buf) |c| {
const digit = charToDigit(c);
if (digit >= radix) {
return error.InvalidChar;
}
// x *= radix
var ov = @mulWithOverflow(x, radix);
if (ov[1] != 0) return error.OverFlow;
// x += digit
ov = @addWithOverflow(ov[0], digit);
if (ov[1] != 0) return error.OverFlow;
x = ov[0];
}
return x;
}
fn charToDigit(c: u8) u8 {
return switch (c) {
'0'...'9' => c - '0',
'A'...'Z' => c - 'A' + 10,
'a'...'z' => c - 'a' + 10,
else => maxInt(u8),
};
}
test "parse u64" {
const result = try parseU64("1234", 10);
try std.testing.expect(result == 1234);
}
Оболочка
$ zig test error_union_parsing_u64.zig
1/1 error_union_parsing_u64.test.parse u64...OK
All 1 tests passed.
Обратите внимание, что возвращаемый тип — !u64. Это означает, что функция либо возвращает целое число с 64 битами без знака, либо ошибку. Мы убрали множество ошибок слева от !, поэтому множество ошибок выводится.
В определении функции вы видите несколько операторов return, которые возвращают ошибку, а внизу оператор return, который возвращает u64. Оба типа преобразуются в anyerror!u64.
Вид использования этой функции зависит от того, что вы хотите сделать:
Вы хотите предоставить значение по умолчанию, если был возвращён ошибка.
Если была возвращена ошибка, вы хотите вернуть ту же ошибку.
Вы уверены, что ошибка не будет возвращена, поэтому хотите безоговорочно её раскрыть.
Вы хотите выполнить разные действия для каждой возможной ошибки.
В этом коде, number будет равно успешно распарсенной строке или значению по умолчанию 13. Тип правой части бинарного оператора catch должен соответствовать типу объединения ошибок без обертки или быть типа noreturn.
Если вы хотите предоставить значение по умолчанию с catch после выполнения некоторой логики, вы можете объединить catch с именованными блоками:
handle_error_with_catch_block.zig.zig
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;
fn doAThing(str: []u8) void {
const number = parseU64(str, 10) catch blk: {
// do things
break :blk 13;
};
_ = number; // number is now initialized
}
try вычисляет выражение объединения ошибок. Если это ошибка, функция возвращает управление с той же ошибкой. В противном случае выражение результатом является значением без обертки.
Возможно, вы уверены, что выражение никогда не будет ошибкой. В этом случае вы можете сделать так:
const number = parseU64("1234", 10) catchunreachable;
Здесь мы точно знаем, что "1234" будет успешно распарсен. Поэтому мы помещаем значение unreachable в правую часть. unreachable вызывает панику в режимах Debug и ReleaseSafe и неопределённое поведение в режимах ReleaseFast и ReleaseSmall. Таким образом, при отладке приложения, если здесь возникнет неожиданная ошибка, приложение завершится соответствующим образом.
Вы можете захотеть выполнить разные действия в разных ситуациях. Для этого мы объединяем выражение if и switch:
handle_all_error_scenarios.zig
fn doAThing(str: []u8) void {
if (parseU64(str, 10)) |number| {
doSomethingWithNumber(number);
} else |err| switch (err) {
error.Overflow => {
// handle overflow...
},
// we promise that InvalidChar won't happen (or crash in debug mode if it does)
error.InvalidChar => unreachable,
}
}
Наконец, вы можете обработать только некоторые ошибки. Для этого вы можете перехватить необработанные ошибки в случае else, который теперь содержит более узкий набор ошибок:
Другим компонентом обработки ошибок являются инструкции defer. Помимо безусловного defer, в Zig есть errdefer, который вычисляет отложенное выражение при выходе из блока, если и только если функция вернула ошибку из этого блока.
Пример:
errdefer_example.zig
fn createFoo(param: i32) !Foo {
const foo = try tryToAllocateFoo();
// now we have allocated foo. we need to free it if the function fails.
// but we want to return it if the function succeeds.
errdefer deallocateFoo(foo);
const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
// tmp_buf is truly a temporary resource, and we for sure want to clean it up
// before this block leaves scope
defer deallocateTmpBuffer(tmp_buf);
if (param > 1337) return error.InvalidParam;
// here the errdefer will not run since we're returning success from the function.
// but the defer will run!
return foo;
}
Преимущество этого состоит в том, что вы получаете надёжную обработку ошибок без излишней громоздкости и когнитивных нагрузок, связанных с обеспечением охвата всех путей выхода. Код освобождения памяти всегда находится непосредственно после кода выделения памяти.
Следует отметить, что инструкции errdefer действуют только до конца блока, в котором они объявлены, и, следовательно, не выполняются, если ошибка возвращается за пределами этого блока:
test_errdefer_slip_ups.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
const Foo = struct {
data: u32,
};
fn tryToAllocateFoo(allocator: Allocator) !*Foo {
return allocator.create(Foo);
}
fn deallocateFoo(allocator: Allocator, foo: *Foo) void {
allocator.destroy(foo);
}
fn getFooData() !u32 {
return 666;
}
fn createFoo(allocator: Allocator, param: i32) !*Foo {
const foo = getFoo: {
var foo = try tryToAllocateFoo(allocator);
errdefer deallocateFoo(allocator, foo); // Only lasts until the end of getFoo
// Calls deallocateFoo on error
foo.data = try getFooData();
break :getFoo foo;
};
// Outside of the scope of the errdefer, so
// deallocateFoo will not be called here
if (param > 1337) return error.InvalidParam;
return foo;
}
test "createFoo" {
try std.testing.expectError(error.InvalidParam, createFoo(std.testing.allocator, 2468));
}
Shell
$ zig test test_errdefer_slip_ups.zig
1/1 test_errdefer_slip_ups.test.createFoo...OK
[gpa] (err): memory address 0x7f11f521a000 leaked:
/home/andy/src/zig/doc/langref/test_errdefer_slip_ups.zig:9:28: 0x103d3cf in tryToAllocateFoo (test)
return allocator.create(Foo);
^/home/andy/src/zig/doc/langref/test_errdefer_slip_ups.zig:22:39: 0x103d5e5 in createFoo (test)
var foo = try tryToAllocateFoo(allocator);
^/home/andy/src/zig/doc/langref/test_errdefer_slip_ups.zig:39:62: 0x103d82d in test.createFoo (test)
try std.testing.expectError(error.InvalidParam, createFoo(std.testing.allocator, 2468));
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104d2a0 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x104340b in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103f9f9 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103f561 in _start (test)
asm volatile (switch (native_arch) {
^
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/58c079cb550addefaa354f72d736afd7/test
Чтобы убедиться, что deallocateFoo вызывается при возврате ошибки, вы должны добавить errdefer за пределами блока:
test_errdefer_block.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
const Foo = struct {
data: u32,
};
fn tryToAllocateFoo(allocator: Allocator) !*Foo {
return allocator.create(Foo);
}
fn deallocateFoo(allocator: Allocator, foo: *Foo) void {
allocator.destroy(foo);
}
fn getFooData() !u32 {
return 666;
}
fn createFoo(allocator: Allocator, param: i32) !*Foo {
const foo = getFoo: {
var foo = try tryToAllocateFoo(allocator);
errdefer deallocateFoo(allocator, foo);
foo.data = try getFooData();
break :getFoo foo;
};
// This lasts for the rest of the function
errdefer deallocateFoo(allocator, foo);
// Error is now properly handled by errdefer
if (param > 1337) return error.InvalidParam;
return foo;
}
test "createFoo" {
try std.testing.expectError(error.InvalidParam, createFoo(std.testing.allocator, 2468));
}
Shell
$ zig test test_errdefer_block.zig
1/1 test_errdefer_block.test.createFoo...OK
All 1 tests passed.
Факт, что errdefer действуют только в пределах блока, особенно важен при использовании циклов:
test_errdefer_loop_leak.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
const Foo = struct { data: *u32 };
fn getData() !u32 {
return 666;
}
fn genFoos(allocator: Allocator, num: usize) ![]Foo {
const foos = try allocator.alloc(Foo, num);
errdefer allocator.free(foos);
for (foos, 0..) |*foo, i| {
foo.data = try allocator.create(u32);
// This errdefer does not last between iterations
errdefer allocator.destroy(foo.data);
// The data for the first 3 foos will be leaked
if (i >= 3) return error.TooManyFoos;
foo.data.* = try getData();
}
return foos;
}
test "genFoos" {
try std.testing.expectError(error.TooManyFoos, genFoos(std.testing.allocator, 5));
}
Shell
$ zig test test_errdefer_loop_leak.zig
1/1 test_errdefer_loop_leak.test.genFoos...OK
[gpa] (err): memory address 0x7f57c7578000 leaked:
/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:15:40: 0x103d7a6 in genFoos (test)
foo.data = try allocator.create(u32);
^/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:29:59: 0x103e0dd in test.genFoos (test)
try std.testing.expectError(error.TooManyFoos, genFoos(std.testing.allocator, 5));
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104e010 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x1043eab in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x10402a9 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103fe11 in _start (test)
asm volatile (switch (native_arch) {
^
[gpa] (err): memory address 0x7f57c7578004 leaked:
/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:15:40: 0x103d7a6 in genFoos (test)
foo.data = try allocator.create(u32);
^/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:29:59: 0x103e0dd in test.genFoos (test)
try std.testing.expectError(error.TooManyFoos, genFoos(std.testing.allocator, 5));
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104e010 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x1043eab in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x10402a9 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103fe11 in _start (test)
asm volatile (switch (native_arch) {
^
[gpa] (err): memory address 0x7f57c7578008 leaked:
/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:15:40: 0x103d7a6 in genFoos (test)
foo.data = try allocator.create(u32);
^/home/andy/src/zig/doc/langref/test_errdefer_loop_leak.zig:29:59: 0x103e0dd in test.genFoos (test)
try std.testing.expectError(error.TooManyFoos, genFoos(std.testing.allocator, 5));
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x104e010 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x1043eab in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x10402a9 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103fe11 in _start (test)
asm volatile (switch (native_arch) {
^
All 1 tests passed.
3 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/29fcda275b7c426418534b679850fa2e/test
При работе с кодом, который выделяет память в цикле, необходимо уделить особое внимание, чтобы избежать утечки памяти при возврате ошибки:
test_errdefer_loop.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
const Foo = struct { data: *u32 };
fn getData() !u32 {
return 666;
}
fn genFoos(allocator: Allocator, num: usize) ![]Foo {
const foos = try allocator.alloc(Foo, num);
errdefer allocator.free(foos);
// Used to track how many foos have been initialized
// (including their data being allocated)
var num_allocated: usize = 0;
errdefer for (foos[0..num_allocated]) |foo| {
allocator.destroy(foo.data);
};
for (foos, 0..) |*foo, i| {
foo.data = try allocator.create(u32);
num_allocated += 1;
if (i >= 3) return error.TooManyFoos;
foo.data.* = try getData();
}
return foos;
}
test "genFoos" {
try std.testing.expectError(error.TooManyFoos, genFoos(std.testing.allocator, 5));
}
Shell
$ zig test test_errdefer_loop.zig
1/1 test_errdefer_loop.test.genFoos...OK
All 1 tests passed.
Несколько дополнительных моментов по обработке ошибок:
Эти примитивы обеспечивают достаточную выразительность, что пропуск проверки на ошибку может стать ошибкой компиляции. Если вы действительно хотите проигнорировать ошибку, вы можете добавить catchunreachable и получить дополнительное преимущество — завершение программы с ошибкой в режимах Debug и ReleaseSafe, если ваше предположение было неверным.
Поскольку Zig понимает типы ошибок, он может преднастроить ветви в пользу того, что ошибки не возникнут. Это небольшое оптимизационное преимущество, недоступное в других языках.
Объединение объединений ошибок создается с помощью бинарного оператора !. Вы можете использовать рефлексию на этапе компиляции для доступа к типу дочернего элемента объединения ошибок:
test_error_union.zig
const expect = @import("std").testing.expect;
test "error union" {
var foo: anyerror!i32 = undefined;
// Coerce from child type of an error union:
foo = 1234;
// Coerce from an error set:
foo = error.SomeError;
// Use compile-time reflection to access the payload type of an error union:
try comptime expect(@typeInfo(@TypeOf(foo)).ErrorUnion.payload == i32);
// Use compile-time reflection to access the error set type of an error union:
try comptime expect(@typeInfo(@TypeOf(foo)).ErrorUnion.error_set == anyerror);
}
Shell
$ zig test test_error_union.zig
1/1 test_error_union.test.error union...OK
All 1 tests passed.
Используйте оператор || для объединения двух наборов ошибок вместе. Результирующий набор ошибок содержит ошибки обоих наборов ошибок. Комментарии к документации из левой части переопределяют комментарии к документации из правой части. В этом примере комментарии к документации для C.PathNotFound являются A doc comment.
Это особенно полезно для функций, возвращающих разные наборы ошибок в зависимости от comptime ветвей. Например, стандартная библиотека Zig использует LinuxFileOpenError || WindowsFileOpenError для набора ошибок при открытии файлов.
test_merging_error_sets.zig
const A = error{
NotDir,
/// A doc comment
PathNotFound,
};
const B = error{
OutOfMemory,
/// B doc comment
PathNotFound,
};
const C = A || B;
fn foo() C!void {
return error.NotDir;
}
test "merge error sets" {
if (foo()) {
@panic("unexpected");
} else |err| switch (err) {
error.OutOfMemory => @panic("unexpected"),
error.PathNotFound => @panic("unexpected"),
error.NotDir => {},
}
}
Shell
$ zig test test_merging_error_sets.zig
1/1 test_merging_error_sets.test.merge error sets...OK
All 1 tests passed.
Поскольку многие функции в Zig возвращают возможную ошибку, Zig поддерживает вывод набора ошибок. Чтобы вывести набор ошибок для функции, добавьте префикс ! к типу возврата функции, например !T:
test_inferred_error_sets.zig
// With an inferred error set
pub fn add_inferred(comptime T: type, a: T, b: T) !T {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
// With an explicit error set
pub fn add_explicit(comptime T: type, a: T, b: T) Error!T {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
const Error = error{
Overflow,
};
const std = @import("std");
test "inferred error set" {
if (add_inferred(u8, 255, 1)) |_| unreachable else |err| switch (err) {
error.Overflow => {}, // ok
}
}
Shell
$ zig test test_inferred_error_sets.zig
1/1 test_inferred_error_sets.test.inferred error set...OK
All 1 tests passed.
Когда функция имеет выведенный набор ошибок, эта функция становится универсальной, и поэтому становится сложнее выполнять с ней определённые действия, такие как получение указателя на функцию или обеспечение согласованности набора ошибок на разных целевых платформах сборки. Кроме того, выведенные наборы ошибок несовместимы с рекурсией.
В этих ситуациях рекомендуется использовать явный набор ошибок. Обычно можно начать с пустого набора ошибок и позволить ошибкам компиляции направить вас к завершению набора.
Эти ограничения могут быть устранены в будущей версии Zig.
Сведения об ошибках возврата
Сведения об ошибках возврата показывают все точки в коде, в которых ошибка возвращалась вызывающей функции. Это делает практичным использование try везде и при этом всё же позволяет узнать, что произошло, если ошибка доходит до выхода из вашего приложения.
$ zig build-exe error_return_trace.zig
$ ./error_return_trace
error: PermissionDenied
/home/andy/src/zig/doc/langref/error_return_trace.zig:34:5: 0x1034e08 in bang1 (error_return_trace)
return error.FileNotFound;
^/home/andy/src/zig/doc/langref/error_return_trace.zig:22:5: 0x1034f13 in baz (error_return_trace)
try bang1();
^/home/andy/src/zig/doc/langref/error_return_trace.zig:38:5: 0x1034f38 in bang2 (error_return_trace)
return error.PermissionDenied;
^/home/andy/src/zig/doc/langref/error_return_trace.zig:30:5: 0x1034fa3 in hello (error_return_trace)
try bang2();
^/home/andy/src/zig/doc/langref/error_return_trace.zig:17:31: 0x103505a in bar (error_return_trace)
error.FileNotFound => try hello(),
^/home/andy/src/zig/doc/langref/error_return_trace.zig:7:9: 0x1035140 in foo (error_return_trace)
try bar();
^/home/andy/src/zig/doc/langref/error_return_trace.zig:2:5: 0x1035198 in main (error_return_trace)
try foo(12);
^
Внимательно изучите этот пример. Это не стек вызовов.
Вы видите, что последней возвращённой ошибкой была PermissionDenied, но исходной ошибкой, которая всё это инициировала, была FileNotFound. В функции bar код обрабатывает исходный код ошибки и затем возвращает другой, из оператора switch. Сведения об ошибках возврата делают это ясным, в то время как стек вызовов выглядел бы так:
$ zig build-exe stack_trace.zig
$ ./stack_trace
thread 3570764 panic: PermissionDenied
/home/andy/src/zig/doc/langref/stack_trace.zig:38:5: 0x1039320 in bang2 (stack_trace)
@panic("PermissionDenied");
^/home/andy/src/zig/doc/langref/stack_trace.zig:30:10: 0x1068bd8 in hello (stack_trace)
bang2();
^/home/andy/src/zig/doc/langref/stack_trace.zig:17:14: 0x10392fc in bar (stack_trace)
hello();
^/home/andy/src/zig/doc/langref/stack_trace.zig:7:12: 0x103721c in foo (stack_trace)
bar();
^/home/andy/src/zig/doc/langref/stack_trace.zig:2:8: 0x103519d in main (stack_trace)
foo(12);
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034a49 in posixCallMainAndExit (stack_trace)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x10345b1 in _start (stack_trace)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Здесь стек вызовов не объясняет, как поток управления в bar добрался до вызова hello(). Для этого пришлось бы открыть отладчик или дополнительно инструментировать приложение, чтобы это выяснить. Сведения об ошибках возврата, с другой стороны, точно показывают, как ошибка поднялась вверх.
Эта функция отладки упрощает быструю итерацию над кодом, который надёжно обрабатывает все условия ошибок. Это означает, что разработчики Zig будут естественным образом писать правильный и надёжный код, чтобы повысить скорость разработки.
Есть несколько способов включить эту функцию отслеживания ошибок возврата:
Возврат ошибки из main
Ошибка попадает в catchunreachable и вы не переопределили обработчик паники по умолчанию
Использование errorReturnTrace для доступа к текущим сведениям о возврате ошибки. Вы можете использовать std.debug.dumpStackTrace для их печати. Эта функция возвращает известное на этапе компиляции значение null при сборке без поддержки отслеживания ошибок возврата.
Для анализа затрат на производительность существует два случая:
когда ошибки не возвращаются
когда возвращаются ошибки
В случае, когда ошибки не возвращаются, стоимость представляет собой одну операцию записи в память, только в первой не-ошибочной функции в графе вызовов, которая вызывает ошибочную функцию, т.е. когда функция, возвращающая void, вызывает функцию, возвращающую error. Это для инициализации этого структура в памяти стека:
Здесь N — максимальная глубина вызова функций, определённая анализом графа вызовов. Рекурсия игнорируется и учитывается как 2.
Указатель на StackTrace передаётся в качестве секретного параметра каждой функции, которая может вернуть ошибку, но он всегда является первым параметром, поэтому он, вероятно, может находиться в регистре и оставаться там.
На этом заканчивается путь, когда ошибки не возникают. Он практически бесплатен с точки зрения производительности.
При генерации кода для функции, возвращающей ошибку, непосредственно перед инструкцией return (только для инструкций return, возвращающих ошибки), Zig генерирует вызов этой функции:
zig_return_error_fn.zig
// marked as "no-inline" in LLVM IR
fn __zig_return_error(stack_trace: *StackTrace) void {
stack_trace.instruction_addresses[stack_trace.index] = @returnAddress();
stack_trace.index = (stack_trace.index + 1) % N;
}
Стоимость составляет 2 математические операции плюс некоторые чтение и запись в памяти. Доступ к памяти ограничен и должен оставаться в кэше в течение всего времени возвращения ошибки.
Что касается стоимости размера кода, 1 вызов функции перед оператором возврата — это не проблема. Тем не менее, у меня есть план по преобразованию вызова __zig_return_error в хвостовой вызов, что снижает стоимость размера кода до нуля. Оператор возврата в коде без отслеживания возврата ошибок может стать инструкцией перехода в коде с отслеживанием возврата ошибок.
Одна область, в которой Zig обеспечивает безопасность без ущерба для эффективности или читаемости, — это тип необязательных значений.
Вопросительный знак символизирует необязательный тип. Вы можете преобразовать тип в необязательный тип, добавив вопросительный знак перед ним, как в этом примере:
Теперь переменная optional_int может быть i32, или null.
Вместо целых чисел давайте поговорим о указателях. Нулевые ссылки являются источником многих исключений во время выполнения, и даже обвиняются в том, что являются худшей ошибкой компьютерных наук.
В Zig их нет.
Вместо этого вы можете использовать необязательный указатель. Он неявно компилируется в обычный указатель, поскольку мы знаем, что мы можем использовать 0 в качестве нулевого значения для необязательного типа. Но компилятор может проверить вашу работу и убедиться, что вы не присваиваете NULL тому, что не может быть NULL.
Обычно недостатком отсутствия NULL является то, что код становится более громоздким. Но давайте сравним некоторые эквивалентные фрагменты кода на C и Zig.
Задача: вызвать malloc, если результат равен NULL, вернуть NULL.
Код на C
call_malloc_in_c.c
// malloc prototype included for reference
void *malloc(size_t size);
struct Foo *do_a_thing(void) {
char *ptr = malloc(1234);
if (!ptr) return NULL;
// ...
}
Здесь Zig, по крайней мере, не менее удобен, чем C, если не более. И тип "ptr" — это [*]u8не?[*]u8. Ключевое слово orelse распаковывает необязательный тип, и поэтому ptr гарантированно не будет NULL повсюду, где он используется в функции.
Другая форма проверки на NULL, которую вы можете увидеть, выглядит следующим образом:
checking_null_in_c.c
void do_a_thing(struct Foo *foo) {
// do some stuff
if (foo) {
do_something_with_foo(foo);
}
// do some stuff
}
В Zig вы можете добиться того же результата:
checking_null_in_zig.zig
const Foo = struct {};
fn doSomethingWithFoo(foo: *Foo) void {
_ = foo;
}
fn doAThing(optional_foo: ?*Foo) void {
// do some stuff
if (optional_foo) |foo| {
doSomethingWithFoo(foo);
}
// do some stuff
}
Ещё раз, важно отметить, что внутри блока if foo больше не является необязательным указателем, а является указателем, который не может быть NULL.
Одним из преимуществ этого является то, что функции, принимающие указатели в качестве аргументов, могут быть аннотированы атрибутом "nonnull" — __attribute__((nonnull)) в GCC. Оптимизатор иногда может принимать лучшие решения, зная, что аргументы-указатели не могут быть NULL.
Необязательный тип создаётся путём добавления ? перед типом. Вы можете использовать рефлексию во время компиляции для доступа к дочернему типу необязательного:
test_optional_type.zig
const expect = @import("std").testing.expect;
test "optional type" {
// Declare an optional and coerce from null:
var foo: ?i32 = null;
// Coerce from child type of an optional
foo = 1234;
// Use compile-time reflection to access the child type of the optional:
try comptime expect(@typeInfo(@TypeOf(foo)).Optional.child == i32);
}
Оболочка
$ zig test test_optional_type.zig
1/1 test_optional_type.test.optional type...OK
All 1 tests passed.
Необязательный указатель гарантированно имеет тот же размер, что и указатель. Адрес необязательного указателя гарантированно равен 0.
test_optional_pointer.zig
const expect = @import("std").testing.expect;
test "optional pointers" {
// Pointers cannot be null. If you want a null pointer, use the optional
// prefix `?` to make the pointer type optional.
var ptr: ?*i32 = null;
var x: i32 = 1;
ptr = &x;
try expect(ptr.?.* == 1);
// Optional pointers are the same size as normal pointers, because pointer
// value 0 is used as the null value.
try expect(@sizeOf(?*i32) == @sizeOf(*i32));
}
Оболочка
$ zig test test_optional_pointer.zig
1/1 test_optional_pointer.test.optional pointers...OK
All 1 tests passed.
Приведение типов преобразует значение одного типа в другой. Zig имеет приведение типов для преобразований, которые, как известно, полностью безопасны и однозначны, и явные приведения типов для преобразований, которые вы не хотели бы случайно выполнять. Также существует третий вид преобразования типов, называемый разрешением типов-аналогов, в случае, когда тип результата должен быть определён на основе нескольких типов операндов.
$ zig test test_type_coercion.zig
1/3 test_type_coercion.test.type coercion - variable declaration...OK
2/3 test_type_coercion.test.type coercion - function call...OK
3/3 test_type_coercion.test.type coercion - @as builtin...OK
All 3 tests passed.
Приведение типов разрешено только в том случае, когда полностью понятно, как перейти от одного типа к другому, и преобразование гарантированно безопасно. Есть одно исключение — это указатели C.
Значения, которые имеют одинаковое представление во время выполнения, могут быть приведены к типу с большей строгостью квалификаторов, независимо от того, насколько вложены квалификаторы:
const — разрешено преобразование из non-const в const
volatile — разрешено преобразование из non-volatile в volatile
align — разрешено преобразование из большего к меньшему выравниванию
Целые числа приводятся к целочисленным типам, которые могут представлять каждое значение старого типа, и аналогично числа с плавающей точкой приводятся к типам чисел с плавающей точкой, которые могут представлять каждое значение старого типа.
const std = @import("std");
const expect = std.testing.expect;
// You can assign constant pointers to arrays to a slice with
// const modifier on the element type. Useful in particular for
// String literals.
test "*const [N]T to []const T" {
const x1: []const u8 = "hello";
const x2: []const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, x1, x2));
const y: []const f32 = &[2]f32{ 1.2, 3.4 };
try expect(y[0] == 1.2);
}
// Likewise, it works when the destination type is an error union.
test "*const [N]T to E![]const T" {
const x1: anyerror![]const u8 = "hello";
const x2: anyerror![]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, try x1, try x2));
const y: anyerror![]const f32 = &[2]f32{ 1.2, 3.4 };
try expect((try y)[0] == 1.2);
}
// Likewise, it works when the destination type is an optional.
test "*const [N]T to ?[]const T" {
const x1: ?[]const u8 = "hello";
const x2: ?[]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, x1.?, x2.?));
const y: ?[]const f32 = &[2]f32{ 1.2, 3.4 };
try expect(y.?[0] == 1.2);
}
// In this cast, the array length becomes the slice length.
test "*[N]T to []T" {
var buf: [5]u8 = "hello".*;
const x: []u8 = &buf;
try expect(std.mem.eql(u8, x, "hello"));
const buf2 = [2]f32{ 1.2, 3.4 };
const x2: []const f32 = &buf2;
try expect(std.mem.eql(f32, x2, &[2]f32{ 1.2, 3.4 }));
}
// Single-item pointers to arrays can be coerced to many-item pointers.
test "*[N]T to [*]T" {
var buf: [5]u8 = "hello".*;
const x: [*]u8 = &buf;
try expect(x[4] == 'o');
// x[5] would be an uncaught out of bounds pointer dereference!
}
// Likewise, it works when the destination type is an optional.
test "*[N]T to ?[*]T" {
var buf: [5]u8 = "hello".*;
const x: ?[*]u8 = &buf;
try expect(x.?[4] == 'o');
}
// Single-item pointers can be cast to len-1 single-item arrays.
test "*T to *[1]T" {
var x: i32 = 1234;
const y: *[1]i32 = &x;
const z: [*]i32 = y;
try expect(z[0] == 1234);
}
Оболочка
$ zig test test_coerce_slices_arrays_and_pointers.zig
1/7 test_coerce_slices_arrays_and_pointers.test.*const [N]T to []const T...OK
2/7 test_coerce_slices_arrays_and_pointers.test.*const [N]T to E![]const T...OK
3/7 test_coerce_slices_arrays_and_pointers.test.*const [N]T to ?[]const T...OK
4/7 test_coerce_slices_arrays_and_pointers.test.*[N]T to []T...OK
5/7 test_coerce_slices_arrays_and_pointers.test.*[N]T to [*]T...OK
6/7 test_coerce_slices_arrays_and_pointers.test.*[N]T to ?[*]T...OK
7/7 test_coerce_slices_arrays_and_pointers.test.*T to *[1]T...OK
All 7 tests passed.
$ zig test test_coerce_optional_wrapped_error_union.zig
1/1 test_coerce_optional_wrapped_error_union.test.coerce to optionals wrapped in error union...OK
All 1 tests passed.
Если число известно во время компиляции и может быть представлено в целевом типе, оно может быть приведено к нему:
test_coerce_large_to_small.zig
const std = @import("std");
const expect = std.testing.expect;
test "coercing large integer type to smaller one when value is comptime-known to fit" {
const x: u64 = 255;
const y: u8 = x;
try expect(y == 255);
}
Оболочка
$ zig test test_coerce_large_to_small.zig
1/1 test_coerce_large_to_small.test.coercing large integer type to smaller one when value is comptime-known to fit...OK
All 1 tests passed.
Объединения с метками могут быть приведены к перечислениям, и перечисления могут быть приведены к объединениям с метками, если они известны во время компиляции как поле объединения, имеющее только одно возможное значение, например, void:
$ zig test test_coerce_tuples_arrays.zig
1/1 test_coerce_tuples_arrays.test.coercion from homogenous tuple to array...OK
All 1 tests passed.
Явные преобразования типов
Явные преобразования типов выполняются с помощью встроенных функций. Некоторые явные преобразования безопасны; некоторые — нет. Некоторые явные преобразования выполняют утверждения на уровне языка; некоторые — нет. Некоторые явные преобразования являются пустыми операциями во время выполнения; некоторые — нет.
@bitCast — изменение типа, но сохранение битовой записи
Этот вид разрешения типов выбирает тип, в который все типы-собратьев могут быть приведены. Вот некоторые примеры:
test_peer_type_resolution.zig
const std = @import("std");
const expect = std.testing.expect;
const mem = std.mem;
test "peer resolve int widening" {
const a: i8 = 12;
const b: i16 = 34;
const c = a + b;
try expect(c == 46);
try expect(@TypeOf(c) == i16);
}
test "peer resolve arrays of different size to const slice" {
try expect(mem.eql(u8, boolToStr(true), "true"));
try expect(mem.eql(u8, boolToStr(false), "false"));
try comptime expect(mem.eql(u8, boolToStr(true), "true"));
try comptime expect(mem.eql(u8, boolToStr(false), "false"));
}
fn boolToStr(b: bool) []const u8 {
return if (b) "true" else "false";
}
test "peer resolve array and const slice" {
try testPeerResolveArrayConstSlice(true);
try comptime testPeerResolveArrayConstSlice(true);
}
fn testPeerResolveArrayConstSlice(b: bool) !void {
const value1 = if (b) "aoeu" else @as([]const u8, "zz");
const value2 = if (b) @as([]const u8, "zz") else "aoeu";
try expect(mem.eql(u8, value1, "aoeu"));
try expect(mem.eql(u8, value2, "zz"));
}
test "peer type resolution: ?T and T" {
try expect(peerTypeTAndOptionalT(true, false).? == 0);
try expect(peerTypeTAndOptionalT(false, false).? == 3);
comptime {
try expect(peerTypeTAndOptionalT(true, false).? == 0);
try expect(peerTypeTAndOptionalT(false, false).? == 3);
}
}
fn peerTypeTAndOptionalT(c: bool, b: bool) ?usize {
if (c) {
return if (b) null else @as(usize, 0);
}
return @as(usize, 3);
}
test "peer type resolution: *[0]u8 and []const u8" {
try expect(peerTypeEmptyArrayAndSlice(true, "hi").len == 0);
try expect(peerTypeEmptyArrayAndSlice(false, "hi").len == 1);
comptime {
try expect(peerTypeEmptyArrayAndSlice(true, "hi").len == 0);
try expect(peerTypeEmptyArrayAndSlice(false, "hi").len == 1);
}
}
fn peerTypeEmptyArrayAndSlice(a: bool, slice: []const u8) []const u8 {
if (a) {
return &[_]u8{};
}
return slice[0..1];
}
test "peer type resolution: *[0]u8, []const u8, and anyerror![]u8" {
{
var data = "hi".*;
const slice = data[0..];
try expect((try peerTypeEmptyArrayAndSliceAndError(true, slice)).len == 0);
try expect((try peerTypeEmptyArrayAndSliceAndError(false, slice)).len == 1);
}
comptime {
var data = "hi".*;
const slice = data[0..];
try expect((try peerTypeEmptyArrayAndSliceAndError(true, slice)).len == 0);
try expect((try peerTypeEmptyArrayAndSliceAndError(false, slice)).len == 1);
}
}
fn peerTypeEmptyArrayAndSliceAndError(a: bool, slice: []u8) anyerror![]u8 {
if (a) {
return &[_]u8{};
}
return slice[0..1];
}
test "peer type resolution: *const T and ?*T" {
const a: *const usize = @ptrFromInt(0x123456780);
const b: ?*usize = @ptrFromInt(0x123456780);
try expect(a == b);
try expect(b == a);
}
test "peer type resolution: error union switch" {
// The non-error and error cases are only peers if the error case is just a switch expression;
// the pattern `if (x) {...} else |err| blk: { switch (err) {...} }` does not consider the
// non-error and error case to be peers.
var a: error{ A, B, C }!u32 = 0;
_ = &a;
const b = if (a) |x|
x + 3
else |err| switch (err) {
error.A => 0,
error.B => 1,
error.C => null,
};
try expect(@TypeOf(b) == ?u32);
// The non-error and error cases are only peers if the error case is just a switch expression;
// the pattern `x catch |err| blk: { switch (err) {...} }` does not consider the unwrapped `x`
// and error case to be peers.
const c = a catch |err| switch (err) {
error.A => 0,
error.B => 1,
error.C => null,
};
try expect(@TypeOf(c) == ?u32);
}
Оболочка
$ zig test test_peer_type_resolution.zig
1/8 test_peer_type_resolution.test.peer resolve int widening...OK
2/8 test_peer_type_resolution.test.peer resolve arrays of different size to const slice...OK
3/8 test_peer_type_resolution.test.peer resolve array and const slice...OK
4/8 test_peer_type_resolution.test.peer type resolution: ?T and T...OK
5/8 test_peer_type_resolution.test.peer type resolution: *[0]u8 and []const u8...OK
6/8 test_peer_type_resolution.test.peer type resolution: *[0]u8, []const u8, and anyerror![]u8...OK
7/8 test_peer_type_resolution.test.peer type resolution: *const T and ?*T...OK
8/8 test_peer_type_resolution.test.peer type resolution: error union switch...OK
All 8 tests passed.
Структура со всеми полями, являющимися типами с нулевой разрядностью.
Объединение только с 1 полем, являющимся типом с нулевой разрядностью.
Эти типы могут иметь только одно возможное значение и, следовательно, требуют 0 бит для представления. Код, использующий эти типы, не включён в конечный сгенерированный код:
zero_bit_types.zig
export fn entry() void {
var x: void = {};
var y: void = {};
x = y;
y = x;
}
При преобразовании в машинный код в теле entry, даже в режиме Debug, не генерируется никакой код. Например, на x86_64:
Эти инструкции ассемблера не содержат никакого кода, связанного со значениями void — они выполняют только пролог и эпилог вызова функции.
void
void может быть полезен для инстанцирования обобщённых типов. Например, при заданном Map(Key, Value), можно передать void в качестве типа Value, чтобы преобразовать его в Set.
test_void_in_hashmap.zig
const std = @import("std");
const expect = std.testing.expect;
test "turn HashMap into a set with void" {
var map = std.AutoHashMap(i32, void).init(std.testing.allocator);
defer map.deinit();
try map.put(1, {});
try map.put(2, {});
try expect(map.contains(2));
try expect(!map.contains(3));
_ = map.remove(2);
try expect(!map.contains(2));
}
Оболочка
$ zig test test_void_in_hashmap.zig
1/1 test_void_in_hashmap.test.turn HashMap into a set with void...OK
All 1 tests passed.
Обратите внимание, что это отличается от использования фиктивного значения для значения хеш-таблицы. Используя void в качестве типа значения, тип записи хеш-таблицы не имеет поля значения, а значит хеш-таблица занимает меньше места. Кроме того, весь код, связанный с хранением и загрузкой значения, удаляется, как показано выше.
void отличается от anyopaque. void имеет известный размер 0 байт, а anyopaque имеет неизвестный, но не нулевой размер.
Выражения типа void — единственные, чьё значение может быть проигнорировано. Например, игнорирование не-void выражения является ошибкой компиляции:
$ zig test test_expression_ignored.zigdoc/langref/test_expression_ignored.zig:2:8: error: value of type 'i32' ignored
foo();
~~~^~
doc/langref/test_expression_ignored.zig:2:8: note: all non-void values must be used
doc/langref/test_expression_ignored.zig:2:8: note: to discard the value, assign it to '_'
Однако, если выражение имеет тип void, ошибки не будет. Результаты выражений могут быть явно проигнорированы, присвоив их _.
test_void_ignored.zig
test "void is ignored" {
returnsVoid();
}
test "explicitly ignoring expression value" {
_ = foo();
}
fn returnsVoid() void {}
fn foo() i32 {
return 1234;
}
Оболочка
$ zig test test_void_ignored.zig
1/2 test_void_ignored.test.void is ignored...OK
2/2 test_void_ignored.test.explicitly ignoring expression value...OK
All 2 tests passed.
Семантика расположения результатов
Во время компиляции каждое выражение и подвыражение Zig получает необязательную информацию о расположении результата. Эта информация определяет тип выражения (тип результата) и место расположения результата в памяти (местоположение результата). Информация является необязательной в том смысле, что не каждое выражение имеет эту информацию: присваивание _, например, не предоставляет никакой информации о типе выражения, а также не предоставляет конкретное место в памяти для его размещения.
В качестве мотивационного примера рассмотрим оператор const x: u32 = 42;. Аннотация типа здесь предоставляет тип результата u32 для выражения инициализации 42, указывая компилятору привести это целое число (изначально типа comptime_int) к этому типу. Мы увидим больше примеров вскоре.
Это не реализационная деталь: изложенная выше логика кодифицирована в спецификации языка Zig и является основным механизмом вывода типов в языке. Эта система в совокупности называется «Семантикой расположения результатов».
Типы результатов
Типы результатов распространяются рекурсивно через выражения, где это возможно. Например, если выражение &e имеет тип результата *u32, то e получает тип результата u32, что позволяет языку выполнить это приведение перед взятием ссылки.
Механизм типа результата используется встроенными функциями приведения типа, такими как @intCast. Вместо того чтобы принимать в качестве аргумента тип, к которому нужно привести, эти встроенные функции используют свой тип результата для определения этой информации. Тип результата часто известен из контекста; в тех случаях, когда он не известен, можно использовать встроенную функцию @as для явного указания типа результата.
Мы можем разбить типы результатов для каждого компонента простого выражения следующим образом:
result_type_propagation.zig
const expectEqual = @import("std").testing.expectEqual;
test "result type propagates through struct initializer" {
const S = struct { x: u32 };
const val: u64 = 123;
const s: S = .{ .x = @intCast(val) };
// .{ .x = @intCast(val) } has result type `S` due to the type annotation
// @intCast(val) has result type `u32` due to the type of the field `S.x`
// val has no result type, as it is permitted to be any integer type
try expectEqual(@as(u32, 123), s.x);
}
Оболочка
$ zig test result_type_propagation.zig
1/1 result_type_propagation.test.result type propagates through struct initializer...OK
All 1 tests passed.
Эта информация о типе результата полезна для вышеупомянутых встроенных функций приведения типа, а также для избежания создания значений до приведения, а также для избежания необходимости явного приведения типов в некоторых случаях. В следующей таблице подробно описывается, как некоторые распространённые выражения распространяют типы результатов, где x и y — произвольные подвыражения.
Выражение
Родительский тип результата
Тип результата подвыражения
const val: T = x
-
x является T
var val: T = x
-
x является T
val = x
-
x является @TypeOf(val)
@as(T, x)
-
x является T
&x
*T
x является T
&x
[]T
x является массивом T
f(x)
-
x имеет тип первого параметра f
.{x}
T
x является std.meta.FieldType(T, .@"0")
.{ .a = x }
T
x является std.meta.FieldType(T, .a)
T{x}
-
x является std.meta.FieldType(T, .@"0")
T{ .a = x }
-
x является std.meta.FieldType(T, .a)
@Type(x)
-
x является std.builtin.Type
@typeInfo(x)
-
x является type
x << y
-
y является std.math.Log2IntCeil(@TypeOf(x))
Расположения результатов
Помимо информации о типе результата, каждое выражение может быть необязательно назначено местоположению результата: указателю, в который значение должно быть записано напрямую. Эта система может использоваться для предотвращения промежуточных копий при инициализации структур данных, что может быть важным для типов, которые должны иметь фиксированный адрес памяти («прикреплённые» типы).
При компиляции простого выражения присваивания x = e, многие языки создадут временное значение e в стеке, а затем присвоят его x, потенциально выполнив при этом преобразование типов. Zig подходит к этому по-другому. Выражению e присваивается тип результата, соответствующий типу x, и местоположение результата &x. Для многих синтаксических форм e, это не оказывает практического влияния. Однако это может иметь важное семантическое влияние при работе с более сложными синтаксическими формами.
Например, если выражение .{ .a = x, .b = y } имеет местоположение результата ptr, то x получает местоположение результата &ptr.a, а y — местоположение результата &ptr.b. Без этой системы это выражение полностью создало бы временное значение структуры в стеке, а затем скопировало бы его в адрес назначения. По сути, Zig упрощает присваивание foo = .{ .a = x, .b = y } до двух утверждений foo.a = x; foo.b = y;.
Это иногда может быть важно при присваивании агрегированного значения, где выражение инициализации зависит от предыдущего значения агрегата. Самый простой способ продемонстрировать это — попытаться поменять поля структуры или массива — следующий логический ход кажется правильным, но на самом деле таковым не является:
result_location_interfering_with_swap.zig
const expect = @import("std").testing.expect;
test "attempt to swap array elements with array initializer" {
var arr: [2]u32 = .{ 1, 2 };
arr = .{ arr[1], arr[0] };
// The previous line is equivalent to the following two lines:
// arr[0] = arr[1];
// arr[1] = arr[0];
// So this fails!
try expect(arr[0] == 2); // succeeds
try expect(arr[1] == 1); // fails
}
Оболочка
$ zig test result_location_interfering_with_swap.zig
1/1 result_location_interfering_with_swap.test.attempt to swap array elements with array initializer...FAIL (TestUnexpectedResult)
/home/andy/src/zig/lib/std/testing.zig:540:14: 0x103ce1f in expect (test)
if (!ok) return error.TestUnexpectedResult;
^/home/andy/src/zig/doc/langref/result_location_interfering_with_swap.zig:10:5: 0x103cf85 in test.attempt to swap array elements with array initializer (test)
try expect(arr[1] == 1); // fails
^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/andy/src/zig/.zig-cache/o/c42c6019fdf548f70655aafe3673a46e/test
В следующей таблице подробно показано, как некоторые распространённые выражения распространяют местоположения результатов, где x и y являются произвольными подвыражениями. Обратите внимание, что некоторые выражения не могут предоставить осмысленные местоположения результатов подвыражениям, даже если у них есть местоположение результата.
Выражение
Местоположение результата
Местоположения результатов подвыражений
const val: T = x
-
x имеет местоположение результата &val
var val: T = x
-
x имеет местоположение результата &val
val = x
-
x имеет местоположение результата &val
@as(T, x)
ptr
x не имеет местоположения результата
&x
ptr
x не имеет местоположения результата
f(x)
ptr
x не имеет местоположения результата
.{x}
ptr
x имеет местоположение результата &ptr[0]
.{ .a = x }
ptr
x имеет местоположение результата &ptr.a
T{x}
ptr
x не имеет местоположения результата (типизированные инициализаторы не распространяют местоположения результатов)
T{ .a = x }
ptr
x не имеет местоположения результата (типизированные инициализаторы не распространяют местоположения результатов)
Приведённый выше пример демонстрирует использование pub для квалификации usingnamespace, что дополнительно делает импортированные объявления pub. Это можно использовать для перенаправления объявлений, обеспечивая точный контроль над тем, какие объявления раскрывает данный файл.
comptime
Zig придаёт значение концепции того, известно ли выражение во время компиляции. Эта концепция используется в нескольких местах, и эти строительные блоки используются для сохранения простоты, читаемости и мощности языка.
Параметры времени компиляции — это то, как Zig реализует обобщения. Это типизация по умолчанию во время компиляции.
compile-time_duck_typing.zig
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
fn gimmeTheBiggerFloat(a: f32, b: f32) f32 {
return max(f32, a, b);
}
fn gimmeTheBiggerInteger(a: u64, b: u64) u64 {
return max(u64, a, b);
}
В Zig типы являются гражданами первого класса. Они могут быть присвоены переменным, переданы как параметры функциям и возвращены из функций. Однако они могут использоваться только в выражениях, известных во время компиляции, поэтому параметр T в приведенном фрагменте должен быть помечен как comptime.
Параметр comptime означает, что:
В месте вызова значение должно быть известно во время компиляции, в противном случае это ошибка компиляции.
В определении функции значение известно во время компиляции.
Например, если мы добавим ещё одну функцию в приведенный выше фрагмент:
test_unresolved_comptime_value.zig
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
test "try to pass a runtime type" {
foo(false);
}
fn foo(condition: bool) void {
const result = max(if (condition) f32 else u64, 1234, 5678);
_ = result;
}
Оболочка
$ zig test test_unresolved_comptime_value.zigdoc/langref/test_unresolved_comptime_value.zig:8:28: error: unable to resolve comptime value
const result = max(if (condition) f32 else u64, 1234, 5678);
^~~~~~~~~
doc/langref/test_unresolved_comptime_value.zig:8:28: note: condition in comptime branch must be comptime-known
referenced by:
test.try to pass a runtime type: doc/langref/test_unresolved_comptime_value.zig:5:5
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Это ошибка, так как программист пытался передать значение, известное только во время выполнения, функции, ожидающей значение, известное во время компиляции.
Другой способ получения ошибки — передача типа, нарушающего проверку типов при анализе функции. Это означает типизацию по умолчанию во время компиляции.
Например:
test_comptime_mismatched_type.zig
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
test "try to compare bools" {
_ = max(bool, true, false);
}
Оболочка
$ zig test test_comptime_mismatched_type.zigdoc/langref/test_comptime_mismatched_type.zig:2:18: error: operator > not allowed for type 'bool'
return if (a > b) a else b;
~~^~~
referenced by:
test.try to compare bools: doc/langref/test_comptime_mismatched_type.zig:5:12
remaining reference traces hidden; use '-freference-trace' to see all reference traces
С другой стороны, внутри определения функции с параметром comptime значение известно во время компиляции. Это означает, что мы фактически могли бы сделать это для типа bool, если бы захотели:
test_comptime_max_with_bool.zig
fn max(comptime T: type, a: T, b: T) T {
if (T == bool) {
return a or b;
} else if (a > b) {
return a;
} else {
return b;
}
}
test "try to compare bools" {
try @import("std").testing.expect(max(bool, false, true) == true);
}
Оболочка
$ zig test test_comptime_max_with_bool.zig
1/1 test_comptime_max_with_bool.test.try to compare bools...OK
All 1 tests passed.
Это работает, потому что Zig неявно встраивает if выражения, когда условие известно во время компиляции, и компилятор гарантирует, что он пропустит анализ не выбранного ветвления.
Это означает, что фактическая функция, сгенерированная для max в этой ситуации, выглядит так:
compiler_generated_function.zig
fn max(a: bool, b: bool) bool {
{
return a or b;
}
}
Весь код, имеющий дело со значениями, известными во время компиляции, удаляется, и остается только необходимый код времени выполнения для выполнения задачи.
То же самое работает для switch выражений — они неявно встраиваются, когда целевое выражение известно во время компиляции.
В Zig программист может пометить переменные как comptime. Это гарантирует компилятору, что все загрузки и сохранения переменной выполняются во время компиляции. Любое нарушение приводит к ошибке компиляции.
Это в сочетании с возможностью inline циклов позволяет нам написать функцию, которая частично вычисляется во время компиляции, а частично — во время выполнения.
Например:
test_comptime_evaluation.zig
const expect = @import("std").testing.expect;
const CmdFn = struct {
name: []const u8,
func: fn (i32) i32,
};
const cmd_fns = [_]CmdFn{
CmdFn{ .name = "one", .func = one },
CmdFn{ .name = "two", .func = two },
CmdFn{ .name = "three", .func = three },
};
fn one(value: i32) i32 {
return value + 1;
}
fn two(value: i32) i32 {
return value + 2;
}
fn three(value: i32) i32 {
return value + 3;
}
fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
var result: i32 = start_value;
comptime var i = 0;
inline while (i < cmd_fns.len) : (i += 1) {
if (cmd_fns[i].name[0] == prefix_char) {
result = cmd_fns[i].func(result);
}
}
return result;
}
test "perform fn" {
try expect(performFn('t', 1) == 6);
try expect(performFn('o', 0) == 1);
try expect(performFn('w', 99) == 99);
}
Оболочка
$ zig test test_comptime_evaluation.zig
1/1 test_comptime_evaluation.test.perform fn...OK
All 1 tests passed.
Этот пример немного искусственный, потому что компонента вычисления во время компиляции излишняя; этот код хорошо работал бы, если бы все было сделано во время выполнения. Но он всё же генерирует другой код. В этом примере функция performFn генерируется три раза для различных значений prefix_char, указанных в программе:
performFn_1
// From the line:
// expect(performFn('t', 1) == 6);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
result = two(result);
result = three(result);
return result;
}
performFn_2
// From the line:
// expect(performFn('o', 0) == 1);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
result = one(result);
return result;
}
performFn_3
// From the line:
// expect(performFn('w', 99) == 99);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
_ = &result;
return result;
}
Обратите внимание, что это происходит даже в отладочной сборке. Это не способ написания более оптимизированного кода, но это способ убедиться, что то, что должно произойти во время компиляции, действительно происходит во время компиляции. Это выявляет больше ошибок и позволяет выразительность, которая в других языках требует использования макросов, сгенерированного кода или препроцессора.
В Zig важно, известно ли данное выражение во время компиляции или во время выполнения. Программист может использовать comptime выражение, чтобы гарантировать, что выражение будет вычислено во время компиляции. Если этого сделать нельзя, компилятор выдаст ошибку. Например:
$ zig test test_comptime_call_extern_function.zigdoc/langref/test_comptime_call_extern_function.zig:5:13: error: comptime call of extern function
exit();
~~~~^~
Нелогично, чтобы программа могла вызвать exit() (или любую другую внешнюю функцию) во время компиляции, поэтому это ошибка компиляции. Однако выражение comptime делает гораздо больше, чем иногда вызывает ошибку компиляции.
Внутри выражения comptime:
Все переменные являются переменными comptime.
Все выражения if, while, for и switch вычисляются во время компиляции или генерируют ошибку компиляции, если это невозможно.
Все выражения return и try недействительны (если функция сама не вызывается во время компиляции).
Весь код с побочными эффектами во время выполнения или зависящий от значений во время выполнения генерирует ошибку компиляции.
Все вызовы функций заставляют компилятор интерпретировать функцию во время компиляции, генерируя ошибку компиляции, если функция пытается сделать что-то, что имеет глобальные побочные эффекты во время выполнения.
Это означает, что программист может создать функцию, которая вызывается как во время компиляции, так и во время выполнения, без необходимости вносить какие-либо изменения в функцию.
Давайте рассмотрим пример:
test_fibonacci_recursion.zig
const expect = @import("std").testing.expect;
fn fibonacci(index: u32) u32 {
if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
test "fibonacci" {
// test fibonacci at run-time
try expect(fibonacci(7) == 13);
// test fibonacci at compile-time
try comptime expect(fibonacci(7) == 13);
}
Командная строка
$ zig test test_fibonacci_recursion.zig
1/1 test_fibonacci_recursion.test.fibonacci...OK
All 1 tests passed.
Представьте, если мы забыли базовый случай рекурсивной функции и попытались запустить тесты:
$ zig test test_fibonacci_comptime_overflow.zigdoc/langref/test_fibonacci_comptime_overflow.zig:5:28: error: overflow of integer type 'u32' with value '-1'
return fibonacci(index - 1) + fibonacci(index - 2);
~~~~~~^~~
doc/langref/test_fibonacci_comptime_overflow.zig:5:21: note: called from here (7 times)
return fibonacci(index - 1) + fibonacci(index - 2);
~~~~~~~~~^~~~~~~~~~~
doc/langref/test_fibonacci_comptime_overflow.zig:9:34: note: called from here
try comptime expect(fibonacci(7) == 13);
~~~~~~~~~^~~
Компилятор генерирует ошибку, представляющую собой стек-трейс попытки вычисления функции во время компиляции.
К счастью, мы использовали целые числа без знака, и поэтому, когда мы попытались вычесть 1 из 0, это вызвало неопределённое поведение, что всегда является ошибкой компиляции, если компилятор знает, что это произошло. Но что бы произошло, если бы мы использовали целые числа со знаком?
Компилятор должен заметить, что вычисление этой функции во время компиляции заняло более 1000 ветвей, и поэтому генерирует ошибку и отказывается. Если программист хочет увеличить бюджет для вычислений во время компиляции, он может использовать встроенную функцию, называемую @setEvalBranchQuota, чтобы изменить значение по умолчанию 1000 на другое.
Однако существует ошибка проектирования в компиляторе, из-за которой вместо правильного поведения происходит переполнение стека. Извините за это. Надеюсь, эта проблема будет решена до следующего релиза.
А что, если мы исправим базовый случай, но неправильно укажем значение в строке expect?
$ zig test test_fibonacci_comptime_unreachable.ziglib/std/debug.zig:412:14: error: reached unreachable code
if (!ok) unreachable; // assertion failure
^~~~~~~~~~~
doc/langref/test_fibonacci_comptime_unreachable.zig:9:24: note: called from here
try comptime assert(fibonacci(7) == 99999);
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
На уровне контейнера (вне любой функции) все выражения неявно являются выражениями comptime. Это означает, что мы можем использовать функции для инициализации сложных статических данных. Например:
test_container-level_comptime_expressions.zig
const first_25_primes = firstNPrimes(25);
const sum_of_first_25_primes = sum(&first_25_primes);
fn firstNPrimes(comptime n: usize) [n]i32 {
var prime_list: [n]i32 = undefined;
var next_index: usize = 0;
var test_number: i32 = 2;
while (next_index < prime_list.len) : (test_number += 1) {
var test_prime_index: usize = 0;
var is_prime = true;
while (test_prime_index < next_index) : (test_prime_index += 1) {
if (test_number % prime_list[test_prime_index] == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
prime_list[next_index] = test_number;
next_index += 1;
}
}
return prime_list;
}
fn sum(numbers: []const i32) i32 {
var result: i32 = 0;
for (numbers) |x| {
result += x;
}
return result;
}
test "variable values" {
try @import("std").testing.expect(sum_of_first_25_primes == 1060);
}
Командная строка
$ zig test test_container-level_comptime_expressions.zig
1/1 test_container-level_comptime_expressions.test.variable values...OK
All 1 tests passed.
При компиляции этой программы Zig генерирует константы с предварительно вычисленным ответом. Вот строки из сгенерированного LLVM IR:
Обратите внимание, что нам не нужно было делать ничего особенного с синтаксисом этих функций. Например, мы могли бы вызвать функцию sum как есть со слайсом чисел, длина и значения которых были известны только во время выполнения.
Общие структуры данных
Zig использует возможности comptime для реализации общих структур данных без введения какого-либо специального синтаксиса.
Вот пример общей структуры данных List.
generic_data_structure.zig
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
// The generic List data structure can be instantiated by passing in a type:
var buffer: [10]i32 = undefined;
var list = List(i32){
.items = &buffer,
.len = 0,
};
Всё это. Это функция, которая возвращает анонимную структуру struct. Для целей сообщений об ошибках и отладки Zig выводит имя "List(i32)" из имени функции и параметров, вызванных при создании анонимной структуры.
Чтобы явно присвоить типу имя, мы назначаем его константе.
В этом примере структура Node ссылается на себя. Это работает, потому что все декларации верхнего уровня независимы от порядка. Пока компилятор может определить размер структуры, он свободен ссылаться на неё. В данном случае Node ссылается на себя как на указатель, у которого есть хорошо определённый размер во время компиляции, поэтому всё работает нормально.
Случайное исследование: print в Zig
Объединяя всё это, давайте посмотрим, как работает print в Zig.
print.zig
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
pub fn main() void {
print("here is a string: '{s}' here is a number: {}\n", .{ a_string, a_number });
}
Командная строка
$ zig build-exe print.zig
$ ./print
here is a string: 'foobar' here is a number: 1234
Давайте разберём реализацию и посмотрим, как это работает:
И что происходит, если мы передаём слишком много аргументов функции print?
test_print_too_many_args.zig
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
test "print too many arguments" {
print("here is a string: '{s}' here is a number: {}\n", .{
a_string,
a_number,
a_number,
});
}
Командная строка
$ zig test test_print_too_many_args.ziglib/std/fmt.zig:203:18: error: unused argument in 'here is a string: '{s}' here is a number: {}
'
1 => @compileError("unused argument in '" ++ fmt ++ "'"),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
print__anon_2377: lib/std/io/Writer.zig:24:26
print: lib/std/io.zig:324:47
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Zig предоставляет программистам инструменты, необходимые для защиты от собственных ошибок.
Zig не заботится о том, является ли аргумент форматирования строковой литералью, а только о том, что это известная во время компиляции величина, которая может быть преобразована в []constu8:
print_comptime-known_format.zig
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
const fmt = "here is a string: '{s}' here is a number: {}\n";
pub fn main() void {
print(fmt, .{ a_string, a_number });
}
Командная строка
$ zig build-exe print_comptime-known_format.zig
$ ./print_comptime-known_format
here is a string: 'foobar' here is a number: 1234
Это работает нормально.
Zig не обрабатывает форматирование строк специально в компиляторе, а вместо этого предоставляет достаточно возможностей, чтобы выполнить эту задачу на пользовательском уровне. Он делает это без введения другого языка поверх Zig, такого как язык макросов или язык препроцессора. Это Zig до самого конца.
В некоторых случаях может потребоваться прямой контроль над машинным кодом, генерируемым программами Zig, вместо того, чтобы полагаться на генерацию кода Zig. В таких случаях можно использовать встроенный ассемблер. Вот пример реализации "Hello, world" на x86_64 Linux с помощью встроенного ассемблера:
$ zig build-exe inline_assembly.zig -target x86_64-linux
$ ./inline_assembly
hello world
Разбор синтаксиса:
Assembly Syntax Explained.zig
pub fn syscall1(number: usize, arg1: usize) usize {
// Inline assembly is an expression which returns a value.
// the `asm` keyword begins the expression.
return asm
// `volatile` is an optional modifier that tells Zig this
// inline assembly expression has side-effects. Without
// `volatile`, Zig is allowed to delete the inline assembly
// code if the result is unused.
volatile (
// Next is a comptime string which is the assembly code.
// Inside this string one may use `%[ret]`, `%[number]`,
// or `%[arg1]` where a register is expected, to specify
// the register that Zig uses for the argument or return value,
// if the register constraint strings are used. However in
// the below code, this is not used. A literal `%` can be
// obtained by escaping it with a double percent: `%%`.
// Often multiline string syntax comes in handy here.
\\syscall
// Next is the output. It is possible in the future Zig will
// support multiple outputs, depending on how
// https://github.com/ziglang/zig/issues/215 is resolved.
// It is allowed for there to be no outputs, in which case
// this colon would be directly followed by the colon for the inputs.
:
// This specifies the name to be used in `%[ret]` syntax in
// the above assembly string. This example does not use it,
// but the syntax is mandatory.
[ret]
// Next is the output constraint string. This feature is still
// considered unstable in Zig, and so LLVM/GCC documentation
// must be used to understand the semantics.
// http://releases.llvm.org/10.0.0/docs/LangRef.html#inline-asm-constraint-string
// https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
// In this example, the constraint string means "the result value of
// this inline assembly instruction is whatever is in $rax".
"={rax}"
// Next is either a value binding, or `->` and then a type. The
// type is the result type of the inline assembly expression.
// If it is a value binding, then `%[ret]` syntax would be used
// to refer to the register bound to the value.
(-> usize),
// Next is the list of inputs.
// The constraint for these inputs means, "when the assembly code is
// executed, $rax shall have the value of `number` and $rdi shall have
// the value of `arg1`". Any number of input parameters is allowed,
// including none.
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1),
// Next is the list of clobbers. These declare a set of registers whose
// values will not be preserved by the execution of this assembly code.
// These do not include output or input registers. The special clobber
// value of "memory" means that the assembly writes to arbitrary undeclared
// memory locations - not only the memory pointed to by a declared indirect
// output. In this example we list $rcx and $r11 because it is known the
// kernel syscall does not preserve these registers.
: "rcx", "r11"
);
}
Для целей x86 и x86_64 используется синтаксис AT&T, а не более популярный синтаксис Intel. Это связано с техническими ограничениями; обработка ассемблера обеспечивается LLVM, и его поддержка синтаксиса Intel является ошибочной и не хорошо протестированной.
Возможно, в будущем Zig будет иметь собственный ассемблер. Это позволило бы ему более тесно интегрироваться в язык, а также быть совместимым с популярным синтаксисом NASM. Этот раздел документации будет обновлён до выхода версии 1.0.0 с окончательным заявлением о статусе синтаксиса AT&T по сравнению с Intel/NASM.
Ограничения вывода
Ограничения вывода всё ещё считаются нестабильными в Zig, поэтому для понимания семантики необходимо использовать документацию LLVM и GCC.
Обратите внимание, что некоторые изменения в ограничениях вывода планируются в рамках задачи #215.
Ограничения ввода
Ограничения ввода всё ещё считаются нестабильными в Zig, поэтому для понимания семантики необходимо использовать документацию LLVM и GCC.
Обратите внимание, что некоторые изменения в ограничениях ввода планируются в рамках задачи #215.
Clobbers
Clobbers — это набор регистров, значения которых не сохраняются после выполнения ассемблерного кода. Они не включают выходные или входные регистры. Специальное значение clobber "memory" означает, что ассемблер вызывает запись в произвольные не объявленные места памяти — не только в память, на которую указывает объявленный косвенный выход.
Отсутствие объявления полного набора clobbers для данного выражения встроенного ассемблера не проверяется неопределённое поведение.
Глобальный ассемблер
Когда выражение ассемблера встречается на уровне контейнера в блоке comptime, это глобальный ассемблер.
Такой ассемблер имеет разные правила, чем встроенный ассемблер. Во-первых, volatile недействителен, поскольку весь глобальный ассемблер включён безусловно. Во-вторых, нет входных, выходных или clobbers. Весь глобальный ассемблер конкатенируется в одну длинную строку и собирается вместе. Нет правил подстановки шаблонов по отношению к %, как есть во встроенных ассемблерных выражениях.
Указатели на асинхронные функции препятствуют определению размера стека.
Эти проблемы преодолимы, но это займет время. Команда Zig в настоящее время сосредоточена на других приоритетах.
Встроенные функции
Встроенные функции предоставляются компилятором и имеют префикс @. Ключевое слово comptime в параметре означает, что параметр должен быть известен на этапе компиляции.
@addrSpaceCast
@addrSpaceCast(ptr: anytype) anytype
Преобразует указатель из одной адресной области в другую. Новая адресная область определяется на основе типа результата. В зависимости от текущей цели и адресных областей это преобразование может быть пустым, сложной операцией или запрещённым. Если преобразование допустимо, то результирующий указатель указывает на ту же область памяти, что и операнд-указатель. Всегда допустимо преобразование указателя между одними и теми же адресными областями.
Выполняет a + b и возвращает кортеж с результатом и возможным флагом переполнения.
@alignCast
@alignCast(ptr: anytype) anytype
ptr может быть *T, ?*T, или []T. Изменяет выравнивание указателя. Выравнивание для использования определяется на основе типа результата.
В сгенерированный код добавлена проверка безопасности выравнивания указателя, чтобы убедиться, что указатель выровнен, как обещано.
@alignOf
@alignOf(comptime T: type) comptime_int
Эта функция возвращает количество байтов, к которым этот тип должен быть выровнен для текущей платформы, чтобы соответствовать C ABI. Когда дочерний тип указателя имеет это выравнивание, выравнивание можно опустить из типа.
Выполняет Преобразование типов. Это преобразование разрешено, когда преобразование однозначно и безопасно, и является предпочтительным способом преобразования между типами, когда это возможно.
@atomicLoad
@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: AtomicOrder) T
Эта встроенная функция атомарно обращается к указателю на T и возвращает значение.
T должен быть указателем, bool, числом с плавающей точкой, целым числом или перечислением.
AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder.
Преобразует значение одного типа в другой тип. Тип возвращаемого значения — это выведенный тип результата.
Утверждает, что @sizeOf(@TypeOf(value)) == @sizeOf(DestType).
Утверждает, что @typeInfo(DestType) != .Pointer. Используйте @ptrCast или @ptrFromInt если вам это нужно.
Может использоваться, например, для:
Преобразование f32 в u32 биты
Преобразование i32 в u32 с сохранением дополнительного кода
Работает на этапе компиляции, если value известно на этапе компиляции. Преобразование типа с неопределённой структурой является ошибкой компиляции; это означает, что помимо ограничений из типов, которые имеют собственные встроенные преобразования (перечисления, указатели, наборы ошибок), простые структуры, союзы ошибок, срезы, необязательные значения и любой другой тип без чётко определённой структуры памяти также нельзя использовать в этой операции.
Возвращает битовый сдвиг поля относительно содержащей его структуры.
Для непростых структур это всегда будет кратно 8. Для упакованных структур поля с небайтовым выравниванием будут иметь один байтовый сдвиг, но разные битовые сдвиги.
Эта функция возвращает количество битов, необходимых для хранения T в памяти, если тип был бы полем в упакованной структуре/союзе. Результат — константа, специфичная для целевой платформы.
Эта функция измеряет размер во время выполнения. Для типов, которые недопустимы во время выполнения, таких как comptime_int и type, результат — 0.
Эта функция вставляет платформоспецифичную инструкцию отладки, которая заставляет отладчики остановиться на ней. В отличие от @trap(), выполнение может продолжиться после этой точки, если программа возобновляется.
Эта функция допустима только в пределах области функции.
Меняет порядок байтов целого числа. Это преобразует целое число с big endian в little endian и наоборот.
Обратите внимание, что для целей размещения памяти с точки зрения порядка байтов тип целого числа должен быть связан с количеством байтов, указанных функцией @sizeOf. Это демонстрируется с помощью u24. @sizeOf(u24) == 4, что означает, что целое число u24 в памяти занимает 4 байта, и эти 4 байта меняют свой порядок в системах little и big endian. С другой стороны, если T задано как u24, то меняется только порядок 3 байтов.
@bitReverse
@bitReverse(integer: anytype) T
@TypeOf(anytype) принимает любой целочисленный тип или целочисленный векторный тип.
Меняет порядок битов значения целого числа, включая бит знака, если применимо.
Например, 0b10110110 (u8 = 182, i8 = -74) становится 0b01101101 (u8 = 109, i8 = 109).
$ zig test test_call_builtin.zig
1/1 test_call_builtin.test.noinline function call...OK
All 1 tests passed.
@call предоставляет больше гибкости, чем обычный синтаксис вызова функций. Перечисление CallModifier воспроизведено здесь:
builtin.CallModifier struct.zig
pub const CallModifier = enum {
/// Equivalent to function call syntax.
auto,
/// Equivalent to async keyword used with function call syntax.
async_kw,
/// Prevents tail call optimization. This guarantees that the return
/// address will point to the callsite, as opposed to the callsite's
/// callsite. If the call is otherwise required to be tail-called
/// or inlined, a compile error is emitted instead.
never_tail,
/// Guarantees that the call will not be inlined. If the call is
/// otherwise required to be inlined, a compile error is emitted instead.
never_inline,
/// Asserts that the function call will not suspend. This allows a
/// non-async function to call an async function.
no_async,
/// Guarantees that the call will be generated with tail call optimization.
/// If this is not possible, a compile error is emitted instead.
always_tail,
/// Guarantees that the call will inlined at the callsite.
/// If this is not possible, a compile error is emitted instead.
always_inline,
/// Evaluates the call at compile-time. If the call cannot be completed at
/// compile-time, a compile error is emitted instead.
compile_time,
};
@cDefine
@cDefine(comptime name: []const u8, value) void
Эта функция может находиться только внутри @cImport.
Это добавляет #define $name $value к временной буфер @cImport.
Эта функция анализирует код C и импортирует функции, типы, переменные и совместимые определения макросов в новый пустой тип структуры, а затем возвращает этот тип.
expression интерпретируется на этапе компиляции. Встроенные функции @cInclude, @cDefine, и @cUndef работают в этом выражении, добавляя в временной буфер, который затем анализируется как код C.
Обычно в вашем приложении должен быть только один @cImport, потому что он позволяет компилятору избежать многократного вызова clang и предотвращает дублирование встроенных функций.
Причинами наличия нескольких @cImport выражений могут быть:
Избегание коллизий символов, например, если foo.h и bar.h оба #define CONNECTION_COUNT
Анализ кода C с различными препроцессорными определениями
Подсчитывает количество старших (ведущих в формате big-endian) нулей в целом числе — "подсчёт старших нулей".
Если operand является известным на этапе компиляции целым числом, возвращаемый тип — comptime_int. В противном случае, возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, достаточным для представления количества нулей в целом числе указанного типа.
Если operand равно нулю, @clz возвращает разрядность целочисленного типа T.
Эта функция выполняет атомную операцию сравнения и обмена с сильной гарантией, возвращая null, если текущее значение не соответствует заданному ожидаемому значению. Эквивалентна данному коду, но атомная:
Если вы используете cmpxchg в цикле повторных попыток, @cmpxchgWeak является лучшим выбором, так как она может быть реализована более эффективно в машинном коде.
T должен быть указателем, bool, числом с плавающей точкой, целым числом или перечислением.
@typeInfo(@TypeOf(ptr)).Pointer.alignment должно быть >= @sizeOf(T).
AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder
Эта функция выполняет атомную операцию сравнения и обмена со слабой гарантией, возвращая null, если текущее значение не соответствует заданному ожидаемому значению. Эквивалентна данному коду, но атомная:
Если вы используете cmpxchg в цикле повторных попыток, случайный сбой не будет проблемой, и cmpxchgWeak является лучшим выбором, так как она может быть реализована более эффективно в машинном коде. Однако, если вам нужна более сильная гарантия, используйте @cmpxchgStrong.
T должен быть указателем, bool, числом с плавающей точкой, целым числом или перечислением.
@typeInfo(@TypeOf(ptr)).Pointer.alignment должно быть >= @sizeOf(T).
AtomicOrder можно найти с помощью @import("std").builtin.AtomicOrder
Эта функция, при семантическом анализе, вызывает ошибку компиляции с сообщением msg.
Существуют способы, позволяющие избежать семантической проверки кода, такие как использование if или switch с константами времени компиляции, а также comptime функции.
@compileLog
@compileLog(args: ...) void
Эта функция выводит переданные ей аргументы во время компиляции.
Чтобы предотвратить случайное оставление сообщений compile log в коде, в сборке добавляется ошибка компиляции, указывающая на заявление compile log. Эта ошибка препятствует генерации кода, но не мешает анализу.
Эта функция может использоваться для отладки кода, выполняющегося на этапе компиляции, используя "printf-отладку".
Подсчитывает количество младших (конечных в формате big-endian) нулей в целом числе — "подсчёт конечных нулей".
Если operand является известным на этапе компиляции целым числом, возвращаемый тип — comptime_int. В противном случае, возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, достаточным для представления количества нулей в целом числе указанного типа.
Если operand равно нулю, @ctz возвращает разрядность целочисленного типа T.
Деление с округлением вниз. Округляет к отрицательной бесконечности. Для беззнаковых целых чисел эквивалентно numerator / denominator. Вызывающая функция гарантирует denominator != 0 и !(@typeInfo(T) == .Int and T.is_signed and numerator == std.math.minInt(T) and denominator == -1).
@divFloor(-5, 3) == -2
(@divFloor(a, b) * b) + @mod(a, b) == a
Для функции, возвращающей возможный код ошибки, используйте @import("std").math.divFloor.
Деление с усечением. Округляет к нулю. Для беззнаковых целых чисел эквивалентно numerator / denominator. Вызывающая функция гарантирует denominator != 0 и !(@typeInfo(T) == .Int and T.is_signed and numerator == std.math.minInt(T) and denominator == -1).
@divTrunc(-5, 3) == -1
(@divTrunc(a, b) * b) + @rem(a, b) == a
Для функции, возвращающей возможный код ошибки, используйте @import("std").math.divTrunc.
Эта функция возвращает константный указатель времени компиляции на нуль-терминированный массив фиксированного размера длиной, равной количеству байтов файла, заданного path. Содержимое массива — содержимое файла. Эквивалентно строковой литерали с содержимым файла.
path может быть абсолютным или относительным по отношению к текущему файлу, так же, как @import.
Преобразует целое число в значение перечисления (enum). Возвращаемый тип — выведенный тип результата.
Попытка преобразовать целое число, не представляющее значение в выбранном типе перечисления, вызывает неопределенное поведение с проверкой безопасности.
В общем случае рекомендуется избегать такого преобразования, так как целочисленное представление ошибки не стабильно относительно изменений исходного кода.
Попытка преобразовать целое число, не соответствующее ни одной ошибке, приводит к защищённому от ошибок неопределённому поведению.
Эта функция возвращает строковое представление ошибки. Строковое представление error.OutOfMem — "OutOfMem".
Если в приложении нет вызовов @errorName, или все вызовы имеют известное на этапе компиляции значение для err, таблица имён ошибок не будет сгенерирована.
@errorReturnTrace
@errorReturnTrace() ?*builtin.StackTrace
Если бинарник скомпилирован с отслеживанием возвращаемых ошибок, и эта функция вызвана в функции, которая вызывает функцию с возвращаемым типом ошибки или объединением ошибок, возвращает объект стека вызовов. В противном случае возвращает null.
@errorCast
@errorCast(value: anytype) anytype
Преобразует значение набора ошибок или объединения ошибок из одного набора ошибок в другой набор ошибок. Возвращаемый тип — выведенный тип результата. Попытка преобразовать ошибку, которая не находится в целевом наборе ошибок, приводит к защищённому от ошибок неопределённому поведению.
Этот встроенный элемент можно вызвать из блока comptime, чтобы условно экспортировать символы. Когда declaration — это функция с соглашением вызова C, а options.linkage — Strong, это эквивалентно ключевому слову export для функции:
Обратите внимание, что даже при использовании export, синтаксис @"foo" для идентификаторов можно использовать для выбора любой строки в качестве имени символа:
export_any_symbol_name.zig
export fn @"A function name that is a complete sentence."() void {}
Оболочка
$ zig build-obj export_any_symbol_name.zig
При просмотре результирующего объекта вы увидите, что символ используется дословно:
00000000000001f0 T A function name that is a complete sentence.
$ zig test test_field_builtin.zig
1/2 test_field_builtin.test.field access by string...OK
2/2 test_field_builtin.test.decl access by string...OK
All 2 tests passed.
Принимая указатель на поле, возвращает базовый указатель структуры.
@floatCast
@floatCast(value: anytype) anytype
Преобразование из одного типа с плавающей запятой в другой. Это преобразование безопасно, но может привести к потере точности числового значения. Тип возвращаемого значения — это выведенный тип результата.
@floatFromInt
@floatFromInt(int: anytype) anytype
Преобразует целое число в ближайшее представление с плавающей запятой. Тип возвращаемого значения — это выведенный тип результата. Для преобразования в обратном направлении используйте @intFromFloat. Эта операция допустима для всех значений всех типов целых чисел.
@frameAddress
@frameAddress() usize
Эта функция возвращает базовый указатель текущей рамки стека.
Последствия этого зависят от целевой платформы и не являются согласованными на всех платформах. Адрес рамки может быть недоступен в режиме релиз из-за агрессивной оптимизации.
Эта функция допустима только в пределах области действия функции.
Возвращает, есть ли у контейнера объявление, соответствующее name.
test_hasDecl_builtin.zig
const std = @import("std");
const expect = std.testing.expect;
const Foo = struct {
nope: i32,
pub var blah = "xxx";
const hi = 1;
};
test "@hasDecl" {
try expect(@hasDecl(Foo, "blah"));
// Even though `hi` is private, @hasDecl returns true because this test is
// in the same file scope as Foo. It would return false if Foo was declared
// in a different file.
try expect(@hasDecl(Foo, "hi"));
// @hasDecl is for declarations; not fields.
try expect(!@hasDecl(Foo, "nope"));
try expect(!@hasDecl(Foo, "nope1234"));
}
Оболочка
$ zig test test_hasDecl_builtin.zig
1/1 test_hasDecl_builtin.test.@hasDecl...OK
All 1 tests passed.
Эта функция находит файл zig, соответствующий path, и добавляет его в сборку, если он еще не добавлен.
Файлы исходного кода Zig неявно являются структурами с именем, равным имени файла без расширения. @import возвращает тип структуры, соответствующий файлу.
Объявления, имеющие ключевое слово pub, могут быть использованы в другом файле исходного кода, отличном от того, в котором они объявлены.
path может быть относительным путем или именем пакета. Если это относительный путь, он относительный к файлу, содержащему вызов функции @import.
Следующие пакеты всегда доступны:
@import("std") - Стандартная библиотека Zig
@import("builtin") - Информация, специфичная для целевой платформы. Команда zig build-exe --show-builtin выводит исходный код в стандартный вывод для справки.
@import("root") - Файл исходного кода корня. Обычно это src/main.zig, но зависит от того, какой файл компилируется.
Возвращает, был ли встроенный элемент запущен в контексте comptime. Результат — константа времени компиляции.
Это можно использовать для предоставления альтернативных, дружественных к компиляции реализаций функций. Его не следует использовать, например, для исключения определенных функций из оценки в процессе компиляции.
Преобразует целое число в другое целое число, сохраняя при этом то же числовое значение. Тип возвращаемого значения — это выведенный тип результата. Попытка преобразовать число, которое выходит за пределы диапазона целевого типа, приводит к безопасному поведению, которое не определено Undefined Behavior.
Преобразует значение перечисления в его целочисленный тип тега. Если передается помеченная объединение, используется значение тега в качестве значения перечисления.
Если существует только одно возможное значение перечисления, результат — comptime_int известное в момент компиляции.
Преобразует value в usize, который представляет собой адрес указателя. value может быть *T или ?*T.
Для преобразования в обратном направлении используйте @ptrFromInt
@max
@max(a: T, b: T) T
Возвращает максимальное значение a и b. Этот встроенный элемент принимает целые числа, числа с плавающей запятой и векторы любого из них. В последнем случае операция выполняется поэлементно.
NaN обрабатываются следующим образом: если один из операндов (парной) операции — NaN, возвращается другой операнд. Если оба операнда — NaN, возвращается NaN.
Эта функция копирует байты из одной области памяти в другую.
dest должно быть изменяемым слайсом, изменяемым указателем на массив или изменяемым указателем на множество элементов указателя. Он может иметь любую выравнивание и любой тип элементов.
source должно быть слайсом, указателем на массив или указателем на множество элементов указателя. Он может иметь любое выравнивание и любой тип элементов.
Тип элемента source должен поддерживать преобразование типов в тип элемента dest. Типы элементов могут иметь разный размер ABI, однако это может повлиять на производительность.
Подобно циклам for, по крайней мере один из source и dest должен предоставлять длину, и если предоставлены две длины, они должны быть равны.
И наконец, две области памяти не должны перекрываться.
@memset
@memset(dest, elem) void
Эта функция устанавливает все элементы области памяти в elem.
dest должно быть изменяемым слайсом или изменяемым указателем на массив. Он может иметь любое выравнивание и любой тип элементов.
elem преобразуется в тип элемента dest.
Для безопасного обнуления конфиденциальных содержимого из памяти используйте std.crypto.utils.secureZero
@min
@min(a: T, b: T) T
Возвращает минимальное значение a и b. Этот встроенный элемент принимает целые числа, числа с плавающей запятой и векторы любого из них. В последнем случае операция выполняется поэлементно.
NaN обрабатываются следующим образом: если один из операндов (парной) операции — NaN, возвращается другой операнд. Если оба операнда — NaN, возвращается NaN.
Эта функция возвращает размер памяти Wasm, идентифицированной как index, в качестве беззнакового значения в единицах страниц Wasm. Обратите внимание, что каждая страница Wasm имеет размер 64 КБ.
Эта функция является низкоуровневым встроенным инструментом без механизмов безопасности, обычно полезным для разработчиков аллокаторов, ориентированных на Wasm. Поэтому, если вы не пишете новый аллокатор с нуля, используйте что-то вроде @import("std").heap.WasmPageAllocator.
Эта функция увеличивает размер памяти Wasm, идентифицированной как index, на delta единиц беззнаковых страниц Wasm. Обратите внимание, что каждая страница Wasm имеет размер 64 КБ. В случае успеха возвращает предыдущий размер памяти; в случае неудачи, если выделение не удалось, возвращает -1.
Эта функция является низкоуровневым встроенным инструментом без механизмов безопасности, обычно полезным для разработчиков аллокаторов, ориентированных на Wasm. Поэтому, если вы не пишете новый аллокатор с нуля, используйте что-то вроде @import("std").heap.WasmPageAllocator.
Операция взятия остатка от деления. Для беззнаковых целых чисел это эквивалентно numerator % denominator. Вызывающая функция гарантирует denominator > 0, в противном случае операция приведет к ошибке деления на ноль при включенных проверках безопасности во время выполнения.
@mod(-5, 3) == 1
(@divFloor(a, b) * b) + @mod(a, b) == a
Для функции, возвращающей код ошибки, см. @import("std").math.mod.
Выполняет a * b и возвращает кортеж с результатом и возможным флагом переполнения.
@panic
@panic(message: []const u8) noreturn
Вызывает обработчик ошибок. По умолчанию обработчик ошибок вызывает публичную функцию panic в файле исходного кода корневого модуля, или, если таковая не определена, функцию std.builtin.default_panic из std/builtin.zig.
В общем случае предпочтительнее использовать @import("std").debug.panic. Однако, @panic может быть полезен в двух сценариях:
Из библиотечного кода, вызывая пользовательскую функцию обработки ошибок, если она была определена в корневом файле исходного кода.
При смешанном использовании кода C и Zig, вызывая стандартную функцию обработки ошибок во всех файлах .o.
Подсчитывает количество установленных битов в целом числе — «количество единиц».
Если operand — известное на этапе компиляции целое число, тип результата — comptime_int. В противном случае, тип результата — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, достаточным для представления количества единиц в битах типа целого числа.
Этот встроенный инструмент сообщает компилятору выдать инструкцию предвычисления, если она поддерживается целевым процессором. Если целевой процессор не поддерживает запрошенную инструкцию предвычисления, этот встроенный инструмент является бесполезной операцией. Эта функция не влияет на поведение программы, только на её характеристики производительности.
Аргумент ptr может быть любым типом указателя и определяет адрес памяти для предвычисления. Эта функция не обращается к значению указателя; вполне законно передавать указатель на недействительную память в эту функцию, и это не приведёт к некорректным действиям.
PrefetchOptions можно найти с помощью @import("std").builtin.PrefetchOptions.
@ptrCast
@ptrCast(value: anytype) anytype
Преобразует указатель одного типа в указатель другого типа. Тип возвращаемого значения — выведенный тип результата.
Изменения адресного пространства указателя, используйте @addrSpaceCast.
Увеличения выравнивания указателя, используйте @alignCast.
Преобразования не-слайсового указателя в слайс, используйте синтаксис срезов ptr[start..end].
@ptrFromInt
@ptrFromInt(address: usize) anytype
Преобразует целое число в указатель. Тип возвращаемого значения — выведенный тип результата. Для преобразования в обратном направлении используйте @intFromPtr. Преобразование адреса 0 в целевой тип, который не является указателем с возможностью быть нулевым и не имеет атрибута allowzero, приведёт к ошибке Pointer Cast Invalid Null при включенных проверках безопасности во время выполнения.
Если целевой тип указателя не допускает ноль в качестве адреса, а address равно нулю, это вызовет проверку на ошибки, а значит, неопределённое поведение.
@rem
@rem(numerator: T, denominator: T) T
Операция взятия остатка от деления. Для беззнаковых целых чисел это эквивалентно numerator % denominator. Вызывающая функция гарантирует denominator > 0, в противном случае операция приведет к ошибке деления на ноль при включенных проверках безопасности во время выполнения.
@rem(-5, 3) == -2
(@divTrunc(a, b) * b) + @rem(a, b) == a
Для функции, возвращающей код ошибки, см. @import("std").math.rem.
Эта функция возвращает адрес следующей машинной инструкции, которая будет выполнена после возвращения из текущей функции.
Последствия этого зависят от целевой платформы и не являются консистентными на всех платформах.
Эта функция имеет смысл только в рамках области видимости функции. Если функция встраивается в вызывающую функцию, возвращаемый адрес будет относиться к вызывающей функции.
Выбирает значения поэлементно из a или b на основе pred. Если pred[i] равно true, соответствующий элемент результата будет a[i], в противном случае b[i].
Обеспечивает, что функция будет иметь выравнивание стека как минимум alignment байт.
@setCold
@setCold(comptime is_cold: bool) void
Сообщает оптимизатору, что текущая функция вызывается редко (или часто). Эта функция имеет смысл только в рамках области видимости функции.
@setEvalBranchQuota
@setEvalBranchQuota(comptime new_quota: u32) void
Увеличивает максимальное количество обратных ветвлений, которые может использовать код выполнения во время компиляции, прежде чем отказаться от этого и выдать ошибку компиляции.
Если new_quota меньше значения по умолчанию (1000) или ранее явно заданного значения, оно игнорируется.
Пример:
test_without_setEvalBranchQuota_builtin.zig
test "foo" {
comptime {
var i = 0;
while (i < 1001) : (i += 1) {}
}
}
Командная строка
$ zig test test_without_setEvalBranchQuota_builtin.zigdoc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: error: evaluation exceeded 1000 backwards branches
while (i < 1001) : (i += 1) {}
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: note: use @setEvalBranchQuota() to raise the branch limit from 1000
Теперь используем @setEvalBranchQuota:
test_setEvalBranchQuota_builtin.zig
test "foo" {
comptime {
@setEvalBranchQuota(1001);
var i = 0;
while (i < 1001) : (i += 1) {}
}
}
Командная строка
$ zig test test_setEvalBranchQuota_builtin.zig
1/1 test_setEvalBranchQuota_builtin.test.foo...OK
All 1 tests passed.
Изменяет правила текущей области видимости относительно определения операций с плавающей точкой.
Strict (по умолчанию) — операции с плавающей точкой строго следуют стандарту IEEE.
Optimized — операции с плавающей точкой могут делать следующее:
Предполагать, что аргументы и результат не являются NaN. Оптимизации требуются для сохранения определённого поведения с NaN, но значение результата не определено.
Предполагать, что аргументы и результат не являются +/-Inf. Оптимизации требуются для сохранения определённого поведения с +/-Inf, но значение результата не определено.
Считать знак нулевого аргумента или результата несущественным.
Использовать обратную величину аргумента вместо деления.
Выполнять контракцию операций с плавающей точкой (например, объединение умножения и сложения в умножение-сложение).
Выполнять алгебраически эквивалентные преобразования, которые могут изменить результаты в плавающей точке (например, перегруппировку).
Эквивалентно -ffast-math в GCC.
Режим работы с плавающей точкой наследуется дочерними областями видимости и может быть изменён в любой области видимости. Вы можете установить режим работы с плавающей точкой в области видимости структуры или модуля с помощью блока comptime.
FloatMode можно найти с помощью @import("std").builtin.FloatMode.
Устанавливает, включены ли проверки безопасности во время выполнения для области видимости, содержащей вызов функции.
test_setRuntimeSafety_builtin.zig
test "@setRuntimeSafety" {
// The builtin applies to the scope that it is called in. So here, integer overflow
// will not be caught in ReleaseFast and ReleaseSmall modes:
// var x: u8 = 255;
// x += 1; // undefined behavior in ReleaseFast/ReleaseSmall modes.
{
// However this block has safety enabled, so safety checks happen here,
// even in ReleaseFast and ReleaseSmall modes.
@setRuntimeSafety(true);
var x: u8 = 255;
x += 1;
{
// The value can be overridden at any scope. So here integer overflow
// would not be caught in any build mode.
@setRuntimeSafety(false);
// var x: u8 = 255;
// x += 1; // undefined behavior in all build modes.
}
}
}
Командная строка
$ zig test test_setRuntimeSafety_builtin.zig -OReleaseFast
1/1 test_setRuntimeSafety_builtin.test.@setRuntimeSafety...thread 3579742 panic: integer overflow
/home/andy/src/zig/doc/langref/test_setRuntimeSafety_builtin.zig:11:11: 0x100ae54 in test.@setRuntimeSafety (test)
x += 1;
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x100c9b0 in main (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x100af44 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x100ae71 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/7c74a2939d84ddd0f45519d7a9f1c0f7/test
Примечание: планируется заменить @setRuntimeSafety на @optimizeFor.
@shlExact
@shlExact(value: T, shift_amt: Log2T) T
Выполняет операцию сдвига влево (<<). Для беззнаковых целых чисел результат является неопределённым, если какие-либо биты 1 сдвигаются за пределы. Для знакомых целых чисел результат является неопределённым, если какие-либо биты, не совпадающие со знаковым битом результата, сдвигаются за пределы.
Тип shift_amt — беззнаковое целое число с log2(@typeInfo(T).Int.bits) битами. Это связано с тем, что shift_amt >= @typeInfo(T).Int.bits — неопределённое поведение.
comptime_int моделируется как целое число с бесконечным количеством битов, что означает, что в таком случае @shlExact всегда производит результат и не может привести к ошибке компиляции.
Выполняет a << b и возвращает кортеж с результатом и возможным битом переполнения.
Тип shift_amt — это целое без знака с log2(@typeInfo(@TypeOf(a)).Int.bits) битами. Это связано с тем, что shift_amt >= @typeInfo(@TypeOf(a)).Int.bits — это неопределённое поведение.
Выполняет операцию правого сдвига (>>). Вызывающая сторона гарантирует, что сдвиг не выведет ни один бит 1.
Тип shift_amt — это целое без знака с log2(@typeInfo(T).Int.bits) битами. Это связано с тем, что shift_amt >= @typeInfo(T).Int.bits — это неопределённое поведение.
Создаёт новый вектор, выбирая элементы из a и b на основе mask.
Каждый элемент в mask выбирает элемент из a или b. Положительные числа выбирают из a, начиная с 0. Отрицательные значения выбирают из b, начиная с -1 и двигаясь вниз. Рекомендуется использовать оператор ~ для индексов из b, чтобы оба индекса могли начинаться с 0 (т.е. ~@as(i32, 0) является -1).
Для каждого элемента mask, если он или выбранное значение из a или b равно undefined, то результирующий элемент равен undefined.
a_len и b_len могут отличаться по длине. Индексы элементов, выходящие за пределы границ в mask, приводят к ошибкам компиляции.
Если a или b — это undefined, это эквивалентно вектору всех undefined с такой же длиной, как у другого вектора. Если оба вектора — undefined, @shuffle возвращает вектор со всеми элементами undefined.
E должен быть целым, вещественным, указателем или bool. Маска может иметь любую длину вектора, и её длина определяет длину результата.
test_shuffle_builtin.zig
const std = @import("std");
const expect = std.testing.expect;
test "vector @shuffle" {
const a = @Vector(7, u8){ 'o', 'l', 'h', 'e', 'r', 'z', 'w' };
const b = @Vector(4, u8){ 'w', 'd', '!', 'x' };
// To shuffle within a single vector, pass undefined as the second argument.
// Notice that we can re-order, duplicate, or omit elements of the input vector
const mask1 = @Vector(5, i32){ 2, 3, 1, 1, 0 };
const res1: @Vector(5, u8) = @shuffle(u8, a, undefined, mask1);
try expect(std.mem.eql(u8, &@as([5]u8, res1), "hello"));
// Combining two vectors
const mask2 = @Vector(6, i32){ -1, 0, 4, 1, -2, -3 };
const res2: @Vector(6, u8) = @shuffle(u8, a, b, mask2);
try expect(std.mem.eql(u8, &@as([6]u8, res2), "world!"));
}
Shell
$ zig test test_shuffle_builtin.zig
1/1 test_shuffle_builtin.test.vector @shuffle...OK
All 1 tests passed.
Эта функция возвращает количество байт, необходимое для хранения T в памяти. Результат — целевой константный результат компиляции.
Этот размер может содержать байты заполнения. Если в памяти были два последовательных элемента T, заполнение было бы смещением в байтах между элементом с индексом 0 и элементом с индексом 1. Для целых чисел, подумайте, хотите ли вы использовать @sizeOf(T) или @typeInfo(T).Int.bits.
Эта функция измеряет размер во время выполнения. Для типов, запрещённых во время выполнения, таких как comptime_int и type, результат — 0.
@reduce(comptime op: std.builtin.ReduceOp, value: anytype) E
Преобразует вектор в скалярное значение (типа E) путём последовательного горизонтального редуцирования его элементов с помощью указанного оператора op.
Не все операторы доступны для всех типов элементов вектора:
Обратите внимание, что .Add и .Mul редукции целых типов являются «переполняющими»; при применении к числам с плавающей точкой ассоциативность операции сохраняется, если только режим с плавающей точкой не установлен на Optimized.
test_reduce_builtin.zig
const std = @import("std");
const expect = std.testing.expect;
test "vector @reduce" {
const V = @Vector(4, i32);
const value = V{ 1, -1, 1, -1 };
const result = value > @as(V, @splat(0));
// result is { true, false, true, false };
try comptime expect(@TypeOf(result) == @Vector(4, bool));
const is_all_true = @reduce(.And, result);
try comptime expect(@TypeOf(is_all_true) == bool);
try expect(is_all_true == false);
}
Shell
$ zig test test_reduce_builtin.zig
1/1 test_reduce_builtin.test.vector @reduce...OK
All 1 tests passed.
Тригонометрическая функция синуса для вещественного числа в радианах. Использует специализированную инструкцию аппаратного обеспечения, если она доступна.
Тригонометрическая функция косинуса для вещественного числа в радианах. Использует специализированную инструкцию аппаратного обеспечения, если она доступна.
Тригонометрическая функция тангенса для вещественного числа в радианах. Использует специализированную инструкцию аппаратного обеспечения, если она доступна.
Возвращает абсолютное значение целого или вещественного числа. Использует специализированную инструкцию аппаратного обеспечения, если она доступна. Тип возвращаемого значения — всегда целое без знака той же разрядности, что и операнд, если операнд является целым. Поддерживаются операнды целых чисел без знака. Библиотечная функция не может переполниться для операндов целых чисел со знаком.
Возвращает наибольшее целое значение, не большее, чем данное вещественное число. Использует специализированную инструкцию аппаратного обеспечения, если она доступна.
Возвращает наименьшее целое значение, не меньшее, чем данное вещественное число. Использует специализированную инструкцию аппаратного обеспечения, если она доступна.
Выполняет a - b и возвращает кортеж с результатом и возможным битом переполнения.
@tagName
@tagName(value: anytype) [:0]const u8
Преобразует значение перечисления или значения объединения в строковую литерал, представляющую имя.
Если перечисление не исчерпывающее, и значение тега не соответствует имени, оно вызывает проверку ошибок на неопределённое поведение Undefined Behavior.
@This
@This() type
Возвращает внутреннюю структуру, перечисление или объединение, внутри которого находится этот вызов функции. Это может быть полезно для анонимной структуры, которая должна ссылаться на себя:
$ zig test test_this_builtin.zig
1/1 test_this_builtin.test.@This()...OK
All 1 tests passed.
Когда @This() используется на уровне файла, он возвращает ссылку на структуру, соответствующую текущему файлу.
@trap
@trap() noreturn
Эта функция вставляет платформозависимую инструкцию перехвата/блокировки, которая может быть использована для аварийного выхода из программы. Это может быть реализовано путём явного вывода неверной инструкции, которая может вызвать исключение некорректной инструкции какого-либо типа. В отличие от @breakpoint(), выполнение не продолжается после этого момента.
Внешне по отношению к области действия функции этот встроенный элемент вызывает ошибку компиляции.
Эта функция обрезает биты из целочисленного типа, что приводит к целочисленному типу меньшего или такого же размера. Тип возвращаемого значения — это выведенный тип результата.
Эта функция всегда обрезает значимые биты целого числа, независимо от порядка байтов на целевой платформе.
Вызов @truncate для числа, выходящего за пределы диапазона целевого типа, определён корректно и является работающим кодом:
Информация о типе структур, объединений, перечислений и opaques имеет объявления, которые также гарантированно расположены в том же порядке, что и в исходном файле.
@typeName
@typeName(T: type) *const [N:0]u8
Эта функция возвращает строковое представление типа в виде массива. Она эквивалентна строковой литерале имени типа. Возвращённое имя типа полностью квалифицировано: в имя типа включено родительское пространство имён с использованием точки.
@TypeOf
@TypeOf(...) type
@TypeOf — это специальная встроенная функция, которая принимает любое количество выражений в качестве параметров и возвращает тип результата, используя Резолюцию типа Peers.
Выражения вычисляются, но гарантируется, что они не имеют побочных эффектов во время выполнения:
test_TypeOf_builtin.zig
const std = @import("std");
const expect = std.testing.expect;
test "no runtime side effects" {
var data: i32 = 0;
const T = @TypeOf(foo(i32, &data));
try comptime expect(T == i32);
try expect(data == 0);
}
fn foo(comptime T: type, ptr: *T) T {
ptr.* += 1;
return ptr.*;
}
Оболочка
$ zig test test_TypeOf_builtin.zig
1/1 test_TypeOf_builtin.test.no runtime side effects...OK
All 1 tests passed.
@unionInit
@unionInit(comptime Union: type, comptime active_field_name: []const u8, init_expr) Union
Это то же самое, что и синтаксис инициализации объединений, за исключением того, что имя поля — это значение, известное во время компиляции (comptime), а не идентификатор.
Возвращает индекс рабочей группы в текущем вызове ядра в измерении dimension.
@workGroupSize
@workGroupSize(comptime dimension: u32) u32
Возвращает количество элементов рабочей группы в измерении dimension.
@workItemId
@workItemId(comptime dimension: u32) u32
Возвращает индекс элемента работы в рабочей группе в измерении dimension. Эта функция возвращает значения в диапазоне от 0 (включительно) до @workGroupSize(dimension) (исключительно).
Накладные расходы Асинхронных функций становятся эквивалентны накладным расходам вызова функции.
@import("builtin").single_threaded становится true, и, следовательно, различные API пользовательского уровня, которые считывают эту переменную, становятся более эффективными. Например, std.Mutex становится пустой структурой данных, а все его функции становятся операциями бездействия.
Неопределённое поведение
Zig имеет много случаев неопределённого поведения. Если неопределённое поведение обнаружено во время компиляции, Zig выдает ошибку компиляции и отказывается продолжать. Большинство случаев неопределённого поведения, которые нельзя обнаружить во время компиляции, можно обнаружить во время выполнения. В этих случаях Zig имеет проверки безопасности. Проверки безопасности можно отключить на основе блоков с помощью @setRuntimeSafety. Режимы сборки ReleaseFast и ReleaseSmall отключают все проверки безопасности (за исключением случаев, когда это переопределено с помощью @setRuntimeSafety) для оптимизации.
Когда проверка безопасности терпит неудачу, Zig завершается с трассировкой стека, подобной этой:
test_undefined_behavior.zig
test "safety check" {
unreachable;
}
Оболочка
$ zig test test_undefined_behavior.zig
1/1 test_undefined_behavior.test.safety check...thread 3571226 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_undefined_behavior.zig:2:5: 0x103ce30 in test.safety check (test)
unreachable;
^/home/andy/src/zig/lib/compiler/test_runner.zig:157:25: 0x1048260 in mainTerminal (test)
if (test_fn.func()) |_| {
^/home/andy/src/zig/lib/compiler/test_runner.zig:37:28: 0x103e21b in main (test)
return mainTerminal();
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103d359 in posixCallMainAndExit (test)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103cec1 in _start (test)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/andy/src/zig/.zig-cache/o/a71f9f37afd5b3c603d2b9ea72373fa7/test
$ zig test test_comptime_reaching_unreachable.zigdoc/langref/test_comptime_reaching_unreachable.zig:5:14: error: reached unreachable code
if (!ok) unreachable; // assertion failure
^~~~~~~~~~~
doc/langref/test_comptime_reaching_unreachable.zig:2:11: note: called from here
assert(false);
~~~~~~^~~~~~~
$ zig build-exe runtime_index_out_of_bounds.zig
$ ./runtime_index_out_of_bounds
thread 3567952 panic: index out of bounds: index 5, len 5
/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:7:13: 0x1037169 in foo (runtime_index_out_of_bounds)
return x[5];
^/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:2:18: 0x10350b6 in main (runtime_index_out_of_bounds)
const x = foo("hello");
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034959 in posixCallMainAndExit (runtime_index_out_of_bounds)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x10344c1 in _start (runtime_index_out_of_bounds)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Преобразование отрицательного числа в целое без знака
$ zig test test_comptime_overflow.zigdoc/langref/test_comptime_overflow.zig:3:10: error: overflow of integer type 'u8' with value '256'
byte += 1;
~~~~~^~~~
$ zig test test_comptime_division_by_zero.zigdoc/langref/test_comptime_division_by_zero.zig:4:19: error: division by zero here causes undefined behavior
const c = a / b;
^
Во время выполнения:
runtime_division_by_zero.zig
const std = @import("std");
pub fn main() void {
var a: u32 = 1;
var b: u32 = 0;
_ = .{ &a, &b };
const c = a / b;
std.debug.print("value: {}\n", .{c});
}
Оболочка
$ zig build-exe runtime_division_by_zero.zig
$ ./runtime_division_by_zero
thread 3575479 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_division_by_zero.zig:7:17: 0x10351f6 in main (runtime_division_by_zero)
const c = a / b;
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034a49 in posixCallMainAndExit (runtime_division_by_zero)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x10345b1 in _start (runtime_division_by_zero)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
$ zig test test_comptime_remainder_division_by_zero.zigdoc/langref/test_comptime_remainder_division_by_zero.zig:4:19: error: division by zero here causes undefined behavior
const c = a % b;
^
Во время выполнения:
runtime_remainder_division_by_zero.zig
const std = @import("std");
pub fn main() void {
var a: u32 = 10;
var b: u32 = 0;
_ = .{ &a, &b };
const c = a % b;
std.debug.print("value: {}\n", .{c});
}
Оболочка
$ zig build-exe runtime_remainder_division_by_zero.zig
$ ./runtime_remainder_division_by_zero
thread 3570535 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_remainder_division_by_zero.zig:7:17: 0x10351f6 in main (runtime_remainder_division_by_zero)
const c = a % b;
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034a49 in posixCallMainAndExit (runtime_remainder_division_by_zero)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x10345b1 in _start (runtime_remainder_division_by_zero)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
$ zig test test_comptime_divExact_remainder.zigdoc/langref/test_comptime_divExact_remainder.zig:4:15: error: exact division produced remainder
const c = @divExact(a, b);
^~~~~~~~~~~~~~~
$ zig test test_comptime_unwrap_null.zigdoc/langref/test_comptime_unwrap_null.zig:3:35: error: unable to unwrap null
const number = optional_number.?;
~~~~~~~~~~~~~~~^~
$ zig build-exe runtime_unwrap_null.zig
$ ./runtime_unwrap_null
thread 3570514 panic: attempt to use null value
/home/andy/src/zig/doc/langref/runtime_unwrap_null.zig:6:35: 0x1035272 in main (runtime_unwrap_null)
const number = optional_number.?;
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034ad9 in posixCallMainAndExit (runtime_unwrap_null)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x1034641 in _start (runtime_unwrap_null)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Один из способов избежать этой ошибки заключается в проверке на Null вместо предположения о ненулевом значении с помощью выражения if:
$ zig test test_comptime_invalid_enum_cast.zigdoc/langref/test_comptime_invalid_enum_cast.zig:8:20: error: enum 'test_comptime_invalid_enum_cast.Foo' has no tag with value '3'
const b: Foo = @enumFromInt(a);
^~~~~~~~~~~~~~~
doc/langref/test_comptime_invalid_enum_cast.zig:1:13: note: enum declared here
const Foo = enum {
^~~~
$ zig test test_comptime_invalid_error_set_cast.zigdoc/langref/test_comptime_invalid_error_set_cast.zig:10:19: error: 'error.B' not a member of error set 'error{A,C}'
_ = @as(Set2, @errorCast(Set1.B));
^~~~~~~~~~~~~~~~~~
$ zig build-exe runtime_incorrect_pointer_alignment.zig
$ ./runtime_incorrect_pointer_alignment
thread 3570122 panic: incorrect alignment
/home/andy/src/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:9:64: 0x1034f0a in foo (runtime_incorrect_pointer_alignment)
const int_slice = mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
^/home/andy/src/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:5:12: 0x1034dc7 in main (runtime_incorrect_pointer_alignment)
if (foo(bytes) != 0x11111111) return error.Wrong;
^/home/andy/src/zig/lib/std/start.zig:524:37: 0x1034cc5 in posixCallMainAndExit (runtime_incorrect_pointer_alignment)
const result = root.main() catch |err| {
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x10347e1 in _start (runtime_incorrect_pointer_alignment)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Неправильный доступ к полю объединения
Во время компиляции:
test_comptime_wrong_union_field_access.zig
comptime {
var f = Foo{ .int = 42 };
f.float = 12.34;
}
const Foo = union {
float: f32,
int: u32,
};
Оболочка
$ zig test test_comptime_wrong_union_field_access.zigdoc/langref/test_comptime_wrong_union_field_access.zig:3:6: error: access of union field 'float' while field 'int' is active
f.float = 12.34;
~^~~~~~
doc/langref/test_comptime_wrong_union_field_access.zig:6:13: note: union declared here
const Foo = union {
^~~~~
$ zig build-exe runtime_wrong_union_field_access.zig
$ ./runtime_wrong_union_field_access
thread 3578787 panic: access of union field 'float' while field 'int' is active
/home/andy/src/zig/doc/langref/runtime_wrong_union_field_access.zig:14:6: 0x103cd20 in bar (runtime_wrong_union_field_access)
f.float = 12.34;
^/home/andy/src/zig/doc/langref/runtime_wrong_union_field_access.zig:10:8: 0x103ac5c in main (runtime_wrong_union_field_access)
bar(&f);
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x103a4f9 in posixCallMainAndExit (runtime_wrong_union_field_access)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x103a061 in _start (runtime_wrong_union_field_access)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Эта безопасность недоступна для extern или packed объединений.
Чтобы изменить активное поле объединения, присвойте все объединение целиком, как это показано ниже:
$ zig test test_comptime_out_of_bounds_float_to_integer_cast.zigdoc/langref/test_comptime_out_of_bounds_float_to_integer_cast.zig:3:36: error: float value '4294967296' cannot be stored in integer type 'i32'
const int: i32 = @intFromFloat(float);
^~~~~
$ zig build-exe runtime_out_of_bounds_float_to_integer_cast.zig
$ ./runtime_out_of_bounds_float_to_integer_cast
thread 3570320 panic: integer part of floating point value out of bounds
/home/andy/src/zig/doc/langref/runtime_out_of_bounds_float_to_integer_cast.zig:4:22: 0x1035169 in main (runtime_out_of_bounds_float_to_integer_cast)
const int: i32 = @intFromFloat(float);
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x10349a9 in posixCallMainAndExit (runtime_out_of_bounds_float_to_integer_cast)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x1034511 in _start (runtime_out_of_bounds_float_to_integer_cast)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Некорректное приведение указателя к значению Null
Это происходит при приведении указателя с адресом 0 к указателю, у которого адрес 0 может быть недопустимым. Например, указатели C-указатели, Необязательные указатели и указатели allowzero допускают адрес ноль, но обычные указатели — нет.
$ zig build-exe runtime_invalid_null_pointer_cast.zig
$ ./runtime_invalid_null_pointer_cast
thread 3572791 panic: cast causes pointer to be null
/home/andy/src/zig/doc/langref/runtime_invalid_null_pointer_cast.zig:4:23: 0x10350bc in main (runtime_invalid_null_pointer_cast)
const ptr: *i32 = @ptrCast(opt_ptr);
^/home/andy/src/zig/lib/std/start.zig:514:22: 0x1034929 in posixCallMainAndExit (runtime_invalid_null_pointer_cast)
root.main();
^/home/andy/src/zig/lib/std/start.zig:266:5: 0x1034491 in _start (runtime_invalid_null_pointer_cast)
asm volatile (switch (native_arch) {
^???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Память
Язык Zig не выполняет управление памятью от имени программиста. Именно поэтому у Zig нет времени выполнения, и код Zig работает бесперебойно во многих средах, включая программное обеспечение в реальном времени, ядра операционных систем, встраиваемые устройства и серверы с низкой задержкой. Вследствие этого программисты Zig всегда должны уметь ответить на вопрос:
Как и Zig, язык программирования C имеет ручное управление памятью. Однако в отличие от Zig, C имеет стандартный выделювач — malloc, realloc, и free. При подключении к libc, Zig предоставляет этот выделювач с помощью std.heap.c_allocator. Однако по соглашению, в Zig нет стандартного выделювача. Вместо этого функции, которым нужно выделять память, принимают параметр Allocator. Аналогичным образом структуры данных, такие как std.ArrayList, принимают параметр Allocator в своих функциях инициализации:
$ zig test test_allocator.zig
1/1 test_allocator.test.using an allocator...OK
All 1 tests passed.
В приведенном выше примере для инициализации FixedBufferAllocator используется 100 байт памяти стека, которая затем передается функции. Для удобства доступен глобальный FixedBufferAllocator для быстрых тестов в std.testing.allocator, который также выполнит основную проверку утечек памяти.
Zig имеет универсальный выделювач, доступный для импорта с помощью std.heap.GeneralPurposeAllocator. Тем не менее, рекомендуется следовать руководству Выбор выделювача.
Выбор выделювача
Выбираемый выделювач зависит от ряда факторов. Вот блок-схема, которая поможет вам принять решение:
Разрабатываете ли вы библиотеку? В этом случае лучше принять Allocator в качестве параметра и позволить пользователям вашей библиотеки решить, какой выделювач использовать.
Подключаетесь ли вы к libc? В этом случае std.heap.c_allocator вероятно, является правильным выбором, по крайней мере, для вашего основного выделювача.
Максимальное количество байтов, которое вам понадобится, ограничено числом, известным на этапе компиляции? В этом случае используйте std.heap.FixedBufferAllocator или std.heap.ThreadSafeFixedBufferAllocator, в зависимости от того, нужна ли вам потокобезопасность.
Является ли ваша программа приложением командной строки, которое выполняется от начала до конца без каких-либо основных циклических шаблонов (таких как основной цикл видеоигры или обработчик запросов веб-сервера), так что имеет смысл освободить все сразу в конце? В этом случае рекомендуется следовать этому шаблону: cli_allocation.zig
При использовании такого выделювача нет необходимости вручную освобождать что-либо. Все освобождается сразу с вызовом arena.deinit().
Относятся ли выделения к циклическому шаблону, такому как основной цикл видеоигры или обработчик запросов веб-сервера? Если все выделения можно освободить сразу в конце цикла, например, после того, как фрейм видеоигры был полностью отрисован или запрос веб-сервера был обработан, то std.heap.ArenaAllocator является отличным кандидатом. Как показано в предыдущем пункте, это позволяет вам освободить целые арены сразу. Обратите также внимание, что если можно установить верхнюю границу памяти, то std.heap.FixedBufferAllocator может быть использован как дополнительная оптимизация.
Пишете ли вы тест, и хотите убедиться, что error.OutOfMemory обрабатывается правильно? В этом случае используйте std.testing.FailingAllocator.
Пишете ли вы тест? В этом случае используйте std.testing.allocator.
Наконец, если ни одно из вышеперечисленного не применимо, вам нужен универсальный выделювач. Универсальный выделювач Zig доступен в виде функции, которая принимает структурукомпиляционного конфигурации и возвращает тип. Как правило, вы создадите один std.heap.GeneralPurposeAllocator в своей основной функции, а затем передадите его или подвыделювачи различным частям вашего приложения.
Строковые литералы, такие как "hello", находятся в глобальной секции данных констант. Вот почему ошибка передавать строковый литерал в изменяемый срез, как в этом примере:
test_string_literal_to_slice.zig
fn foo(s: []u8) void {
_ = s;
}
test "string literal to mutable slice" {
foo("hello");
}
Оболочка
$ zig test test_string_literal_to_slice.zigdoc/langref/test_string_literal_to_slice.zig:6:9: error: expected type '[]u8', found '*const [5:0]u8'
foo("hello");
^~~~~~~
doc/langref/test_string_literal_to_slice.zig:6:9: note: cast discards const qualifier
doc/langref/test_string_literal_to_slice.zig:1:11: note: parameter type declared here
fn foo(s: []u8) void {
^~~~
Однако, если вы сделаете срез постоянным, то он будет работать:
test_string_literal_to_const_slice.zig
fn foo(s: []const u8) void {
_ = s;
}
test "string literal to constant slice" {
foo("hello");
}
Оболочка
$ zig test test_string_literal_to_const_slice.zig
1/1 test_string_literal_to_const_slice.test.string literal to constant slice...OK
All 1 tests passed.
Подобно строковым литералам, объявления const, когда значение известно на этапе компиляции, хранятся в глобальной секции данных констант. Также переменные времени компиляции хранятся в глобальной секции данных констант.
Объявления var внутри функций хранятся в кадре стека функции. После возврата функции любые указатели на переменные в кадре стека функции становятся недействительными ссылками, и обращение к ним становится неконтролируемым неопределенным поведением.
Объявления var на верхнем уровне или в объявлениях структур хранятся в глобальной секции данных.
Местоположение памяти, выделенной с помощью allocator.alloc или allocator.create, определяется реализацией выделювача.
TODO: локальные переменные потока
Реализация выделювача
Программисты Zig могут реализовать свои собственные выделювачи, выполнив интерфейс Allocator. Для этого необходимо внимательно изучить документационные комментарии в std/mem.zig, а затем предоставить allocFn и resizeFn.
Существует много примеров выделювачей для вдохновения. Посмотрите на std/heap.zig и std.heap.GeneralPurposeAllocator.
Ошибка выделения памяти в куче
Многие языки программирования выбирают обработать возможность ошибки выделения памяти в куче, безусловно, завершая работу. По соглашению, программисты Zig не считают это удовлетворительным решением. Вместо этого error.OutOfMemory представляет ошибку выделения памяти в куче, и библиотеки Zig возвращают этот код ошибки всякий раз, когда ошибка выделения памяти в куче препятствует успешному выполнению операции.
Некоторые утверждают, что поскольку некоторые операционные системы, такие как Linux, по умолчанию имеют включенную возможность перераспределения памяти, нет смысла обрабатывать ошибку выделения памяти в куче. Здесь много проблем с этим рассуждением:
Только некоторые операционные системы имеют функцию перераспределения памяти.
В Linux она включена по умолчанию, но ее можно настроить.
В Windows перераспределения памяти нет.
Встраиваемые системы не имеют перераспределения памяти.
В любительских операционных системах перераспределение памяти может быть или отсутствовать.
Для систем реального времени не только отсутствует перераспределение памяти, но обычно максимальный объем памяти на приложение определяется заранее.
При написании библиотеки одной из основных целей является повторное использование кода. Правильная обработка ошибок выделения памяти делает библиотеку пригодной для повторного использования в более широком контексте.
Хотя некоторые программные продукты зависят от включения функции перераспределения памяти, ее существование является источником бесчисленных проблем с пользовательским опытом. Когда система с включенным перераспределением памяти, например, Linux по умолчанию, приближается к исчерпанию памяти, система зависает и становится неработоспособной. На этом этапе OOM Killer выбирает приложение для уничтожения на основе эвристических правил. Это непредсказуемое решение часто приводит к уничтожению важного процесса и часто не возвращает систему в рабочее состояние.
Рекурсия
Рекурсия — это фундаментальный инструмент при моделировании программного обеспечения. Однако у нее часто упускается из виду одна проблема: неограниченное выделение памяти.
Короткий вывод заключается в том, что в настоящее время рекурсия работает нормально, как ожидается. Хотя код Zig еще не защищен от переполнения стека, планируется, что будущая версия Zig предоставит такую защиту с некоторой степенью сотрудничества со стороны кода Zig.
Срок службы и владение
Программист Zig несет ответственность за обеспечение того, чтобы к указателю не был выполнен доступ, когда память, на которую он указывает, больше недоступна. Обратите внимание, что срез — это форма указателя, так как он ссылается на другую память.
Чтобы предотвратить ошибки, следует придерживаться некоторых полезных соглашений при работе с указателями. В общем случае, когда функция возвращает указатель, в документации к функции должно быть указано, кто «владеет» указателем. Эта концепция помогает программисту решить, когда и, если это необходимо, освободить указатель.
Например, документация функции может указать «вызывающая функция владеет возвращаемой памятью», в этом случае код, вызывающий функцию, должен иметь план, когда освободить эту память. Вероятно, в этой ситуации функция будет принимать параметр Allocator.
END_OF_DOCUMENT_MARKER
Иногда жизненный цикл указателя может быть более сложным. Например, срез std.ArrayList(T).items имеет жизненный цикл, остающийся действительным до следующего изменения размера списка, например, путем добавления новых элементов.
В документации API для функций и структур данных следует уделять большое внимание объяснению семантики владения и жизненного цикла указателей. Владение определяет, чья ответственность заключается в освобождении памяти, на которую ссылается указатель, а жизненный цикл определяет момент, когда память становится недоступной (чтобы не возникло неопределённое поведение).
Переменные компиляции
Переменные компиляции доступны импортом пакета "builtin", который компилятор делает доступным для каждого файла исходного кода Zig. Он содержит константы времени компиляции, такие как текущий целевой процессор, порядок байтов и режим выпуска.
TODO: Объяснить, как файл исходного кода корня находит другие файлы
TODO: pub fn main
TODO: pub fn panic
TODO: Если происходит связывание с libc, можно использовать export fn main
TODO: Порядок независимых объявлений верхнего уровня
TODO: Ленивый анализ
TODO: Использование comptime { _ = @import() }
Система сборки Zig
Система сборки Zig предоставляет кроссплатформенный и независимый от зависимостей способ объявления логики, необходимой для сборки проекта. С помощью этой системы логика сборки проекта записывается в файл build.zig, используя API системы сборки Zig для объявления и конфигурации артефактов сборки и других задач.
Некоторые примеры задач, которые может помочь решить система сборки:
Выполнение задач параллельно и кэширование результатов.
Зависимость от других проектов.
Предоставление пакета для зависимости от него других проектов.
Создание артефактов сборки путем выполнения компилятора Zig. Это включает в себя сборку исходного кода Zig, а также исходного кода C и C++.
Получение конфигурационных параметров пользователя и использование этих параметров для конфигурации сборки.
Представление конфигурации сборки в виде значений comptime путём предоставления файла, который можно импортировать кодом Zig.
Кэширование артефактов сборки, чтобы избежать излишнего повторения шагов.
Выполнение артефактов сборки или системных инструментов.
Запуск тестов и проверка соответствия выходных данных выполнения артефакта сборки ожидаемому значению.
Запуск zig fmt на кодовой базе или её подмножестве.
Пользовательские задачи.
Для использования системы сборки выполните zig build --help, чтобы увидеть справочное меню командной строки. Оно будет включать параметры, специфичные для проекта, объявленные в скрипте build.zig.
Функция @cImport принимает выражение в качестве параметра. Это выражение вычисляется во время компиляции и используется для управления директивами препроцессора и включения нескольких файлов .h:
@cImport Expression
const builtin = @import("builtin");
const c = @cImport({
@cDefine("NDEBUG", builtin.mode == .ReleaseFast);
if (something) {
@cDefine("_GNU_SOURCE", {});
}
@cInclude("stdlib.h");
if (something) {
@cUndef("_GNU_SOURCE");
}
@cInclude("soundio.h");
});
Возможность трансляции C в Zig доступна как инструмент командной строки через zig translate-c. Он требует единственного имени файла в качестве аргумента. Он также может принять набор необязательных флагов, которые передаются в clang. Он записывает преобразованный файл в стандартный вывод.
-I: Указывает каталог поиска файлов включаемых файлов. Может использоваться несколько раз. Эквивалентно флагу clang -I. Текущий каталог не включается по умолчанию; используйте -I., чтобы включить его.
-D: Определяет макрос препроцессора. Эквивалентно флагу clang -D.
-cflags [flags] --: Передает произвольные дополнительные флаги командной строки в clang. Обратите внимание: список флагов должен заканчиваться --
-target: тройное обозначение целевой архитектуры для преобразованного кода Zig. Если целевая архитектура не указана, будет использоваться текущая целевая архитектура.
Использование -target и -cflags
Важно! При трансляции C-кода с помощью zig translate-c вы обязательно должны использовать ту же -target архитектуру, которую вы будете использовать при компиляции преобразованного кода. Кроме того, вы обязательно должны убедиться, что используемые -cflags, если таковые имеются, соответствуют cflags, используемым на целевой системе. Использование неправильной -target или -cflags может привести к ошибкам парсинга clang или Zig, или к неявным проблемам совместимости ABI при компоновке с C-кодом.
@cImport и zig translate-c используют одинаковый базовый функционал трансляции C, поэтому с технической точки зрения они эквивалентны. На практике @cImport полезно как быстрый и простой способ доступа к числовым константам, определениям типов и записям без необходимости дополнительных настроек. Если вам нужно передать cflags в clang или вы хотите отредактировать преобразованный код, рекомендуется использовать zig translate-c и сохранить результаты в файл. Общие причины редактирования сгенерированного кода включают: изменение параметров anytype в макросах типа функций на более конкретные типы; изменение [*c]T указателей на [*]T или *T указатели для повышения безопасности типов; и включение или отключение проверки времени выполнения в определённых функциях.
Функция трансляции C (используемая как через zig translate-c, так и через @cImport) интегрирована с системой кэширования Zig. Последующие запуски с тем же исходным файлом, целевой архитектурой и cflags будут использовать кэш вместо повторного преобразования того же кода.
Чтобы узнать, где хранятся кэшированные файлы при компиляции кода, использующего @cImport, используйте флаг --verbose-cimport:
$ zig build-exe verbose_cimport_flag.zig -lc --verbose-cimport
info(compilation): C import source: /home/andy/src/zig/.zig-cache/o/f4e9c68cba40c97888f064d67b031021/cimport.h
info(compilation): C import .d file: /home/andy/src/zig/.zig-cache/o/f4e9c68cba40c97888f064d67b031021/cimport.h.d
info(compilation): C import output: /home/andy/src/zig/.zig-cache/o/1b63455e1d0d323f51bdc4909717e28b/cimport.zig
$ ./verbose_cimport_flag
cimport.h содержит файл для трансляции (составленный из вызовов @cInclude, @cDefine, и @cUndef), cimport.h.d — список зависимостей файла, а cimport.zig содержит преобразованный вывод.
Некоторые конструкции C не могут быть преобразованы в Zig — например, goto, структуры с битами и макросы с подстановкой токенов. Zig использует приведение к меньшему типу для продолжения трансляции в случае непереводимых сущностей.
Приведение к меньшему типу бывает трёх видов — непрозрачный, extern и @compileError. C-структуры и объединения, которые не могут быть правильно преобразованы, будут преобразованы как opaque{}. Функции, содержащие непрозрачные типы или конструкции кода, которые не могут быть преобразованы, будут применены к extern объявлениям. Таким образом, непереводимые типы по-прежнему могут использоваться как указатели, а непереводимые функции могут вызываться, если компоновщик знает о скомпилированной функции.
@compileError используется, когда определения верхнего уровня (глобальные переменные, прототипы функций, макросы) не могут быть преобразованы или приведены к меньшему типу. Поскольку Zig использует ленивый анализ для определений верхнего уровня, непереводимые сущности не приведут к ошибке компиляции в вашем коде, пока вы их не будете использовать.
Перевод с C осуществляет попытку перевода макросов, похожих на функции, в эквивалентные функции Zig. Поскольку макросы C работают на уровне лексических токенов, не все макросы C могут быть переведены в Zig. Макросы, которые не могут быть переведены, будут понижены до @compileError. Обратите внимание, что код C, который использует макросы, будет переведён без каких-либо дополнительных проблем (поскольку Zig работает с предварительно обработанным исходным кодом с расширенными макросами). Просто сами макросы могут быть непереводимы в Zig.
Рассмотрим следующий пример:
macro.c
#define MAKELOCAL(NAME, INIT) int NAME = INIT
int foo(void) {
MAKELOCAL(a, 1);
MAKELOCAL(b, 2);
return a + b;
}
Оболочка
$ zig translate-c macro.c > macro.zig
macro.zig
pub export fn foo() c_int {
var a: c_int = 1;
_ = &a;
var b: c_int = 2;
_ = &b;
return a + b;
}
pub const MAKELOCAL = @compileError("unable to translate C expr: unexpected token .Equal"); // macro.c:1:9
Обратите внимание, что foo был переведен правильно, несмотря на использование непереводимого макроса. MAKELOCAL был понижен до @compileError, так как он не может быть выражен как функция Zig; это просто означает, что вы не можете напрямую использовать MAKELOCAL из Zig.
Этот тип следует избегать, когда это возможно. Единственная допустимая причина использования указателя C — это в автоматически сгенерированном коде из перевода кода C.
При импорте заголовочных файлов C неясно, следует ли переводить указатели как указатели на один элемент (*T) или на несколько элементов ([*]T). Указатели C являются компромиссом, чтобы код Zig мог напрямую использовать переведённые заголовочные файлы.
[*c]T — указатель C.
Поддерживает всю синтаксическую конструкцию других двух типов указателей (*T) и ([*]T).
Преобразуется в другие типы указателей, а также в Указатели с возможностью отсутствия. При преобразовании указателя C в не-указатель с возможностью отсутствия, проверка на безопасность Определённое поведение происходит, если адрес равен 0.
Разрешает адрес 0. На не-самостоятельных целях, обращение к адресу 0 проверяется на безопасность Определённое поведение. Указатели C с возможностью отсутствия добавляют ещё один бит для отслеживания значения NULL, как и ?usize. Обратите внимание, что создание указателя C с возможностью отсутствия не требуется, так как можно использовать обычные Указатели с возможностью отсутствия.
Не поддерживает только Zig-атрибуты указателей, такие как выравнивание. Используйте обычные Указатели, пожалуйста!
Когда указатель C указывает на одну структуру (не массив), разыменование указателя C позволяет получить доступ к полям или данным члена структуры. Синтаксис выглядит так:
ptr_to_struct.*.struct_member
Это сравнимо с выполнением -> в C.
Когда указатель C указывает на массив структур, синтаксис возвращается к этому:
const std = @import("std");
const testing = std.testing;
const builtin = @import("builtin");
fn add(count: c_int, ...) callconv(.C) c_int {
var ap = @cVaStart();
defer @cVaEnd(&ap);
var i: usize = 0;
var sum: c_int = 0;
while (i < count) : (i += 1) {
sum += @cVaArg(&ap, c_int);
}
return sum;
}
test "defining a variadic function" {
if (builtin.cpu.arch == .aarch64 and builtin.os.tag != .macos) {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) {
// https://github.com/ziglang/zig/issues/16961
return error.SkipZigTest;
}
try std.testing.expectEqual(@as(c_int, 0), add(0));
try std.testing.expectEqual(@as(c_int, 1), add(1, @as(c_int, 1)));
try std.testing.expectEqual(@as(c_int, 3), add(2, @as(c_int, 1), @as(c_int, 2)));
}
Оболочка
$ zig test test_defining_variadic_function.zig
1/1 test_defining_variadic_function.test.defining a variadic function...OK
All 1 tests passed.
Экспорт библиотеки C
Одним из основных вариантов использования Zig является экспорт библиотеки с C ABI для вызова другими языками программирования. Ключевое слово export перед функциями, переменными и типами делает их частью API библиотеки:
// This header is generated by zig from mathtest.zig
#include "mathtest.h"
#include <stdio.h>
int main(int argc, char **argv) {
int32_t result = add(42, 1337);
printf("%d\n", result);
return 0;
}
Zig поддерживает создание WebAssembly «из коробки».
Самостоятельно работающий
Для сред разработки, таких как веб-браузер и nodejs, создайте исполняемый файл с использованием целевой системы freestanding OS. Вот пример запуска кода Zig, скомпилированного в WebAssembly с помощью nodejs.
Поддержка Zig для WebAssembly System Interface (WASI) находится в активной разработке. Пример использования стандартной библиотеки и чтения аргументов командной строки:
Более интересный пример — извлечение списка преоткрытий из среды выполнения. Теперь это поддерживается в стандартной библиотеке с помощью std.fs.wasi.Preopens:
Цель относится к компьютеру, который будет использоваться для запуска исполняемого файла. Она состоит из архитектуры процессора, набора включённых функций процессора, операционной системы, минимальной и максимальной версии операционной системы, ABI и версии ABI.
Zig — универсальный язык программирования, что означает, что он разработан для генерации оптимального кода для широкого набора целей. Команда zig targets предоставляет информацию обо всех целях, которые известны компилятору.
Если опция целевого компьютера не указана в компиляторе, по умолчанию выбирается хост-компьютер, что означает, что полученный исполняемый файл не подходит для копирования на другой компьютер. Для копирования исполняемого файла на другой компьютер, компилятору необходимо знать требования к целевому компьютеру через опцию -target.
Стандартная библиотека Zig (@import("std")) содержит платформенно-независимые абстракции, что делает один и тот же исходный код пригодным для многих целей. Некоторые коды более переносимы, чем другие. В целом, код Zig чрезвычайно переносим по сравнению с другими языками программирования.
Каждая платформа требует своих собственных реализаций, чтобы обеспечить работу кроссплатформенных абстракций Zig. Эти реализации находятся на различных стадиях завершения. Каждый тегированный выпуск компилятора поставляется с примечаниями к выпуску, которые содержат полную таблицу поддержки для каждой цели.
Руководство по стилю
Эти соглашения по кодированию не навязываются компилятором, но они поставляются в этом руководстве вместе с компилятором, чтобы предоставить точку отсчёта, если кто-то захочет сослаться на авторитет по согласованному стилю кодирования Zig.
Избегайте избыточности в именах
Избегайте этих слов в именах типов:
Значение
Данные
Контекст
Менеджер
utils, misc или инициалы кого-либо
Всё является значением, все типы — данными, всё — контекстом, вся логика управляет состоянием. Ничто не передаётся с помощью слова, применимого ко всем типам.
Искушение использовать «утилиты», «разное» или инициалы кого-то — это неумение классифицировать или, чаще, чрезмерная классификация. Такие объявления могут жить в корне модуля, который их нуждается, без необходимости в именованном пространстве.
Избегайте избыточных имён в полных квалифицированных пространствах имён
Каждому объявлению компилятор присваивает полное квалифицированное пространство имён, создавая древовидную структуру. Выбирайте имена, основанные на полном квалифицированном пространстве имён, и избегайте избыточных сегментов имён.
В этом примере «json» повторяется в полном квалифицированном пространстве имён. Решением является удаление Json из JsonValue. В этом примере у нас есть пустая структура с именем json, но помните, что файлы также действуют как часть полного квалифицированного пространства имён.
Этот пример является исключением из правила, указанного в Избегайте избыточности в именах. Значение типа было сведено к его сути: это значение json. Название не может быть более конкретным без искажения.
Отступы
Отступы: 4 пробела
Открывающие фигурные скобки на одной строке, если только вам не нужно переносить.
Если список элементов содержит более 2 элементов, помещайте каждый элемент на отдельную строку и используйте возможность добавления дополнительной запятой в конце.
Длина строки: стремитесь к 100; руководствуйтесь здравым смыслом.
Имена
Грубо говоря: camelCaseFunctionName, TitleCaseTypeName, snake_case_variable_name. Более точно:
Если x является type, то x должно быть TitleCase, за исключением случаев, когда это struct с 0 полями и никогда не должно быть экземпляризовано, в этом случае оно рассматривается как «пространство имён» и использует snake_case.
Если x вызываемый, и тип возвращаемого значения x является type, то x должно быть TitleCase.
Если x вызываемый иначе, то x должно быть camelCase.
В противном случае, x должно быть snake_case.
Аббревиатуры, начальные слова, собственные имена или любые другие слова, имеющие правила прописных букв в английском языке, подчиняются правилам именования, как и любое другое слово. Даже аббревиатуры длиной всего в 2 буквы подчиняются этим правилам.
Имена файлов делятся на два типа: типы и пространства имён. Если файл (неявный структуру) имеет поля верхнего уровня, его следует именовать как любую другую структуру с полями, используя TitleCase. В противном случае, следует использовать snake_case. Имена каталогов должны быть snake_case.
Это общие рекомендации; если имеет смысл сделать что-то другое, сделайте то, что имеет смысл. Например, если существует установленная конвенция, такая как ENOENT, следуйте установленной конвенции.
Примеры
style_example.zig
const namespace_name = @import("dir_name/file_name.zig");
const TypeName = @import("dir_name/TypeName.zig");
var global_var: i32 = undefined;
const const_name = 42;
const primitive_type_alias = f32;
const string_alias = []u8;
const StructName = struct {
field: i32,
};
const StructAlias = StructName;
fn functionName(param_name: TypeName) void {
var functionPointer = functionName;
functionPointer();
functionPointer = otherFunction;
functionPointer();
}
const functionAlias = functionName;
fn ListTemplateFunction(comptime ChildType: type, comptime fixed_size: usize) type {
return List(ChildType, fixed_size);
}
fn ShortList(comptime T: type, comptime n: usize) type {
return struct {
field_name: [n]T,
fn methodName() void {}
};
}
// The word XML loses its casing when used in Zig identifiers.
const xml_document =
\\<?xml version="1.0" encoding="UTF-8"?>
\\<document>
\\</document>
;
const XmlParser = struct {
field: i32,
};
// The initials BE (Big Endian) are just another word in Zig identifier names.
fn readU32Be() u32 {}
Исключите любую информацию, которая избыточна на основе названия документируемого объекта.
Рекомендуется дублировать информацию по нескольким аналогичным функциям, так как это помогает IDE и другим инструментам предоставлять более подробную справку.
Используйте слово предполагать для указания инвариантов, которые вызывают неопределённое поведение при нарушении.
Используйте слово утверждать для указания инвариантов, которые вызывают проверяемое на безопасностьнеопределённое поведение при нарушении.
Кодировка исходного кода
Исходный код Zig закодирован в UTF-8. Недействительная последовательность байтов UTF-8 приводит к ошибке компиляции.
Во всем исходном коде Zig (включая комментарии) некоторые кодовые точки запрещены:
Управляющие символы ASCII, за исключением U+000a (LF), U+000d (CR) и U+0009 (HT): U+0000 - U+0008, U+000b - U+000c, U+000e - U+0001f, U+007f.
Не-ASCII Unicode окончания строк: U+0085 (NEL), U+2028 (LS), U+2029 (PS).
LF (значение байта 0x0a, кодовая точка U+000a, '\n') — разделитель строк в исходном коде Zig. Это значение байта завершает каждую строку исходного кода Zig, кроме последней строки файла. Рекомендуется, чтобы непустые исходные файлы заканчивались пустой строкой, что означает, что последний байт будет 0x0a (LF).
Каждый LF может быть немедленно предшествующим одиночным CR (значение байта 0x0d, кодовая точка U+000d, '\r') для создания окончания строки в стиле Windows, но это не рекомендуется. Обратите внимание, что в многострочных строках последовательности CRLF будут закодированы как LF при компиляции в программу Zig. CR в любом другом контексте не допускается.
HT (табуляция) (значение байта 0x09, кодовая точка U+0009, '\t') взаимозаменяемы с SP (пробел) (значение байта 0x20, кодовая точка U+0020, ' ') в качестве разделителя токенов, но использование табуляции не рекомендуется. См. Грамматика.
Для совместимости с другими инструментами компилятор игнорирует метку порядка байтов UTF-8 (U+FEFF), если она является первой кодовой точкой Unicode в исходном тексте. Метка порядка байтов не допускается больше нигде в исходном коде.
Обратите внимание, что запуск zig fmt на исходном файле выполнит все здесь упомянутые рекомендации.
Обратите внимание, что инструмент, читающий исходный код Zig, может сделать предположения, если исходный код предполагается корректным исходным кодом Zig. Например, при определении концов строк инструмент может использовать простой поиск, такой как /\n/, или расширенный поиск, такой как /\r\n?|[\n\u0085\u2028\u2029]/, и в любом случае окончания строк будут правильно определены. Другой пример: при определении пробелов перед первым токеном в строке инструмент может использовать простой поиск, такой как /[ \t]/, или расширенный поиск, такой как /\s/, и в любом случае пробелы будут правильно определены.
Справочник по ключевым словам
Ключевое слово
Описание
addrspace
Ключевое слово addrspace.
TODO добавить документацию для addrspace
align
align может использоваться для указания выравнивания указателя. Также его можно использовать после объявления переменной или функции для указания выравнивания указателей на эту переменную или функцию.
await может использоваться для приостановки текущей функции до завершения фрейма, предоставленного после await. await копирует значение, возвращенное из фрейма целевой функции, в вызывающую функцию.
break может использоваться с меткой блока для возврата значения из блока. Также его можно использовать для выхода из цикла до естественного завершения итерации.
catch может использоваться для оценки выражения, если выражение перед ним оценивается как ошибка. Выражение после catch может необязательно захватить значение ошибки.
comptime перед объявлением может использоваться для маркировки переменных или параметров функций как известных на этапе компиляции. Также его можно использовать для гарантии выполнения выражения на этапе компиляции.
const объявляет переменную, которая не может быть изменена. Используется как атрибут указателя, он обозначает, что значение, на которое указывает указатель, не может быть изменено.
errdefer выполнит выражение, когда поток управления покинет текущий блок, если функция возвращает ошибку, выражение errdefer может захватить значение без обертки.
export делает функцию или переменную внешне видимой в сгенерированном объектом файле. Экспортируемые функции по умолчанию используют соглашение о вызовах C.
extern может использоваться для объявления функции или переменной, которая будет разрешена во время линковки (при статической линковке) или во время выполнения (при динамической линковке).
Выражение if может проверять булевы выражения, значения с возможностью отсутствия или союзы ошибок. Для значений с возможностью отсутствия или союзов ошибок, выражение if может захватить значение без обертки.
inline может использоваться для маркировки выражения цикла, таким образом, оно будет развёрнуто на этапе компиляции. Также его можно использовать для принудительного встраивания функции во все места вызова.
Ключевое слово nosuspend может использоваться перед блоком, оператором или выражением, чтобы отметить область, где не достигаются точки приостановки. В частности, внутри области nosuspend:
Использование ключевого слова suspend приводит к ошибке компиляции.
Использование await на фрейме функции, который ещё не завершен, приводит к проверке на ошибки неопределённого поведения.
Вызов асинхронной функции может привести к проверке на ошибки неопределённого поведения, потому что он эквивалентен await async some_async_fn(), который содержит await.
Код внутри области nosuspend не делает окружающую функцию асинхронной функцией.
suspend заставит поток управления вернуться в точку вызова или возобновления функции. suspend также может использоваться перед блоком внутри функции, чтобы позволить функции получить доступ к своему фрейму перед возвратом потока управления в точку вызова.
switch
Выражение switch может использоваться для проверки значений общего типа. switch случаи могут захватывать значения полей размеченного союза.
try вычисляет выражение объединения ошибок. Если это ошибка, то функция возвращает управление с той же ошибкой. В противном случае выражение приводит к распакованному значению.
unreachable может использоваться для утверждения, что поток управления никогда не достигнет определённой точки. В зависимости от режима сборки, unreachable может вызвать панику.
Вызывает панику в режимах Debug и ReleaseSafe, или при использовании zig test.
Не вызывает панику в режимах ReleaseFast и ReleaseSmall.
usingnamespace — это объявление верхнего уровня, которое импортирует все публичные объявления операнда (который должен быть структурой, объединением или перечислением) в текущую область видимости.
volatile можно использовать для обозначения того, что загрузка или сохранение указателя имеют побочные эффекты. Также может изменить выражение встроенного ассемблера, чтобы обозначить, что оно имеет побочные эффекты.
Выражение while может использоваться для многократной проверки булевого, необязательного или объединения выражений ошибок, и прекращения цикла, когда выражение принимает значение false, null или ошибку соответственно.
Контейнер в Zig — это любой синтаксический конструкт, который действует как пространство имён для хранения объявлений переменных и функций. Контейнеры также являются определениями типов, которые могут быть экземпляризованы. Структуры, перечисления, объединения, непрозрачные типы, и даже сами файлы исходного кода Zig являются контейнерами.
Хотя контейнеры (кроме файлов исходного кода Zig) используют фигурные скобки для окружения своего определения, их не следует путать с блоками или функциями. Контейнеры не содержат операторов.