Zig — это язык программирования общего назначения и набор инструментов для создания надёжного, оптимального и повторно используемого программного обеспечения.
Надёжность
Корректная работа даже в крайних случаях, например при нехватке памяти.
Оптимальность
Написание программ так, чтобы они работали и выполнялись наилучшим образом.
Повторное использование
Один и тот же код работает в разных средах с различными ограничениями.
Удобство сопровождения
Точная передача намерений компилятору и другим программистам. Язык требует мало усилий для чтения кода и устойчив к изменению требований и сред.
Часто самый эффективный способ узнать что-то новое — посмотреть примеры, поэтому в этой документации показано, как использовать каждую возможность Zig. Документация размещена на одной странице, чтобы вы могли выполнять поиск средствами браузера.
Примеры кода в этом документе компилируются и проверяются в составе основного набора тестов Zig.
Этот HTML-документ не зависит от внешних файлов, поэтому его можно использовать автономно.
Стандартная библиотека Zig содержит часто используемые алгоритмы, структуры данных и определения, помогающие создавать программы и библиотеки. В этой документации вы встретите множество примеров использования стандартной библиотеки Zig. Чтобы узнать о ней больше, перейдите по ссылке выше.
Кроме того, документация стандартной библиотеки Zig поставляется с каждым дистрибутивом Zig. Её можно отобразить с помощью локального веб-сервера:
В большинстве случаев уместнее выводить данные в stderr, а успешная запись сообщения в поток не имеет значения. Кроме того, часто бывает полезна форматированная печать. Для этого распространённого случая существует более простой 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.zig/home/andy/src/zig/doc/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.zig/home/andy/src/zig/doc/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.
};
Идентификаторы
Идентификаторы должны начинаться с буквы или символа подчёркивания; далее может следовать любое количество букв, цифр и символов подчёркивания. Они не должны совпадать с ключевыми словами. См. раздел Справочник по ключевым словам.
Синтаксис строковых идентификаторов
Если требуется имя, не соответствующее этим требованиям, например для связывания с внешними библиотеками, можно использовать синтаксис @"".
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";
беззнаковое целое число размером с указатель. См. также #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
long 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. Тип целочисленных литералов.
comptime_float
(нет)
Допускается только для значений, известных во время comptime. Тип литералов с плавающей точкой.
Помимо перечисленных выше целочисленных типов, можно использовать целые числа произвольной разрядности: для этого указывается идентификатор i или u, за которым следуют цифры. Например, идентификатор i7 обозначает знаковое 7-битное целое число. Максимальная допустимая разрядность целочисленного типа — 65535.
Строковые литералы и литералы кодовых точек Unicode
Строковые литералы — это константные одноэлементные указатели на массивы байтов с нулевым терминатором. Тип строковых литералов кодирует и длину, и наличие нулевого терминатора, поэтому такие литералы можно приводить как к срезам, так и к указателям с нулевым терминатором. Разыменование строковых литералов преобразует их в массивы.
Поскольку исходный код Zig имеет кодировку UTF-8, любые не-ASCII-байты в строковом литерале исходного кода сохраняют своё значение UTF-8 в строке программы на Zig; компилятор не изменяет эти байты. В строковый литерал можно включить байты, не являющиеся UTF-8, с помощью записи \xNN.
При индексировании строки, содержащей не-ASCII-байты, возвращаются отдельные байты независимо от того, образуют ли они допустимую последовательность UTF-8.
Литералы кодовых точек Unicode имеют тип comptime_int, как и целочисленные литералы. Все escape-последовательности допустимы как в строковых литералах, так и в литералах кодовых точек Unicode.
string_literals.zig
const print = @import("std").debug.print;
const mem = @import("std").mem; // will be used to compare bytes
pub fn main() void {
const bytes = "hello";
print("{}\n", .{@TypeOf(bytes)}); // *const [5:0]u8
print("{d}\n", .{bytes.len}); // 5
print("{c}\n", .{bytes[1]}); // 'e'
print("{d}\n", .{bytes[5]}); // 0
print("{}\n", .{'e' == '\x65'}); // true
print("{d}\n", .{'\u{1f4a9}'}); // 128169
print("{d}\n", .{'💯'}); // 128175
print("{u}\n", .{'⚡'});
print("{}\n", .{mem.eql(u8, "hello", "h\x65llo")}); // true
print("{}\n", .{mem.eql(u8, "💯", "\xf0\x9f\x92\xaf")}); // also true
const invalid_utf8 = "\xff\xfe"; // non-UTF-8 strings are possible with \xNN notation.
print("0x{x}\n", .{invalid_utf8[1]}); // indexing them returns individual bytes...
print("0x{x}\n", .{"💯"[1]}); // ...as does indexing part-way through non-ASCII characters
}
В многострочных строковых литералах нет escape-последовательностей, и они могут занимать несколько строк. Чтобы начать многострочный строковый литерал, используйте токен \\. Как и комментарий, строковый литерал продолжается до конца строки. Конец строки не включается в строковый литерал. Однако если следующая строка начинается с \\, к строке добавляется символ перевода строки, и строковый литерал продолжается.
Используйте ключевое слово 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:5: error: cannot assign to constant
y += 1;
^
referenced by:
main: /home/andy/src/zig/doc/langref/constant_identifier_cannot_change.zig:12:8
callMain [inlined]: /home/andy/src/zig/lib/std/start.zig:685:59
callMainWithArgs [inlined]: /home/andy/src/zig/lib/std/start.zig:637:20
posixCallMainAndExit: /home/andy/src/zig/lib/std/start.zig:590:38
2 reference(s) hidden; use '-freference-trace=6' to see all references
const распространяется на все байты, на которые непосредственно указывает идентификатор. У указателей есть собственное свойство 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 означает: «Это не имеющее смысла значение. Использование этого значения будет ошибкой. Значение не будет использовано или будет перезаписано до использования».
В режимах Debug и ReleaseSafe Zig записывает байты 0xaa в неопределённую память. Это позволяет рано обнаруживать ошибки и помогает выявлять использование неопределённой памяти в отладчике. Однако такое поведение является лишь особенностью реализации, а не семантикой языка, поэтому код не гарантированно сможет его обнаружить.
Присваивание с деструктуризацией позволяет разделить элементы индексируемых агрегатных типов (кортежей, массивов, векторов):
destructuring_to_existing.zig
const print = @import("std").debug.print;
pub fn main() void {
var x: u32 = undefined;
var y: u32 = undefined;
var z: u32 = undefined;
const tuple = .{ 1, 2, 3 };
x, y, z = tuple;
print("tuple: x = {}, y = {}, z = {}\n", .{x, y, z});
const array = [_]u32{ 4, 5, 6 };
x, y, z = array;
print("array: x = {}, y = {}, z = {}\n", .{x, y, z});
const vector: @Vector(3, u32) = .{ 7, 8, 9 };
x, y, z = vector;
print("vector: x = {}, y = {}, z = {}\n", .{x, y, z});
}
Оболочка
$ zig build-exe destructuring_to_existing.zig
$ ./destructuring_to_existing
tuple: x = 1, y = 2, z = 3
array: x = 4, y = 5, z = 6
vector: x = 7, y = 8, z = 9
Выражение деструктуризации может находиться только внутри блока (то есть не на уровне контейнера). Левая часть присваивания должна состоять из списка элементов, разделённых запятыми; каждый элемент может быть lvalue (например, существующей переменной `var`) или объявлением переменной:
destructuring_mixed.zig
const print = @import("std").debug.print;
pub fn main() void {
var x: u32 = undefined;
const tuple = .{ 1, 2, 3 };
x, var y : u32, const z = tuple;
print("x = {}, y = {}, z = {}\n", .{x, y, z});
// y is mutable
y = 100;
// You can use _ to throw away unwanted values.
_, x, _ = tuple;
print("x = {}", .{x});
}
Оболочка
$ zig build-exe destructuring_mixed.zig
$ ./destructuring_mixed
x = 1, y = 2, z = 3
x = 2
Перед деструктуризацией можно поставить ключевое слово comptime; в этом случае всё выражение деструктуризации вычисляется во время comptime. Все объявленные var будут comptimevar, а все выражения (как расположения результатов, так и выражение присваиваемого значения) вычисляются во время comptime.
Код, написанный в одном или нескольких объявлениях 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);
// However, in most cases it is more convenient to use a more specific function like `expectEqual`.
// This gives you much clearer and more helpful error messages when a test fails.
try std.testing.expectEqual(42, addOne(41));
}
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.expectEqual(42, addOne(41));
}
/// 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, чтобы убедиться, что при входном значении 41 она возвращает 42. С точки зрения этого теста функция 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 — это объединение ошибок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:615:14: 0x1234e39 in expect (std.zig)
if (!ok) return error.TestUnexpectedResult;
^
/home/andy/src/zig/doc/langref/testing_failure.zig:4:5: 0x1234f2e in test.expect this to fail (testing_failure.zig)
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/8b3fa8da11c9a50847bac61c047fa567/test --seed=0xf0359694
Пропуск тестов
Один из способов пропустить тесты — отфильтровать их с помощью параметра командной строки --test-filter [text] команды zig test. При этом в тестовую сборку включаются только тесты, имя которых содержит указанный текст фильтра. Обратите внимание: безымянные тесты запускаются даже при использовании параметра командной строки --test-filter [text].
Чтобы программно пропустить тест, верните из 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, стандартный исполнитель тестов сообщит об обнаруженных утечках, связанных с использованием тестового аллокатора:
$ zig test testing_detect_test.zig
1/1 testing_detect_test.test.builtin.is_test...OK
All 1 tests passed.
Вывод тестов и ведение журнала
Стандартный исполнитель тестов и пространство имён тестирования стандартной библиотеки Zig выводят сообщения в поток стандартной ошибки.
Пространство имён Testing
Пространство имён testing стандартной библиотеки Zig содержит полезные функции, помогающие создавать тесты. Помимо функции 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 также есть функции для сравнения срезов, строк и других значений. Другие доступные функции можно найти в пространстве имён std.testingстандартной библиотеки Zig.
Документация инструмента тестирования
У zig test есть несколько параметров командной строки, влияющих на компиляцию. Полный список см. в выводе команды zig test --help.
При объявлении переменной обычно предпочтительнее использовать const, а не var. Это облегчает чтение кода как людям, так и компьютерам и создаёт больше возможностей для оптимизации.
Переменным запрещено затенять идентификаторы из внешней области видимости.
Для связывания с переменной, экспортированной из другого объектного файла, можно использовать ключевое слово extern или встроенную функцию @extern. Чтобы сделать переменную доступной другим объектным файлам на этапе компоновки, можно использовать ключевое слово export или встроенную функцию @export. В обоих случаях тип переменной должен быть совместим с ABI C.
Переменные уровня контейнера имеют статическое время жизни, не зависят от порядка объявления и анализируются отложенно. Начальное значение переменных уровня контейнера неявно вычисляется во время 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 expectEqual = std.testing.expectEqual;
test "comptime vars" {
var x: i32 = 1;
comptime var y: i32 = 1;
x += 1;
y += 1;
try expectEqual(2, x);
try expectEqual(2, y);
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 использует представление в дополнительном коде.
Zig поддерживает следующие типы чисел с плавающей запятой:
f16 — IEEE-754-2008 binary16
f32 — IEEE-754-2008 binary32
f64 — IEEE-754-2008 binary64
f80 — IEEE-754-2008, расширенная точность 80 бит
f128 — IEEE-754-2008 binary128
c_longdouble — соответствует long double для целевого ABI языка C
Литералы с плавающей запятой
Литералы с плавающей запятой имеют тип 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
Для этого теста необходимо разделить код на два объектных файла — иначе оптимизатор вычислит все значения во время компиляции, когда действует строгий режим.
Знаковые целочисленные операнды должны быть известны на этапе comptime и быть положительными. В остальных случаях используйте вместо этого @divTrunc, @divFloor или @divExact.
Знаковые операнды и операнды с плавающей запятой должны быть известны на этапе comptime и быть положительными. В остальных случаях используйте вместо этого @rem или @mod.
Если a равно null, возвращает b («значение по умолчанию»), иначе возвращает распакованное значение a. Обратите внимание, что b может быть значением типа noreturn.
Если a является error, возвращает b («значение по умолчанию»), иначе возвращает распакованное значение a. Обратите внимание, что b может быть значением типа noreturn. err — это error, доступное в области видимости выражения b.
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual('h' + 'e' + 'l' * 2 + 'o', sum);
}
// modifiable array
var some_integers: [100]i32 = undefined;
test "modify an array" {
for (&some_integers, 0..) |*item, i| {
item.* = @intCast(i);
}
try expectEqual(10, some_integers[10]);
try expectEqual(99, some_integers[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 expectEqual(4, fancy_array[4].x);
try expectEqual(8, fancy_array[4].y);
}
// 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 expectEqual(3, more_points[4].x);
try expectEqual(6, more_points[4].y);
try expectEqual(10, more_points.len);
}
Командная оболочка
$ 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-инструкций, если это возможно. Типы векторов создаются встроенной функцией @Vector.
Векторы, как правило, поддерживают те же встроенные операторы, что и их базовые типы. Единственное исключение — ключевые слова `and` и `or` для векторов логических значений: эти операторы влияют на поток управления, что для векторов недопустимо. Все остальные операции выполняются поэлементно и возвращают вектор той же длины, что и входные векторы. К ним относятся:
Арифметические операции (+, -, /, *, @divFloor, @sqrt, @ceil, @log и т. д.)
Побитовые операторы (>>, <<, &, |, ~ и т. д.)
Операторы сравнения (<, >, == и т. д.)
Логическое НЕ (!)
Запрещено применять математический оператор к сочетанию скаляров (отдельных чисел) и векторов. В Zig есть встроенная функция @splat, позволяющая легко преобразовать скаляры в векторы; для преобразования векторов в скаляры предусмотрены @reduce и синтаксис индексирования массивов. Векторы также поддерживают присваивание из массивов фиксированной длины и обратно, если длина известна на этапе comptime.
Для перестановки элементов внутри векторов и между ними в Zig предусмотрены функции @shuffle и @select.
Операции над векторами, длина которых меньше собственного SIMD-размера целевой машины, обычно компилируются в одну SIMD-инструкцию; векторы, длина которых больше собственного SIMD-размера целевой машины, компилируются в несколько SIMD-инструкций. Если для данной операции в целевой архитектуре нет поддержки SIMD, компилятор по умолчанию обрабатывает элементы вектора по одному. Zig поддерживает любую длину вектора, известную на этапе comptime, вплоть до 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 can be coerced to arrays, and vice versa.
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
Векторы и массивы имеют чётко определённое битовое расположение, поэтому поддерживают взаимное преобразование с помощью @bitCast. Приведение типов неявно выполняет @bitCast.
Байтовое расположение массивов определено, а векторов — нет, поэтому применение @ptrCast для преобразования между ними приводит к недопустимому поведению.
*[N]T — указатель на N элементов, эквивалентный указателю на один элемент — массив.
Поддерживает синтаксис индексирования: array_ptr[i]
Поддерживает синтаксис среза: array_ptr[start..end]
Поддерживает свойство len: array_ptr.len
Поддерживает вычитание указателей: array_ptr - array_ptr
[]T — это срез (толстый указатель, содержащий указатель типа [*]T и длину).
Поддерживает синтаксис индексирования: slice[i]
Поддерживает синтаксис среза: slice[start..end]
Поддерживает свойство len: slice.len
Используйте &x, чтобы получить указатель на один элемент:
test_single_item_pointer.zig
const expectEqual = @import("std").testing.expectEqual;
test "address of syntax" {
// Get the address of a variable:
const x: i32 = 1234;
const x_ptr = &x;
// Dereference a pointer:
try expectEqual(1234, x_ptr.*);
// When you get the address of a const variable, you get a const single-item pointer.
try expectEqual(*const i32, @TypeOf(x_ptr));
// 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 expectEqual(*i32, @TypeOf(y_ptr));
y_ptr.* += 1;
try expectEqual(5679, y_ptr.*);
}
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 expectEqual(*u8, @TypeOf(ptr));
try expectEqual(3, array[2]);
ptr.* += 1;
try expectEqual(4, array[2]);
}
test "slice syntax" {
// Get a pointer to a variable:
var x: i32 = 1234;
const x_ptr = &x;
// Convert to array pointer using slice syntax:
const x_array_ptr = x_ptr[0..1];
try expectEqual(*[1]i32, @TypeOf(x_array_ptr));
// Coerce to many-item pointer:
const x_many_ptr: [*]i32 = x_array_ptr;
try expectEqual(1234, x_many_ptr[0]);
}
Командная оболочка
$ zig test test_single_item_pointer.zig
1/3 test_single_item_pointer.test.address of syntax...OK
2/3 test_single_item_pointer.test.pointer array access...OK
3/3 test_single_item_pointer.test.slice syntax...OK
All 3 tests passed.
Zig поддерживает арифметику указателей. Лучше присвоить указатель переменной [*]T и увеличивать значение этой переменной. Например, прямое увеличение указателя, полученного из среза, повредит его.
test_pointer_arithmetic.zig
const expectEqual = @import("std").testing.expectEqual;
test "pointer arithmetic with many-item pointer" {
const array = [_]i32{ 1, 2, 3, 4 };
var ptr: [*]const i32 = &array;
try expectEqual(1, ptr[0]);
ptr += 1;
try expectEqual(2, ptr[0]);
// slicing a many-item pointer without an end is equivalent to
// pointer arithmetic: `ptr[start..] == ptr + start`
try expectEqual(ptr[1..], ptr + 1);
// subtraction between any two pointers except slices based on element size is supported
try expectEqual(1, &ptr[1] - &ptr[0]);
}
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 expectEqual(1, slice[0]);
try expectEqual(4, slice.len);
slice.ptr += 1;
// now the slice is in an bad state since len has not been updated
try expectEqual(2, slice[0]);
try expectEqual(4, slice.len);
}
Командная оболочка
$ 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.
Для срезов проверяются границы, поэтому они защищены от такого рода недопустимого поведения. Это одна из причин, по которой мы предпочитаем срезы указателям.
test_slice_bounds.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(2, slice.len);
try expectEqual(4, array[3]);
slice[1] += 1;
try expectEqual(5, array[3]);
}
Командная оболочка
$ zig test test_slice_bounds.zig
1/1 test_slice_bounds.test.pointer slicing...OK
All 1 tests passed.
Указатели работают и во время компиляции, если код не зависит от неопределённого расположения в памяти:
test_comptime_pointers.zig
const expectEqual = @import("std").testing.expectEqual;
test "comptime pointers" {
comptime {
var x: i32 = 1;
const ptr = &x;
ptr.* += 1;
x += 1;
try expectEqual(3, ptr.*);
}
}
Командная оболочка
$ zig test test_comptime_pointers.zig
1/1 test_comptime_pointers.test.comptime pointers...OK
All 1 tests passed.
Чтобы преобразовать целочисленный адрес в указатель, используйте @ptrFromInt. Чтобы преобразовать указатель в целое число, используйте @intFromPtr:
$ zig test test_integer_pointer_conversion.zig
1/1 test_integer_pointer_conversion.test.@intFromPtr and @ptrFromInt...OK
All 1 tests passed.
Zig может сохранять адреса памяти в коде comptime, если указатель никогда не разыменовывается:
test_comptime_pointer_conversion.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(usize, @TypeOf(addr));
try expectEqual(0xdeadbee0, addr);
}
}
Командная оболочка
$ zig test test_comptime_pointer_conversion.zig
1/1 test_comptime_pointer_conversion.test.comptime @ptrFromInt...OK
All 1 tests passed.
@ptrCast преобразует тип элемента указателя в другой тип. При этом создаётся новый указатель, который может привести к необнаружимому недопустимому поведению в зависимости от проходящих через него операций чтения и записи. Если возможно, обычно предпочтительнее использовать другие виды преобразования типов вместо @ptrCast.
test_pointer_casting.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "pointer casting" {
const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x12, 0x12, 0x12 };
const u32_ptr: *const u32 = @ptrCast(&bytes);
try expectEqual(0x12121212, u32_ptr.*);
// 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 expectEqual(0x12121212, u32_value);
// And even another way, the most straightforward way to do it:
try expectEqual(0x12121212, @as(u32, @bitCast(bytes)));
}
test "pointer child type" {
// pointer types have a `child` field which tells you the type they point to.
try expectEqual(u32, @typeInfo(*u32).pointer.child);
}
Командная оболочка
$ 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.
Предполагается, что чтение и запись не имеют побочных эффектов. Если определённая операция чтения или записи должна иметь побочные эффекты, например при вводе-выводе, отображённом на память (MMIO), используйте volatile. В следующем коде гарантируется выполнение всех операций чтения и записи с mmio_ptr в том же порядке, что и в исходном коде:
$ zig test test_volatile.zig
1/1 test_volatile.test.volatile...OK
All 1 tests passed.
Обратите внимание, что volatile не имеет отношения к параллелизму и атомарным операциям. Если вы видите код, в котором volatile используется не для отображения ввода-вывода в память, скорее всего, это ошибка.
Выравнивание
У каждого типа есть выравнивание — число байтов, на которое должен без остатка делиться адрес памяти при загрузке значения этого типа из памяти или его сохранении в памяти. Чтобы узнать это значение для любого типа, можно использовать @alignOf.
Выравнивание зависит от архитектуры процессора, но всегда является степенью двойки и меньше 1 << 29.
Для типов указателей можно явно указать выравнивание в байтах. Если оно не указано, предполагается, что выравнивание равно выравниванию базового типа.
test_variable_alignment.zig
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
test "variable alignment" {
var x: i32 = 1234;
try expectEqual(*i32, @TypeOf(&x));
try expect(@intFromPtr(&x) % @alignOf(i32) == 0);
// The implicitly-aligned pointer can be coerced to be explicitly-aligned to
// the alignment of the underlying type `i32`:
const ptr: *align(@alignOf(i32)) i32 = &x;
try expectEqual(1234, ptr.*);
}
Оболочка
$ 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 932714 panic: incorrect alignment
/home/andy/src/zig/doc/langref/test_incorrect_pointer_alignment.zig:10:68: 0x1235076 in foo (test_incorrect_pointer_alignment.zig)
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:48: 0x1234ede in test.pointer alignment safety (test_incorrect_pointer_alignment.zig)
try std.testing.expectEqual(0x11111111, foo(bytes));
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/7f8d19ec74bf7dbcd82b039049234c1a/test --seed=0x2210595e
allowzero
Этот атрибут указателя позволяет указателю иметь нулевой адрес. Это требуется только для целевой платформы без операционной системы, где нулевой адрес может быть отображён в память. Для представления нулевых указателей используйте вместо этого опциональные указатели. Опциональные указатели с allowzero имеют размер, отличный от размера указателей. В этом примере кода, если бы у указателя не было атрибута allowzero, возникла бы паника недопустимого нулевого значения при приведении указателя:
test_allowzero.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
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 expectEqual(0, @intFromPtr(ptr));
}
Оболочка
$ 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:34: note: parameter type declared here
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
^~~~~~~~~~~~~
referenced by:
callMain [inlined]: /home/andy/src/zig/lib/std/start.zig:685:59
callMainWithArgs [inlined]: /home/andy/src/zig/lib/std/start.zig:637:20
main: /home/andy/src/zig/lib/std/start.zig:662:28
1 reference(s) hidden; use '-freference-trace=4' to see all references
Срез — это указатель и длина. Массив отличается от среза тем, что длина массива является частью типа и известна во время компиляции, тогда как длина среза известна во время выполнения. Доступ к обоим можно получить через поле len.
test_basic_slices.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual([]i32, @TypeOf(slice));
try expectEqual(&array[0], &slice[0]);
try expectEqual(array.len, slice.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 expectEqual(*[array.len]i32, @TypeOf(array_ptr));
// 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 expectEqual(*[length]i32, @TypeOf(array_ptr_len));
// Using the address-of operator on a slice gives a single-item pointer.
try expectEqual(*i32, @TypeOf(&slice[0]));
// Using the `ptr` field gives a many-item pointer.
try expectEqual([*]i32, @TypeOf(slice.ptr));
try expectEqual(@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.
// Empty slices can be created like this:
const empty1 = &[0]u8{};
// If the type is known you can use this short hand:
const empty2: []u8 = &.{};
try expectEqual(0, empty1.len);
try expectEqual(0, empty2.len);
// A zero-length initialization can always be used to create an empty slice, even if the slice is mutable.
// This is because the pointed-to data is zero bits long, so its immutability is irrelevant.
}
Оболочка
$ zig test test_basic_slices.zig
1/1 test_basic_slices.test.basic slices...thread 927257 panic: index out of bounds: index 10, len 4
/home/andy/src/zig/doc/langref/test_basic_slices.zig:41:10: 0x1236eb4 in test.basic slices (test_basic_slices.zig)
slice[10] += 1;
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/5351e7c63c9dd9ef396b7b8b54eb8d1c/test --seed=0x92b0e709
Это одна из причин, по которой мы предпочитаем срезы указателям.
test_slices.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
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 expectEqualStrings("hello 世界", hello_world);
}
test "slice pointer" {
var array: [10]u8 = undefined;
const ptr = &array;
try expectEqual(*[10]u8, @TypeOf(ptr));
// 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 expectEqual([]u8, @TypeOf(slice));
slice[2] = 3;
try expectEqual(3, array[2]);
// Again, slicing with comptime-known indexes will produce another pointer
// to an array:
const ptr2 = slice[2..3];
try expectEqual(1, ptr2.len);
try expectEqual(3, ptr2[0]);
try expectEqual(*[1]u8, @TypeOf(ptr2));
}
Оболочка
$ 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], где data — указатель на множество элементов, массив или срез, а 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 expectEqual = std.testing.expectEqual;
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 929349 panic: sentinel mismatch: expected 0, found 1
/home/andy/src/zig/doc/langref/test_sentinel_mismatch.zig:13:24: 0x1234ecf in test.sentinel mismatch (test_sentinel_mismatch.zig)
const slice = array[0..runtime_length :0];
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/c6d75cc8280b6dc18f9d56dd44b174a2/test --seed=0x1972e607
// 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,
};
// Declare an instance of a struct.
const p: Point = .{
.x = 0.12,
.y = 0.34,
};
// Functions in the struct's namespace can be called 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;
}
};
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
try expectEqual(0.0, v1.dot(v2));
// 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 expectEqual(0.0, Vec3.dot(v1, v2));
}
// Structs can have declarations.
// Structs can have 0 fields.
const Empty = struct {
pub const PI = 3.14;
};
test "struct namespaced variable" {
try expectEqual(3.14, Empty.PI);
try expectEqual(0, @sizeOf(Empty));
// Empty structs can be instantiated the same as usual.
const does_nothing: Empty = .{};
_ = does_nothing;
}
// Struct field order is determined by the compiler, however, a base pointer
// can be computed from 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 expectEqual(0.9, point.y);
}
// Structs can be returned from functions.
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.
try expectEqual(LinkedList(i32), LinkedList(i32));
const list = LinkedList(i32){
.first = null,
.last = null,
.len = 0,
};
try expectEqual(0, list.len);
// Since types are first class values you can instantiate the type
// by assigning it to a variable:
const ListOfInts = LinkedList(i32);
try expectEqual(LinkedList(i32), ListOfInts);
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 expectEqual(1234, list2.first.?.data);
// instead of try expectEqual(1234, list2.first.?.*.data);
}
const expectEqual = @import("std").testing.expectEqual;
Для каждого поля структуры можно задать выражение, определяющее значение поля по умолчанию. Такие выражения выполняются во время компиляции и позволяют не указывать поле в литеральном выражении структуры:
Значения полей по умолчанию уместны только в том случае, если инварианты данных структуры не могут быть нарушены из-за пропуска этого поля при инициализации.
Например, ниже приведён неподходящий способ задать значение поля структуры по умолчанию:
$ zig build-exe bad_default_value.zig
$ ./bad_default_value
thread 932709 panic: reached unreachable code
/home/andy/src/zig/lib/std/debug.zig:421:14: 0x1025eb9 in assert (std.zig)
if (!ok) unreachable; // assertion failure
^
/home/andy/src/zig/doc/langref/bad_default_value.zig:8:15: 0x11d611a in categorize (bad_default_value.zig)
assert(t.maximum >= t.minimum);
^
/home/andy/src/zig/doc/langref/bad_default_value.zig:19:42: 0x11d4bce in main (bad_default_value.zig)
const category = threshold.categorize(0.90);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d51ac in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Выше видно, к чему приводит игнорирование этого принципа. Значения полей по умолчанию привели к нарушению инварианта данных, что вызвало недопустимое поведение.
Чтобы исправить это, удалите значения по умолчанию у всех полей структуры и задайте именованное значение по умолчанию:
Если для инициализации значения структуры без нарушения инвариантов данных требуется значение, известное во время выполнения, используйте метод инициализации, принимающий эти значения во время выполнения и заполняющий остальные поля.
extern struct
Структура externstruct имеет расположение в памяти, соответствующее ABI C для целевой платформы.
Если строго определённое расположение в памяти не требуется, лучше использовать struct, поскольку он накладывает меньше ограничений на компилятор.
См. packed struct — структуру, ABI которой соответствует базовому целому числу; она может быть полезна для моделирования флагов.
Структуры packed, как и enum, основаны на концепции различной интерпретации целых чисел. У всех упакованных структур есть базовое целое число, которое определяется неявно на основе общего числа битов полей или задаётся явно. Упакованные структуры имеют чётко определённое расположение в памяти — ABI у них в точности совпадает с ABI базового целого числа.
Каждое поле упакованной структуры интерпретируется как логическая последовательность битов, расположенных от младшего к старшему. Допустимые типы полей:
Поле типа целое число использует ровно столько битов, какова его разрядность. Например, для u5 будут использоваться 5 битов базового целого числа.
$ zig test test_packed_structs.zig
1/1 test_packed_structs.test.@bitCast between packed structs...OK
All 1 tests passed.
Базовое целое число может быть выведено или указано явно. Если оно выводится, оно будет беззнаковым. Если оно указано явно, во время компиляции будет проверено, что его разрядность в точности совпадает с общей разрядностью полей:
$ zig test test_missized_packed_struct.zig/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:22: error: backing integer bit width does not match total bit width of fields
const S = packed struct(u32) { a: u16, b: u8 };
~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:29: note: backing integer 'u32' has bit width '32'
const S = packed struct(u32) { a: u16, b: u8 };
^~~
/home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:2:22: note: struct fields have total bit width '24'
referenced by:
test.missized packed struct: /home/andy/src/zig/doc/langref/test_missized_packed_struct.zig:3:17
В 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.zig/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: error: expected type '*const u3', found '*align(1:3:1) u3'
try expectEqual(2, bar(&bit_field.b));
^~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer host size '1' cannot cast into pointer host size '0'
/home/andy/src/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
/home/andy/src/zig/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 expectEqual = std.testing.expectEqual;
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 = &ptr.b;
try expectEqual(2, ptr_to_b.*);
}
Оболочка
$ 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_packed_struct_equality.zig
1/1 test_packed_struct_equality.test.packed struct equality...OK
All 1 tests passed.
Доступ к полям и присваивание можно рассматривать как сокращённую запись для сдвигов битов базового целого числа. Эти операции не являются атомарными, поэтому будьте осторожны при использовании синтаксиса доступа к полям вместе с вводом-выводом, отображённым в память (MMIO). Вместо доступа к полям указателейvolatile сначала создайте полностью сформированное новое значение, а затем запишите его через указатель volatile.
packed_struct_mmio.zig
pub const GpioRegister = packed struct(u8) {
GPIO0: bool,
GPIO1: bool,
GPIO2: bool,
GPIO3: bool,
reserved: u4 = 0,
};
const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);
pub fn writeToGpio(new_states: GpioRegister) void {
// Example of what not to do:
// BAD! gpio.GPIO0 = true; BAD!
// Instead, do this:
gpio.* = new_states;
}
Именование структур
Поскольку все структуры являются анонимными, 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.
Кортежи
Анонимные структуры можно создавать без указания имён полей; такие структуры называются «кортежами». Пустой кортеж выглядит как .{}; он встречается в одном из примеров Hello World.
Полям неявно присваиваются имена — числа, начиная с 0. Поскольку их имена являются целыми числами, к ним нельзя обращаться с помощью синтаксиса ., не обернув их также в @"". Имена внутри @"" всегда распознаются как идентификаторы.
Как и массивы, кортежи имеют поле .len, могут индексироваться (если индекс известен во время компиляции) и поддерживают операторы ++ и **. Кроме того, по ним можно выполнять итерацию с помощью inline for.
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
const expectEqualStrings = @import("std").testing.expectEqualStrings;
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 expectEqual(0, @intFromEnum(Value.zero));
try expectEqual(1, @intFromEnum(Value.one));
try expectEqual(2, @intFromEnum(Value.two));
}
// 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 expectEqual(100, @intFromEnum(Value2.hundred));
try expectEqual(1000, @intFromEnum(Value2.thousand));
try expectEqual(1000000, @intFromEnum(Value2.million));
}
// 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 expectEqual(0, @intFromEnum(Value3.a));
try expectEqual(8, @intFromEnum(Value3.b));
try expectEqual(9, @intFromEnum(Value3.c));
try expectEqual(4, @intFromEnum(Value3.d));
try expectEqual(5, @intFromEnum(Value3.e));
}
// 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 expectEqualStrings(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 expectEqual(u2, @typeInfo(Small).@"enum".tag_type);
}
// @typeInfo tells us the field count and the fields names:
test "@typeInfo" {
try expectEqual(4, @typeInfo(Small).@"enum".fields.len);
try expectEqualStrings(@typeInfo(Small).@"enum".fields[1].name, "two");
}
// @tagName gives a [:0]const u8 representation of an enum value:
test "@tagName" {
try expectEqualStrings(@tagName(Small.three), "three");
}
По умолчанию перечисления не гарантированно совместимы с ABI C:
enum_export_error.zig
const Foo = enum { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
Оболочка
$ zig build-obj enum_export_error.zig -target x86_64-linux/home/andy/src/zig/doc/langref/enum_export_error.zig:2:17: error: parameter of type 'enum_export_error.Foo' not allowed in function with calling convention 'x86_64_sysv'
export fn entry(foo: Foo) void {
^~~~~~~~
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: integer tag type of enum is inferred
const Foo = enum { a, b, c };
^~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: consider explicitly specifying the integer tag type
/home/andy/src/zig/doc/langref/enum_export_error.zig:1:13: note: enum declared here
referenced by:
root: /home/andy/src/zig/lib/std/start.zig:13:22
comptime: /home/andy/src/zig/lib/std/start.zig:20:9
2 reference(s) hidden; use '-freference-trace=4' to see all references
Чтобы перечисление было совместимо с ABI C, укажите для него явный тип тега:
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 expectEqual = std.testing.expectEqual;
const Color = enum {
auto,
off,
on,
};
test "enum literals" {
const color1: Color = .auto;
const color2 = Color.auto;
try expectEqual(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.
Неполное перечисление
Неполное перечисление можно создать, добавив в конец поле _. Для перечисления необходимо указать тип тега, и оно не может включать все значения перечисления.
При использовании @enumFromInt с неполным перечислением применяются правила безопасности @intCast для приведения к целочисленному типу тега, но в остальном результатом всегда будет корректное значение перечисления.
В операторе switch для неполного перечисления можно использовать ветвь _ вместо ветви else. При наличии ветви _ компилятор выдаст ошибку, если в switch не обработаны все известные имена тегов.
$ 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 933934 panic: access of union field 'float' while field 'int' is active
/home/andy/src/zig/doc/langref/test_wrong_union_access.zig:8:12: 0x1234e62 in test.simple union (test_wrong_union_access.zig)
payload.float = 12.34;
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/a910feedacc78c90cf0c411c52cd47d5/test --seed=0x39660e11
Чтобы активировать другое поле, присвойте значение всему объединению:
Чтобы инициализировать объединение, если имя тега известно во время компиляции, см. @unionInit.
Объединение с тегом
Объединения можно объявить с типом тега-перечисления. Это превращает объединение в объединение с тегом, которое можно использовать в выражениях switch. При переключении по объединению с тегом значение тега можно получить с помощью дополнительного захвата. Объединения с тегом приводятся к своему типу тега: Приведение типов: объединения и перечисления.
test_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const ComplexTypeTag = enum {
ok,
not_ok,
};
const ComplexType = union(ComplexTypeTag) {
ok: u8,
not_ok: void,
};
test "switch on tagged union" {
const c = ComplexType{ .ok = 42 };
try expectEqual(ComplexTypeTag.ok, @as(ComplexTypeTag, c));
switch (c) {
.ok => |value| try expectEqual(42, value),
.not_ok => unreachable,
}
switch (c) {
.ok => |_, tag| {
// Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok':
comptime std.debug.assert(tag == .ok);
},
.not_ok => unreachable,
}
}
test "get tag type" {
try expectEqual(ComplexTypeTag, std.meta.Tag(ComplexType));
}
Оболочка
$ 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, поставьте * перед именем переменной, чтобы сделать её указателем:
$ zig test test_union_method.zig
1/1 test_union_method.test.union method...OK
All 1 tests passed.
Объединения с выведенными типами тегов-перечислений также могут присваивать выведенным тегам порядковые значения. Для этого тегу необходимо указать явный целочисленный тип. С помощью @intFromEnum можно получить порядковое значение, соответствующее активному полю.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Number = union {
int: i32,
float: f64,
};
test "anonymous union literal syntax" {
const i: Number = .{ .int = 42 };
const f = makeNumber();
try expectEqual(42, i.int);
try expectEqual(12.34, f.float);
}
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, который не раскрывает детали структуры. Пример:
Идентификаторам никогда не разрешается «скрывать» другие идентификаторы, используя то же имя:
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.zig/home/andy/src/zig/doc/langref/test_shadowing.zig:6:13: error: local variable shadows declaration of 'pi'
var pi: i32 = 1234;
^~
/home/andy/src/zig/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.
Пустые блоки
Пустой блок эквивалентен void{}:
test_empty_block.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test {
const a = {};
const b = void{};
try expectEqual(void, @TypeOf(a));
try expectEqual(void, @TypeOf(b));
try expectEqual(a, b);
}
Оболочка
$ 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 expectEqual = std.testing.expectEqual;
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 expectEqual(1, b);
}
// 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 expectEqual = @import("std").testing.expectEqual;
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 expectEqual(6, b);
try expectEqual(2, a.c.x);
}
Оболочка
$ zig test test_switch_tagged_union.zig
1/1 test_switch_tagged_union.test.switch on tagged union...OK
All 1 tests passed.
const std = @import("std");
const expect = std.testing.expect;
const Color = enum {
auto,
off,
on,
};
test "enum literals with switch" {
const color = Color.off;
const result = switch (color) {
.auto => false,
.on => false,
.off => true,
};
try expect(result);
}
Оболочка
$ zig test test_exhaustive_switch.zig
1/1 test_exhaustive_switch.test.enum literals with switch...OK
All 1 tests passed.
Переключение по ошибкам
При переключении по ошибкам допускаются некоторые особые случаи, упрощающие шаблоны обобщённого программирования:
test_switch_on_errors.zig
const FileOpenError0 = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
fn openFile0() FileOpenError0 {
return error.OutOfMemory;
}
test "unreachable else prong" {
switch (openFile0()) {
error.AccessDenied, error.FileNotFound => |e| return e,
error.OutOfMemory => {},
// 'openFile0' cannot return any more errors, so an 'else' prong would be
// statically known to be unreachable. Nonetheless, in this case, adding
// one does not raise an "unreachable else prong" compile error:
else => unreachable,
}
// Allowed unreachable else prongs are:
// `else => unreachable,`
// `else => return,`
// `else => |e| return e,` (where `e` is any identifier)
}
const FileOpenError1 = error{
AccessDenied,
SystemResources,
FileNotFound,
};
fn openFile1() FileOpenError1 {
return error.SystemResources;
}
fn openFileGeneric(comptime kind: u1) switch (kind) {
0 => FileOpenError0,
1 => FileOpenError1,
} {
return switch (kind) {
0 => openFile0(),
1 => openFile1(),
};
}
test "comptime unreachable errors not in error set" {
switch (openFileGeneric(1)) {
error.AccessDenied, error.FileNotFound => |e| return e,
error.OutOfMemory => comptime unreachable, // not in `FileOpenError1`!
error.SystemResources => {},
}
}
Оболочка
$ zig test test_switch_on_errors.zig
1/2 test_switch_on_errors.test.unreachable else prong...OK
2/2 test_switch_on_errors.test.comptime unreachable errors not in error set...OK
All 2 tests passed.
switch с меткой
Если оператору switch присвоена метка, на него можно ссылаться из break или continue. break вернёт значение из switch.
Оператор continue, ссылающийся на switch, должен иметь операнд. При выполнении он перейдёт к соответствующей ветви, как если бы switch был выполнен повторно, а continue заменил начальное значение switch.
test_switch_continue.zig
const std = @import("std");
test "switch continue" {
sw: switch (@as(i32, 5)) {
5 => continue :sw 4,
// `continue` can occur multiple times within a single switch prong.
2...4 => |v| {
if (v > 3) {
continue :sw 2;
} else if (v == 3) {
// `break` can target labeled loops.
break :sw;
}
continue :sw 1;
},
1 => return,
else => unreachable,
}
}
Оболочка
$ zig test test_switch_continue.zig
1/1 test_switch_continue.test.switch continue...OK
All 1 tests passed.
Семантически это эквивалентно следующему циклу:
test_switch_continue_equivalent.zig
const std = @import("std");
test "switch continue, equivalent loop" {
var sw: i32 = 5;
while (true) {
switch (sw) {
5 => {
sw = 4;
continue;
},
2...4 => |v| {
if (v > 3) {
sw = 2;
continue;
} else if (v == 3) {
break;
}
sw = 1;
continue;
},
1 => return,
else => unreachable,
}
}
}
Оболочка
$ zig test test_switch_continue_equivalent.zig
1/1 test_switch_continue_equivalent.test.switch continue, equivalent loop...OK
All 1 tests passed.
Это может повысить ясность, например, конечных автоматов, где синтаксис continue :sw .next_state однозначен, явен и сразу понятен.
Однако исходный пример — это переключение по каждому элементу массива; использование одного оператора switch может повысить ясность и производительность:
test_switch_dispatch_loop.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Instruction = enum {
add,
mul,
end,
};
fn evaluate(initial_stack: []const i32, code: []const Instruction) !i32 {
var buffer: [8]i32 = undefined;
var stack = std.ArrayList(i32).initBuffer(&buffer);
try stack.appendSliceBounded(initial_stack);
var ip: usize = 0;
return vm: switch (code[ip]) {
// Because all code after `continue` is unreachable, this branch does
// not provide a result.
.add => {
try stack.appendBounded(stack.pop().? + stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.mul => {
try stack.appendBounded(stack.pop().? * stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.end => stack.pop().?,
};
}
test "evaluate" {
const result = try evaluate(&.{ 7, 2, -3 }, &.{ .mul, .add, .end });
try expectEqual(1, result);
}
Оболочка
$ zig test test_switch_dispatch_loop.zig
1/1 test_switch_dispatch_loop.test.evaluate...OK
All 1 tests passed.
Если операнд для continue известен на этапе comptime, его можно преобразовать в безусловный переход к соответствующему варианту. Такой переход предсказывается абсолютно точно и поэтому обычно выполняется очень быстро.
Если операнд известен только во время выполнения, каждый continue может содержать встроенный условный переход (в идеале — через таблицу переходов), что позволяет процессору независимо предсказывать цель каждого варианта. При преобразовании в цикл все переходы проходили бы через одну точку диспетчеризации, что затруднило бы предсказание переходов.
Встроенные варианты switch
Варианты switch можно пометить как inline, чтобы сгенерировать тело варианта для каждого возможного значения, которое оно может принимать. Захваченное значение при этом будет известно на этапе comptime.
test_inline_switch.zig
const std = @import("std");
const expect = std.testing.expect;
const expectError = std.testing.expectError;
fn isFieldOptional(comptime T: type, field_index: usize) !bool {
const fields = @typeInfo(T).@"struct".fields;
return switch (field_index) {
// This prong is analyzed twice with `idx` being a
// comptime-known value each time.
inline 0, 1 => |idx| @typeInfo(fields[idx].type) == .optional,
else => return error.IndexOutOfBounds,
};
}
const Struct1 = struct { a: u32, b: ?u32 };
test "using @typeInfo with runtime values" {
var index: usize = 0;
try expect(!try isFieldOptional(Struct1, index));
index += 1;
try expect(try isFieldOptional(Struct1, index));
index += 1;
try expectError(error.IndexOutOfBounds, isFieldOptional(Struct1, index));
}
// Calls to `isFieldOptional` on `Struct1` get unrolled to an equivalent
// of this function:
fn isFieldOptionalUnrolled(field_index: usize) !bool {
return switch (field_index) {
0 => false,
1 => true,
else => return error.IndexOutOfBounds,
};
}
Оболочка
$ zig test test_inline_switch.zig
1/1 test_inline_switch.test.using @typeInfo with runtime values...OK
All 1 tests passed.
Ключевое слово inline также можно использовать с диапазонами:
Варианты inlineelse можно использовать как типобезопасную альтернативу циклам inlinefor:
test_inline_else.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
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 expectEqual(5, withFor(any));
try expectEqual(5, withSwitch(any));
}
Оболочка
$ zig test test_inline_else.zig
1/1 test_inline_else.test.inline for and inline else similarity...OK
All 1 tests passed.
При использовании встроенного варианта для переключения по объединению можно дополнительно захватить значение тега перечисления объединения на этапе comptime, даже если его полезная нагрузка может быть известна только во время выполнения.
test_inline_switch_union_tag.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
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 expectEqual(42, getNum(u));
}
Оболочка
$ 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 expectEqual = @import("std").testing.expectEqual;
test "while basic" {
var i: usize = 0;
while (i < 10) {
i += 1;
}
try expectEqual(10, i);
}
Оболочка
$ zig test test_while.zig
1/1 test_while.test.while basic...OK
All 1 tests passed.
Используйте break, чтобы досрочно выйти из цикла while.
test_while_break.zig
const expectEqual = @import("std").testing.expectEqual;
test "while break" {
var i: usize = 0;
while (true) {
if (i == 10)
break;
i += 1;
}
try expectEqual(10, i);
}
Оболочка
$ 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 expectEqual = @import("std").testing.expectEqual;
test "while continue" {
var i: usize = 0;
while (true) {
i += 1;
if (i < 10)
continue;
break;
}
try expectEqual(10, i);
}
Оболочка
$ zig test test_while_continue.zig
1/1 test_while_continue.test.while continue...OK
All 1 tests passed.
Циклы while поддерживают выражение продолжения, которое выполняется при переходе к следующей итерации цикла. Ключевое слово continue учитывает это выражение.
test_while_continue_expression.zig
const expectEqual = @import("std").testing.expectEqual;
const expect = @import("std").testing.expect;
test "while loop continue expression" {
var i: usize = 0;
while (i < 10) : (i += 1) {}
try expectEqual(10, i);
}
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. При выходе из цикла while с помощью break ветвь 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 с optional-значениями
Подобно выражениям if, циклы while могут принимать optional-значение в качестве условия и захватывать полезную нагрузку. При встрече значения null цикл завершается.
Если в выражении while присутствует синтаксис |x|, условие while должно иметь optional-тип.
При итерации по optional-значению допускается ветвь else. В этом случае она выполнится при первом встреченном значении null.
test_while_null_capture.zig
const expectEqual = @import("std").testing.expectEqual;
test "while null capture" {
var sum1: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum1 += value;
}
try expectEqual(3, sum1);
// null capture with an else block
var sum2: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum2 += value;
} else {
try expectEqual(3, sum2);
}
// 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 expectEqual(3, i);
}
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 и цикл завершается.
Если в выражении while присутствует синтаксис else |x|, условие while должно иметь тип объединения ошибок.
test_while_error_capture.zig
const expectEqual = @import("std").testing.expectEqual;
test "while error union capture" {
var sum1: u32 = 0;
numbers_left = 3;
while (eventuallyErrorSequence()) |value| {
sum1 += value;
} else |err| {
try expectEqual(error.ReachedZero, err);
}
}
var numbers_left: u32 = undefined;
fn eventuallyErrorSequence() anyerror!u32 {
return if (numbers_left == 0) error.ReachedZero else blk: {
numbers_left -= 1;
break :blk numbers_left;
};
}
Оболочка
$ zig test test_while_error_capture.zig
1/1 test_while_error_capture.test.while error union capture...OK
All 1 tests passed.
inline while
Циклы while можно встраивать. В этом случае цикл разворачивается, что позволяет коду выполнять действия, возможные только на этапе компиляции, например использовать типы как значения первого класса.
test_inline_while.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(9, sum);
}
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.
Рекомендуется использовать циклы inline только по одной из следующих причин:
Для корректной работы семантики цикл должен выполняться на этапе comptime.
У вас есть результаты тестирования производительности, доказывающие, что принудительное разворачивание цикла таким способом заметно ускоряет его выполнение.
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(16, sum);
// To iterate over a portion of a slice, reslice.
for (items[0..1]) |value| {
sum += value;
}
try expectEqual(20, sum);
// 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 expectEqual(usize, @TypeOf(i));
sum2 += @as(i32, @intCast(i));
}
try expectEqual(10, sum2);
// 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 expectEqual(10, sum3);
}
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 expectEqual(21, count);
}
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 expectEqual(4, items[0]);
try expectEqual(5, items[1]);
try expectEqual(3, items[2]);
}
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 expectEqual(12, sum);
break :blk sum;
};
try expectEqual(12, result);
}
Если циклу for присвоено имя, на него можно ссылаться из break или continue во вложенном цикле:
test_for_nested_break.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "nested break" {
var count: usize = 0;
outer: for (1..6) |_| {
for (1..6) |_| {
count += 1;
break :outer;
}
}
try expectEqual(1, count);
}
test "nested continue" {
var count: usize = 0;
outer: for (1..9) |_| {
for (1..6) |_| {
count += 1;
continue :outer;
}
}
try expectEqual(8, count);
}
Оболочка
$ 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.
inline for
Циклы for можно встраивать. В этом случае цикл разворачивается, что позволяет коду выполнять действия, возможные только на этапе компиляции, например использовать типы как значения первого класса. Значения захвата и итератора во встроенных циклах for известны во время компиляции.
$ zig test test_inline_for.zig
1/1 test_inline_for.test.inline for loop...OK
All 1 tests passed.
Рекомендуется использовать циклы inline только по одной из следующих причин:
Для корректной работы семантики цикл должен выполняться на этапе comptime.
У вас есть результаты тестирования производительности, доказывающие, что принудительное разворачивание цикла таким способом заметно ускоряет его выполнение.
// If expressions have three uses, corresponding to the three types:
// * bool
// * ?T
// * anyerror!T
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(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 expectEqual(value, 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!u32 = error.BadValue;
if (b) |value| {
_ = value;
unreachable;
} else |err| {
try expectEqual(err, error.BadValue);
}
// The else and |err| capture is strictly required.
if (a) |value| {
try expectEqual(value, 0);
} else |_| {}
// To check only the error value, use an empty block expression.
if (b) |_| {} else |err| {
try expectEqual(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 expectEqual(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 с optional-значениями
test_if_optionals.zig
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
test "if optional" {
// If expressions test for null.
const a: ?u32 = 0;
if (a) |value| {
try expectEqual(0, value);
} else {
unreachable;
}
const b: ?u32 = null;
if (b) |_| {
unreachable;
} else {
try expect(true);
}
// The else is not required.
if (a) |value| {
try expectEqual(0, value);
}
// 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 expectEqual(2, value);
} 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 expectEqual(0, optional_value.?);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!?u32 = null;
if (b) |optional_value| {
try expectEqual(null, optional_value);
} else |_| {
unreachable;
}
const c: anyerror!?u32 = error.BadValue;
if (c) |optional_value| {
_ = optional_value;
unreachable;
} else |err| {
try expectEqual(error.BadValue, err);
}
// 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 expectEqual(9, optional_value.?);
} 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 932713 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_assertion_failure.zig:3:14: 0x1234e59 in assert (test_assertion_failure.zig)
if (!ok) unreachable; // assertion failure
^
/home/andy/src/zig/doc/langref/test_assertion_failure.zig:8:11: 0x1234e2e in test.this will fail (test_assertion_failure.zig)
assert(false);
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/1209ea68e9a74969bb29b43362f9d1da/test --seed=0x3aa77c99
На этапе компиляции
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.zig/home/andy/src/zig/doc/langref/test_comptime_unreachable.zig:10:16: error: unreachable code
assert(@TypeOf(unreachable) == noreturn);
^~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/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 expectEqual = std.testing.expectEqual;
// 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.
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;
// The @branchHint builtin can be used to tell the optimizer that a function is rarely called ("cold").
fn abort() noreturn {
@branchHint(.cold);
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 expectEqual(11, doOp(add, 5, 6));
try expectEqual(-1, doOp(sub2, 5, 6));
}
Оболочка
$ zig test test_functions.zig
1/1 test_functions.test.function...OK
All 1 tests passed.
Существует различие между телом функции и указателем на функцию. Тела функций имеют типы, доступные только на этапе comptime, тогда как значения указателей на функции могут быть известны во время выполнения.
Параметры, передаваемые по значению
Примитивные типы, например целые числа и числа с плавающей точкой, передаваемые в качестве параметров, копируются, после чего копия становится доступной в теле функции. Это называется «передачей по значению». Копирование примитивного типа практически ничего не стоит и обычно сводится к установке значения регистра.
Структуры, объединения и массивы иногда эффективнее передавать по ссылке, поскольку стоимость копирования может быть очень высокой в зависимости от размера. При передаче таких типов в качестве параметров Zig может выбрать копирование и передачу по значению либо передачу по ссылке — в зависимости от того, какой способ, по мнению 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 expectEqual = @import("std").testing.expectEqual;
test "pass struct to function" {
try expectEqual(3, foo(Point{ .x = 1, .y = 2 }));
}
Оболочка
$ 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
const std = @import("std");
pub fn main() void {
if (foo(1200, 34) != 1234) {
@compileError("bad");
}
}
inline fn foo(a: i32, b: i32) i32 {
std.debug.print("runtime a = {} b = {}", .{ a, b });
return a + b;
}
Оболочка
$ zig build-exe inline_call.zig
$ ./inline_call
runtime a = 1200 b = 34
Если удалить inline, тест завершится ошибкой компиляции вместо успешного выполнения.
Как правило, лучше позволить компилятору самостоятельно решать, когда встраивать функцию, за исключением следующих случаев:
Чтобы изменить количество кадров стека вызовов в целях отладки.
Чтобы обеспечить распространение comptime-свойств аргументов на возвращаемое значение функции, как в примере выше.
Этого требуют реальные измерения производительности.
Обратите внимание, что inline фактически ограничивает возможности компилятора. Это может отрицательно сказаться на размере двоичного файла, скорости компиляции и даже производительности во время выполнения.
$ zig test test_fn_reflection.zig
1/1 test_fn_reflection.test.fn reflection...OK
All 1 tests passed.
Ошибки
Тип множества ошибок
Множество ошибок похоже на перечисление. Однако каждой ошибке с уникальным именем во всей компиляции присваивается беззнаковое целое число больше 0. Одно и то же имя ошибки можно объявлять несколько раз — в этом случае ему будет присвоено одно и то же целое значение.
По умолчанию тип множества ошибок — u16, однако если максимальное число различных значений ошибок указано с помощью параметра командной строки --error-limit [num], будет использоваться целочисленный тип с минимальным количеством битов, необходимым для представления всех значений ошибок.
Ошибку из подмножества можно привести к надмножеству:
$ zig test test_coerce_error_superset_to_subset.zig/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: error: expected type 'error{OutOfMemory}', found 'error{AccessDenied,FileNotFound,OutOfMemory}'
return err;
^~~
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.AccessDenied' not a member of destination error set
/home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.FileNotFound' not a member of destination error set
/home/andy/src/zig/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: /home/andy/src/zig/doc/langref/test_coerce_error_superset_to_subset.zig:12:8
Для объявления множества ошибок с единственным значением и получения этого значения предусмотрено сокращённое обозначение:
anyerror обозначает глобальное множество ошибок. Оно содержит все ошибки во всей единице компиляции, то есть является объединением всех остальных множеств ошибок.
Любое множество ошибок можно привести к глобальному, а ошибку из глобального множества можно явно преобразовать в неглобальное. При этом вставляется языковая проверка assert, гарантирующая, что значение ошибки действительно входит в целевое множество ошибок.
Как правило, глобального множества ошибок следует избегать, поскольку оно не позволяет компилятору знать во время компиляции, какие ошибки возможны. Знание множества ошибок на этапе компиляции полезно для автоматически генерируемой документации и информативных сообщений об ошибках, например при пропуске возможного значения ошибки в 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.expectEqual(1234, result);
}
Оболочка
$ 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
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 по умолчанию приводит к аварийному завершению при срабатывании проверки безопасности. Таким образом, если во время отладки приложения здесь неожиданно возникнет ошибка, приложение завершится сбоем, как и должно.
Возможно, вы захотите выполнять разные действия в разных ситуациях. Для этого можно объединить выражения 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 может дополнительно захватывать ошибку:
$ zig test test_errdefer_capture.zig
1/1 test_errdefer_capture.test.errdefer capture...OK
All 1 tests passed.
Ещё несколько замечаний об обработке ошибок:
Эти средства достаточно выразительны, чтобы отсутствие проверки ошибки приводило к ошибке компиляции. Если вы действительно хотите проигнорировать ошибку, можно добавить catchunreachable и получить дополнительную гарантию: приложение аварийно завершится в режимах Debug и ReleaseSafe, если ваше предположение окажется неверным.
Поскольку Zig понимает типы ошибок, он может отдавать предпочтение ветвям, в которых ошибка не возникает. Это небольшое преимущество в производительности, недоступное в других языках.
Объединение ошибок создаётся с помощью бинарного оператора !. Чтобы получить дочерний тип объединения ошибок, можно использовать рефлексию на этапе компиляции:
test_error_union.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(i32, @typeInfo(@TypeOf(foo)).error_union.payload);
// Use compile-time reflection to access the error set type of an error union:
try comptime expectEqual(anyerror, @typeInfo(@TypeOf(foo)).error_union.error_set);
}
Оболочка
$ 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 => {},
}
}
Оболочка
$ 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
}
}
Оболочка
$ 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: 0x11d4b9c in bang1 (error_return_trace.zig)
return error.FileNotFound;
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:22:5: 0x11d4bfc in baz (error_return_trace.zig)
try bang1();
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:38:5: 0x11d4c3c in bang2 (error_return_trace.zig)
return error.PermissionDenied;
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:30:5: 0x11d4d1c in hello (error_return_trace.zig)
try bang2();
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:17:31: 0x11d4e2a in bar (error_return_trace.zig)
error.FileNotFound => try hello(),
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:7:9: 0x11d4f0a in foo (error_return_trace.zig)
try bar();
^
/home/andy/src/zig/doc/langref/error_return_trace.zig:2:5: 0x11d5011 in main (error_return_trace.zig)
try foo(12);
^
Внимательно рассмотрите этот пример. Это не трассировка стека.
Видно, что в итоге была возвращена ошибка PermissionDenied, но исходной ошибкой, с которой всё началось, была FileNotFound. В функции bar код обрабатывает исходный код ошибки, а затем возвращает другую ошибку из оператора switch. Трассировки возврата ошибок наглядно показывают это, тогда как трассировка стека выглядела бы так:
$ zig build-exe stack_trace.zig
$ ./stack_trace
thread 930211 panic: PermissionDenied
/home/andy/src/zig/doc/langref/stack_trace.zig:38:5: 0x11d5e7a in bang2 (stack_trace.zig)
@panic("PermissionDenied");
^
/home/andy/src/zig/doc/langref/stack_trace.zig:30:10: 0x11d5efc in hello (stack_trace.zig)
bang2();
^
/home/andy/src/zig/doc/langref/stack_trace.zig:17:14: 0x11d5eb3 in bar (stack_trace.zig)
hello();
^
/home/andy/src/zig/doc/langref/stack_trace.zig:7:12: 0x11d5898 in foo (stack_trace.zig)
bar();
^
/home/andy/src/zig/doc/langref/stack_trace.zig:2:8: 0x11d5801 in main (stack_trace.zig)
foo(12);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Здесь трассировка стека не объясняет, как управление в bar дошло до вызова hello(). Чтобы это выяснить, пришлось бы открыть отладчик или дополнительно инструментировать приложение. Трассировка возврата ошибки, напротив, точно показывает, как ошибка распространялась вверх по стеку вызовов.
Эта возможность отладки упрощает быструю доработку кода, надёжно обрабатывающего все возможные ошибки. В результате разработчики на Zig, стремясь повысить скорость разработки, естественным образом пишут корректный и надёжный код.
Есть несколько способов активировать трассировку возврата ошибок:
Вернуть ошибку из main
Ошибка доходит до catchunreachable, а обработчик паники по умолчанию не переопределён
Использовать errorReturnTrace для доступа к текущей трассировке возврата. Чтобы вывести её, можно использовать std.debug.dumpStackTrace. При сборке без поддержки трассировки возврата ошибок эта функция возвращает значение null, известное на этапе comptime.
Для анализа затрат на производительность рассмотрим два случая:
ошибки не возвращаются
возвращаются ошибки
Если ошибки не возвращаются, затраты сводятся к одной операции записи в память, выполняемой только в первой не допускающей ошибок функции графа вызовов, которая вызывает функцию, допускающую ошибки, то есть когда функция, возвращающая 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;
}
Затраты составляют две математические операции и несколько чтений и записей в память. Доступ к памяти ограничен и должен оставаться в кэше на протяжении всего распространения ошибки вверх по цепочке вызовов.
Что касается размера кода, один вызов функции перед оператором возврата — несущественная затрата. Тем не менее у меня есть план сделать вызов __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 expectEqual = @import("std").testing.expectEqual;
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 expectEqual(i32, @typeInfo(@TypeOf(foo)).optional.child);
}
Оболочка
$ zig test test_optional_type.zig
1/1 test_optional_type.test.optional type...OK
All 1 tests passed.
null
Как и undefined, null имеет собственный тип, и использовать его можно только после приведения к другому типу:
null.zig
const optional_value: ?i32 = null;
Опциональные указатели
Размер опционального указателя гарантированно совпадает с размером указателя. null опционального указателя гарантированно равен адресу 0.
test_optional_pointer.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(1, ptr.?.*);
// Optional pointers are the same size as normal pointers, because pointer
// value 0 is used as the null value.
try expectEqual(@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.
Целые числа неявно преобразуются в целочисленные типы, способные представить любое значение исходного типа; аналогично, числа с плавающей точкой преобразуются в типы, способные представить любое значение исходного типа.
Целые числа неявно преобразуются в числа с плавающей точкой, если любое возможное целое значение можно представить без округления (то есть точность целого числа не превышает точность мантиссы числа с плавающей точкой). Целочисленные типы большего размера, которые нельзя безопасно преобразовать неявно, нужно преобразовывать явно с помощью @floatFromInt.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectEqualSlices = std.testing.expectEqualSlices;
// 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 expectEqualStrings(x1, x2);
const y: []const f32 = &[2]f32{ 1.2, 3.4 };
try expectEqual(1.2, y[0]);
}
// 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 expectEqualStrings(try x1, try x2);
const y: anyerror![]const f32 = &[2]f32{ 1.2, 3.4 };
try expectEqual(1.2, (try y)[0]);
}
// 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 expectEqualStrings(x1.?, x2.?);
const y: ?[]const f32 = &[2]f32{ 1.2, 3.4 };
try expectEqual(1.2, y.?[0]);
}
// 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 expectEqualStrings("hello", x);
const buf2 = [2]f32{ 1.2, 3.4 };
const x2: []const f32 = &buf2;
try expectEqualSlices(f32, &[2]f32{ 1.2, 3.4 }, x2);
}
// 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 expectEqual('o', x[4]);
// 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 expectEqual('o', x.?[4]);
}
// 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 expectEqual(1234, z[0]);
}
// Sentinel-terminated slices can be coerced into sentinel-terminated pointers
test "[:x]T to [*:x]T" {
const buf: [:0]const u8 = "hello";
const buf2: [*:0]const u8 = buf;
try expectEqual('o', buf2[4]);
}
Оболочка
$ zig test test_coerce_slices_arrays_and_pointers.zig
1/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to []const T...OK
2/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to E![]const T...OK
3/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to ?[]const T...OK
4/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to []T...OK
5/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to [*]T...OK
6/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to ?[*]T...OK
7/8 test_coerce_slices_arrays_and_pointers.test.*T to *[1]T...OK
8/8 test_coerce_slices_arrays_and_pointers.test.[:x]T to [*:x]T...OK
All 8 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 expectEqual = std.testing.expectEqual;
test "coercing large integer type to smaller one when value is comptime-known to fit" {
const x: u64 = 255;
const y: u8 = x;
try expectEqual(255, y);
}
Оболочка
$ 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:
test_coerce_unions_enums.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const E = enum {
one,
two,
three,
};
const U = union(E) {
one: i32,
two: f32,
three,
};
const U2 = union(enum) {
a: void,
b: f32,
fn tag(self: U2) usize {
switch (self) {
.a => return 1,
.b => return 2,
}
}
};
test "coercion between unions and enums" {
const u = U{ .two = 12.34 };
const e: E = u; // coerce union to enum
try expectEqual(E.two, e);
const three = E.three;
const u_2: U = three; // coerce enum to union
try expectEqual(E.three, u_2);
const u_3: U = .three; // coerce enum literal to union
try expectEqual(E.three, u_3);
const u_4: U2 = .a; // coerce enum literal to union with inferred enum tag type.
try expectEqual(1, u_4.tag());
// The following example is invalid.
// error: coercion from enum '@EnumLiteral()' to union 'test_coerce_unions_enum.U2' must initialize 'f32' field 'b'
//var u_5: U2 = .b;
//try expectEqual(2, u_5.tag());
}
Оболочка
$ zig test test_coerce_unions_enums.zig
1/1 test_coerce_unions_enums.test.coercion between unions and enums...OK
All 1 tests passed.
$ zig test test_coerce_tuples_arrays.zig
1/1 test_coerce_tuples_arrays.test.coercion from homogeneous tuple to array...OK
All 1 tests passed.
Явное приведение типов
Явное приведение типов выполняется с помощью встроенных функций. Некоторые явные приведения безопасны, некоторые — нет. Некоторые выполняют проверки на уровне языка, другие — нет. Некоторые ничего не делают во время выполнения, другие — нет.
@bitCast — изменить тип, сохранив битовое представление
При таком определении типа выбирается тип, в который можно преобразовать все совместимые типы. Вот несколько примеров:
test_peer_type_resolution.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
test "peer resolve int widening" {
const a: i8 = 12;
const b: i16 = 34;
const c = a + b;
try expectEqual(46, c);
try expectEqual(i16, @TypeOf(c));
}
test "peer resolve small int and float" {
// This only works for integer types that can coerce to the float type.
// Larger integer types will cause a compiler error; no float widening occurs.
var i: u8 = 12;
var f: f32 = 34;
_ = .{ &i, &f };
const x = i + f;
try expectEqual(x, 46.0);
try expectEqual(@TypeOf(x), f32);
}
test "peer resolve arrays of different size to const slice" {
try expectEqualStrings("true", boolToStr(true));
try expectEqualStrings("false", boolToStr(false));
try comptime expectEqualStrings("true", boolToStr(true));
try comptime expectEqualStrings("false", boolToStr(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 expectEqualStrings("aoeu", value1);
try expectEqualStrings("zz", value2);
}
test "peer type resolution: ?T and T" {
try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
comptime {
try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
}
}
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 expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
comptime {
try expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
}
}
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 expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
}
comptime {
var data = "hi".*;
const slice = data[0..];
try expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
}
}
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 expectEqual(a, b);
try expectEqual(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 expectEqual(?u32, @TypeOf(b));
// 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 expectEqual(?u32, @TypeOf(c));
}
Оболочка
$ zig test test_peer_type_resolution.zig
1/9 test_peer_type_resolution.test.peer resolve int widening...OK
2/9 test_peer_type_resolution.test.peer resolve small int and float...OK
3/9 test_peer_type_resolution.test.peer resolve arrays of different size to const slice...OK
4/9 test_peer_type_resolution.test.peer resolve array and const slice...OK
5/9 test_peer_type_resolution.test.peer type resolution: ?T and T...OK
6/9 test_peer_type_resolution.test.peer type resolution: *[0]u8 and []const u8...OK
7/9 test_peer_type_resolution.test.peer type resolution: *[0]u8, []const u8, and anyerror![]u8...OK
8/9 test_peer_type_resolution.test.peer type resolution: *const T and ?*T...OK
9/9 test_peer_type_resolution.test.peer type resolution: error union switch...OK
All 9 tests passed.
Структура, все поля которой имеют тип нулевого размера.
Объединение только с одним полем, имеющим тип нулевого размера.
У таких типов может быть только одно возможное значение, поэтому для его представления требуется 0 бит. Код, использующий такие типы, не включается в итоговый сгенерированный код:
zero_bit_types.zig
export fn entry() void {
var x: void = {};
var y: void = {};
x = y;
y = x;
}
При преобразовании в машинный код тело entry не содержит сгенерированного кода даже в режиме отладки. Например, для 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.zig/home/andy/src/zig/doc/langref/test_expression_ignored.zig:2:8: error: value of type 'i32' ignored
foo();
~~~^~
/home/andy/src/zig/doc/langref/test_expression_ignored.zig:2:8: note: all non-void values must be used
/home/andy/src/zig/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 имеет тип @FieldType(T, "0")
.{ .a = x }
T
x имеет тип @FieldType(T, "a")
T{x}
-
x имеет тип @FieldType(T, "0")
T{ .a = x }
-
x имеет тип @FieldType(T, "a")
@Int(x, y)
-
x имеет тип std.builtin.Signedness, а y имеет тип u16
@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 expectEqual = @import("std").testing.expectEqual;
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 expectEqual(2, arr[0]); // succeeds
try expectEqual(1, arr[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...expected 1, found 2
FAIL (TestExpectedEqual)
/home/andy/src/zig/lib/std/testing.zig:118:17: 0x1234e54 in expectEqualInner__anon_42791 (std.zig)
return error.TestExpectedEqual;
^
/home/andy/src/zig/doc/langref/result_location_interfering_with_swap.zig:10:5: 0x1234fc5 in test.attempt to swap array elements with array initializer (result_location_interfering_with_swap.zig)
try expectEqual(1, arr[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/2726b856c754d9fa245ea804407f8c1d/test --seed=0x22805754
В таблице ниже показано, как некоторые распространённые выражения распространяют расположения результата; 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 не имеет расположения результата (типизированные инициализаторы не распространяют расположения результата)
@Int(x, y)
-
x и y не имеют расположений результата
@typeInfo(x)
ptr
x не имеет расположения результата
x << y
ptr
x и y не имеют расположений результата
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.zig/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:8:28: error: unable to resolve comptime value
const result = max(if (condition) f32 else u64, 1234, 5678);
^~~~~~~~~
/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:8:24: note: argument to comptime parameter must be comptime-known
const result = max(if (condition) f32 else u64, 1234, 5678);
^~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:1:8: note: parameter declared comptime here
fn max(comptime T: type, a: T, b: T) T {
^~~~~~~~
referenced by:
test.try to pass a runtime type: /home/andy/src/zig/doc/langref/test_unresolved_comptime_value.zig:5:8
Это ошибка, поскольку программист попытался передать функции, ожидающей значение, известное на этапе компиляции, значение, известное только во время выполнения.
Ошибка также может возникнуть, если мы передадим тип, нарушающий работу анализатора типов при анализе функции. Именно это означает утиную типизацию на этапе компиляции.
Например:
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.zig/home/andy/src/zig/doc/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: /home/andy/src/zig/doc/langref/test_comptime_mismatched_type.zig:5:12
С другой стороны, внутри определения функции с параметром 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.expectEqual(true, max(bool, false, 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 expectEqual = @import("std").testing.expectEqual;
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 expectEqual(6, performFn('t', 1));
try expectEqual(1, performFn('o', 0));
try expectEqual(99, performFn('w', 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.zig/home/andy/src/zig/doc/langref/test_comptime_call_extern_function.zig:5:13: error: comptime call of extern function
exit();
~~~~^~
/home/andy/src/zig/doc/langref/test_comptime_call_extern_function.zig:4:5: note: 'comptime' keyword forces comptime evaluation
comptime {
^~~~~~~~
Нет смысла в том, чтобы программа могла вызывать exit() (или любую другую внешнюю функцию) на этапе компиляции, поэтому это приводит к ошибке компиляции. Однако выражение comptime делает гораздо больше, чем просто иногда вызывает ошибку компиляции.
Внутри выражения comptime:
Все переменные являются переменными comptime.
Все выражения if, while, for и switch вычисляются на этапе компиляции либо вызывают ошибку компиляции, если это невозможно.
Все выражения return и try недопустимы (если только сама функция не вызывается на этапе компиляции).
Весь код с побочными эффектами во время выполнения или зависящий от значений времени выполнения вызывает ошибку компиляции.
При всех вызовах функций компилятор интерпретирует функцию на этапе компиляции и выдаёт ошибку компиляции, если функция пытается выполнить действие с глобальными побочными эффектами во время выполнения.
Это означает, что программист может создать функцию, вызываемую как на этапе компиляции, так и во время выполнения, не изменяя саму функцию.
Рассмотрим пример:
test_fibonacci_recursion.zig
const expectEqual = @import("std").testing.expectEqual;
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 expectEqual(13, fibonacci(7));
// test fibonacci at compile-time
try comptime expectEqual(13, fibonacci(7));
}
Оболочка
$ zig test test_fibonacci_recursion.zig
1/1 test_fibonacci_recursion.test.fibonacci...OK
All 1 tests passed.
Представьте, что мы забыли базовый случай рекурсивной функции и попытались запустить тесты:
$ zig test test_fibonacci_comptime_overflow.zig/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:28: error: overflow of integer type 'u32' with value '-1'
return fibonacci(index - 1) + fibonacci(index - 2);
~~~~~~^~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:21: note: called at comptime here (7 times)
return fibonacci(index - 1) + fibonacci(index - 2);
~~~~~~~~~^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_overflow.zig:9:43: note: called at comptime here
try comptime expectEqual(13, fibonacci(7));
~~~~~~~~~^~~
Компилятор выдаёт ошибку со стеком вызовов, возникшим при попытке вычислить функцию на этапе компиляции.
К счастью, мы использовали беззнаковое целое число, поэтому при попытке вычесть 1 из 0 возникло недопустимое поведение, которое всегда приводит к ошибке компиляции, если компилятор знает, что оно произошло. Но что было бы, если бы мы использовали знаковое целое число?
Предполагается, что компилятор заметит, что вычисление этой функции на этапе компиляции заняло более 1000 ветвлений, выдаст ошибку и прекратит работу. Если программист хочет увеличить бюджет вычислений на этапе компиляции, можно использовать встроенную функцию @setEvalBranchQuota, чтобы изменить значение по умолчанию 1000 на другое.
Однако в компиляторе есть ошибка проектирования, из-за которой вместо правильного поведения здесь происходит переполнение стека. Мне очень жаль. Надеюсь, это будет исправлено до следующего выпуска.
Что произойдёт, если мы исправим базовый случай, но укажем неверное значение в строке expect?
$ zig test test_fibonacci_comptime_unreachable.zig/home/andy/src/zig/lib/std/debug.zig:421:14: error: reached unreachable code
if (!ok) unreachable; // assertion failure
^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_fibonacci_comptime_unreachable.zig:9:24: note: called at comptime 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.expectEqual(1060, sum_of_first_25_primes);
}
Оболочка
$ 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.zig/home/andy/src/zig/lib/std/Io/Writer.zig:736:18: error: unused argument in 'here is a string: '{s}' here is a number: {}
'
1 => @compileError("unused argument in '" ++ fmt ++ "'"),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
print__anon_42792: /home/andy/src/zig/lib/std/debug.zig:312:39
test.print too many arguments: /home/andy/src/zig/doc/langref/test_print_too_many_args.zig:7:10
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 = true, .r11 = true });
}
Для целевых платформ x86 и x86_64 используется синтаксис AT&T, а не более популярный синтаксис Intel. Это связано с техническими ограничениями: разбор ассемблера выполняет LLVM, а поддержка синтаксиса Intel в LLVM содержит ошибки и недостаточно хорошо протестирована.
Возможно, когда-нибудь у Zig появится собственный ассемблер. Это позволит ему более естественно интегрироваться в язык и поддерживать популярный синтаксис NASM. Этот раздел документации будет обновлён до выпуска версии 1.0.0, когда появится окончательное заявление о статусе синтаксиса AT&T по сравнению с Intel/NASM.
Ограничения выходных данных
Ограничения выходных данных в Zig всё ещё считаются нестабильными, поэтому для понимания их семантики необходимо использовать документацию LLVM и документацию GCC.
Обратите внимание, что в рамках задачи № 215 планируются некоторые несовместимые изменения ограничений выходных данных.
Ограничения входных данных
Ограничения входных данных в Zig всё ещё считаются нестабильными, поэтому для понимания их семантики необходимо использовать документацию LLVM и документацию GCC.
Обратите внимание, что в рамках задачи № 215 планируются некоторые несовместимые изменения ограничений входных данных.
Изменяемые регистры
Список изменяемых регистров — это набор регистров, значения которых не сохраняются при выполнении ассемблерного кода. Выходные и входные регистры в него не входят. Специальное значение изменяемого регистра "memory" означает, что ассемблер выполняет запись в произвольные необъявленные области памяти, а не только в память, на которую указывает объявленный косвенный выходной параметр.
Необъявление полного набора изменяемых регистров для данного выражения встроенного ассемблера является непроверяемым недопустимым поведением.
Глобальный ассемблер
Если ассемблерное выражение находится в блоке comptime на уровне контейнера, это глобальный ассемблер.
Для этого вида ассемблера действуют иные правила, чем для встроенного ассемблера. Во-первых, volatile недопустим, поскольку весь глобальный ассемблер включается безусловно. Во-вторых, у него нет входных данных, выходных данных или изменяемых регистров. Весь глобальный ассемблер без изменений объединяется в одну длинную строку и собирается вместе. Здесь нет правил подстановки шаблонов для %, подобных тем, что применяются в выражениях встроенного ассемблера.
Асинхронные функции перестали работать с выпуском версии 0.11.0. Текущий план — вернуть их в качестве низкоуровневого примитива для реализации ввода-вывода.
Встроенные функции предоставляются компилятором и имеют префикс @. Ключевое слово comptime в параметре означает, что параметр должен быть известен на этапе компиляции.
@addrSpaceCast
@addrSpaceCast(ptr: anytype) anytype
Преобразует указатель из одного адресного пространства в другое. Новое адресное пространство определяется на основе типа результата. В зависимости от текущей целевой платформы и адресных пространств это преобразование может не выполнять никаких действий, быть сложной операцией или быть недопустимым. Если преобразование допустимо, результирующий указатель указывает на ту же область памяти, что и операнд-указатель. Преобразование указателя между одинаковыми адресными пространствами всегда допустимо.
Эта функция возвращает количество байтов, на которое должен быть выровнен данный тип для соответствия ABI C на текущей целевой платформе. Если тип, на который указывает указатель, имеет такое выравнивание, его можно не указывать в типе.
Выполняет приведение типа. Это преобразование допускается, если оно однозначно и безопасно, и по возможности является предпочтительным способом преобразования между типами.
@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.
Подсказывает оптимизатору, насколько вероятно выполнение указанной ветви потока управления.
BranchHint можно найти с помощью @import("std").builtin.BranchHint.
Эта функция допустима только в качестве первого оператора в ветви потока управления или первого оператора функции.
@breakpoint
@breakpoint() void
Эта функция вставляет платформозависимую инструкцию отладочной ловушки, которая останавливает выполнение в отладчике. В отличие от @trap(), после этой точки выполнение может продолжиться, если возобновить работу программы.
Эта функция допустима только в области видимости функции.
Меняет порядок байтов целого числа. Это преобразует целое число с порядком байтов от старшего к младшему в целое число с порядком от младшего к старшему и наоборот.
Обратите внимание: для размещения в памяти с учётом порядка байтов целочисленный тип должен соответствовать количеству байтов, возвращаемому функцией @sizeOf. Это демонстрируется на примере u24. @sizeOf(u24) == 4, то есть u24 занимает в памяти 4 байта, и именно эти 4 байта меняются местами в системах с порядком байтов от младшего к старшему и от старшего к младшему. Если же для T указать тип u24, будут переставлены только 3 байта.
@bitReverse
@bitReverse(integer: anytype) T
@TypeOf(anytype) принимает любой целочисленный тип или векторный целочисленный тип.
Меняет порядок битов целочисленного значения, включая знаковый бит, если он есть.
Например, 0b10110110 (u8 = 182, i8 = -74) преобразуется в 0b01101101 (u8 = 109, i8 = 109).
Вызывает функцию так же, как и вызов выражения с помощью круглых скобок:
test_call_builtin.zig
const expectEqual = @import("std").testing.expectEqual;
test "noinline function call" {
try expectEqual(12, @call(.auto, add, .{ 3, 9 }));
}
fn add(a: i32, b: i32) i32 {
return a + b;
}
Оболочка
$ 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,
/// 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_suspend,
/// 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 be 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 с разными определениями препроцессора
Подсчитывает количество нулевых битов в старших разрядах целого числа (при порядке от старшего байта к младшему) — «подсчёт ведущих нулей».
Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, достаточным для представления числа битов целочисленного типа.
Если 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(...) void
Эта функция выводит переданные ей аргументы во время компиляции.
Чтобы случайно не оставить инструкции журнала компиляции в кодовой базе, в сборку добавляется ошибка компиляции с указанием на инструкцию журнала компиляции. Эта ошибка не позволяет сгенерировать код, но в остальном не мешает анализу.
Эту функцию можно использовать для «отладки с помощью printf» кода, выполняющегося во время компиляции.
Подсчитывает количество младших (в смысле порядка байтов от старшего к младшему — конечных) нулей в целом числе — «подсчёт конечных нулей».
Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, необходимым для представления количества битов целочисленного типа.
Если 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.
Преобразует целое число в значение перечисления. Возвращаемый тип выводится из контекста.
Попытка преобразовать целое число, которому не соответствует значение перечисления, приводит к недопустимому поведению, проверяемому средствами безопасности. Обратите внимание: значениям неполного перечисления соответствуют все целые числа из целочисленного типа его тега: значение _ представляет все остальные неназванные целые числа типа тега перечисления.
Эта функция возвращает строковое представление ошибки. Строковое представление error.OutOfMem — "OutOfMem".
Если во всём приложении нет вызовов @errorName или для всех вызовов значение err известно во время компиляции, таблица имён ошибок не создаётся.
@errorReturnTrace
@errorReturnTrace() ?*builtin.StackTrace
Если двоичный файл собран с трассировкой возврата ошибок и эта функция вызывается из функции, которая вызывает функцию с типом возврата «ошибка» или «объединение ошибки», возвращает объект трассировки стека. В противном случае возвращает null.
@errorCast
@errorCast(value: anytype) anytype
Преобразует значение типа «набор ошибок» или «объединение ошибки» из одного набора ошибок в другой. Возвращаемый тип выводится из контекста. Попытка преобразовать ошибку, которой нет в целевом наборе ошибок, приводит к недопустимому поведению, проверяемому средствами безопасности.
Создаёт в выходном объектном файле символ, ссылающийся на цель ptr.
ptr должен указывать на глобальную переменную или константу, известную во время компиляции.
Эту встроенную функцию можно вызвать из блока comptime, чтобы условно экспортировать символы. Если ptr указывает на функцию с соглашением о вызовах 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.
Выполняет доступ к полю по строке, известной во время компиляции. Работает как с полями, так и с объявлениями.
test_field_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Point = struct {
x: u32,
y: u32,
pub var z: u32 = 1;
};
test "field access by string" {
var p = Point{ .x = 0, .y = 0 };
@field(p, "x") = 4;
@field(p, "y") = @field(p, "x") + 1;
try expectEqual(4, @field(p, "x"));
try expectEqual(5, @field(p, "y"));
}
test "decl access by string" {
try expectEqual(1, @field(Point, "z"));
@field(Point, "z") = 2;
try expectEqual(2, @field(Point, "z"));
}
Оболочка
$ 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.
Получив указатель на поле структуры или объединения, возвращает указатель на структуру или объединение, содержащие это поле. Возвращаемый тип (указатель на соответствующую родительскую структуру или объединение) выводится из контекста.
Если field_ptr не указывает на поле field_name экземпляра типа результата, а тип результата имеет неопределённую раскладку, возникает непроверяемое недопустимое поведение.
@FieldType
@FieldType(comptime Type: type, comptime field_name: []const u8) type
Получив тип и имя одного из его полей, возвращает тип этого поля.
@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.
Импортирует файл по пути target, добавляя его в компиляцию, если он ещё не добавлен. target — это либо относительный путь к другому файлу от файла, содержащего вызов @import, либо имя модуля, при котором импорт ссылается на корневой исходный файл этого модуля. В обоих случаях путь к файлу должен оканчиваться на .zig (для исходного файла Zig) или .zon (для файла данных ZON).
Если target указывает на исходный файл Zig, то @import возвращает соответствующий тип структуры этого файла — по сути, как если бы вызов встроенной функции был заменён на struct { FILE_CONTENTS }. Возвращаемый тип — type.
Если target указывает на файл ZON, то @import возвращает значение литерала в файле. Если тип результата выводится из контекста (типы результата), возвращаемым типом будет этот тип, а литерал ZON интерпретируется как значение этого типа (тип результата передаётся через выражение ZON). В противном случае возвращаемым типом будет тип эквивалентного выражения Zig — по сути, как если бы вызов встроенной функции был заменён содержимым файла ZON.
Для импорта всегда доступны следующие модули:
@import("std") — стандартная библиотека Zig
@import("builtin") — сведения, зависящие от целевой платформы. Команда zig build-exe --show-builtin выводит исходный код в stdout для справки.
@import("root") — псевдоним корневого модуля. В типичной структуре проекта это означает, что он ссылается на src/main.zig.
Возвращает, выполнялась ли встроенная функция в контексте comptime. Результат является константой времени компиляции.
Это можно использовать для предоставления альтернативных реализаций функций, пригодных для comptime. Не следует, например, использовать эту функцию, чтобы исключить выполнение определённых функций во время компиляции.
Преобразует одно целое число в другое, сохраняя то же числовое значение. Возвращаемый тип выводится из контекста. Попытка преобразовать число, выходящее за диапазон целевого типа, приводит к недопустимому поведению, проверяемому средствами безопасности.
$ zig test test_intCast_builtin.zig
1/1 test_intCast_builtin.test.integer cast panic...thread 934579 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/test_intCast_builtin.zig:4:19: 0x1234e40 in test.integer cast panic (test_intCast_builtin.zig)
const b: u8 = @intCast(a);
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/22604da2684fadfdf98324181ed766ec/test --seed=0x9949decf
Чтобы усечь значащие биты числа, выходящего за диапазон целевого типа, используйте @truncate.
Если T равно comptime_int, то семантически это эквивалентно приведению типа.
@intFromBool
@intFromBool(value: bool) u1
Преобразует true в @as(u1, 1), а false — в @as(u1, 0).
Преобразует значение перечисления в его целочисленный тип тега. Если передано помеченное объединение, в качестве значения перечисления используется значение тега.
Если возможно только одно значение перечисления, результатом будет comptime_int, известное на этапе comptime.
Преобразует value в usize, являющееся адресом указателя. value может быть *T или ?*T.
Для обратного преобразования используйте @ptrFromInt
@max
@max(...) T
Принимает два или более аргумента и возвращает наибольшее из переданных значений (максимум). Эта встроенная функция принимает целые числа, числа с плавающей точкой и векторы любого из этих типов. В последнем случае операция выполняется поэлементно.
NaN обрабатываются следующим образом: возвращается наибольшее из переданных значений, не являющихся NaN. Если все операнды — NaN, возвращается NaN.
Эта функция копирует байты из одной области памяти в другую.
dest должен быть изменяемым срезом, изменяемым указателем на массив или изменяемым указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.
source должен быть срезом, указателем на массив или указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.
Тип элементов source должен иметь такое же представление в памяти, как тип элементов dest.
Как и в циклах for, длину должен задавать хотя бы один из аргументов — source или dest; если заданы обе длины, они должны быть равны.
Наконец, области памяти не должны перекрываться.
@memset
@memset(dest, elem) void
Эта функция устанавливает для всех элементов области памяти значение elem.
dest должен быть изменяемым срезом или изменяемым указателем на массив. Он может иметь любое выравнивание и любой тип элементов.
elem приводится к типу элементов dest.
Для безопасного обнуления конфиденциальных данных в памяти следует использовать std.crypto.secureZero
@memmove
@memmove(dest, source) void
Эта функция копирует байты из одной области памяти в другую, но, в отличие от @memcpy, области могут перекрываться.
dest должен быть изменяемым срезом, изменяемым указателем на массив или изменяемым указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.
source должен быть срезом, указателем на массив или указателем на множество элементов указателем. Он может иметь любое выравнивание и любой тип элементов.
Тип элементов source должен иметь такое же представление в памяти, как тип элементов dest.
Как и в циклах for, длину должен задавать хотя бы один из аргументов — source или dest; если заданы обе длины, они должны быть равны.
@min
@min(...) T
Принимает два или более аргумента и возвращает наименьшее из переданных значений (минимум). Эта встроенная функция принимает целые числа, числа с плавающей точкой и векторы любого из этих типов. В последнем случае операция выполняется поэлементно.
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.
Подсчитывает количество установленных битов в целом числе — «подсчёт единичных битов».
Возвращаемый тип — беззнаковое целое число или вектор беззнаковых целых чисел с минимальным количеством битов, необходимым для представления количества битов целочисленного типа.
Эта встроенная функция указывает компилятору выдать инструкцию предварительной выборки, если целевой ЦП её поддерживает. Если целевой ЦП не поддерживает запрошенную инструкцию предварительной выборки, встроенная функция ничего не делает. Эта функция не влияет на поведение программы, а только на её производительность.
Аргумент ptr может иметь любой тип указателя и задаёт адрес памяти для предварительной выборки. Эта функция не разыменовывает указатель; передача указателя на недопустимую область памяти полностью законна и не приводит к недопустимому поведению.
PrefetchOptions можно найти с помощью @import("std").builtin.PrefetchOptions.
@ptrCast
@ptrCast(value: anytype) anytype
Преобразует указатель одного типа в указатель другого типа. Возвращаемый тип выводится из контекста.
Изменение адресного пространства указателя — используйте @addrSpaceCast.
Увеличение выравнивания указателя — используйте @alignCast.
Преобразование указателя, не являющегося срезом, в срез — используйте синтаксис среза ptr[start..end].
@ptrFromInt
@ptrFromInt(address: usize) anytype
Преобразует целое число в указатель. Возвращаемый тип выводится из контекста. Для обратного преобразования используйте @intFromPtr. Преобразование адреса 0 в целевой тип, который не является необязательным и не имеет атрибута allowzero, приводит к панике недопустимый 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.
Эта функция возвращает адрес следующей инструкции машинного кода, которая будет выполнена после возврата из текущей функции.
Последствия этого зависят от целевой платформы и различаются на разных платформах.
Эта функция допустима только в области видимости функции. Если функция встраивается в вызывающую функцию, возвращаемый адрес будет относиться к вызывающей функции.
Увеличивает максимальное количество обратных переходов, которое может выполнить код во время компиляции, прежде чем выполнение будет прервано с ошибкой компиляции.
Если 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.zig/home/andy/src/zig/doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: error: evaluation exceeded 1000 backwards branches
while (i < 1001) : (i += 1) {}
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/andy/src/zig/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; // Unchecked Illegal 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; // Unchecked Illegal Behavior in all build modes.
}
}
}
Оболочка
$ zig test test_setRuntimeSafety_builtin.zig -OReleaseFast
1/1 test_setRuntimeSafety_builtin.test.@setRuntimeSafety...thread 935744 panic: integer overflow
/home/andy/src/zig/doc/langref/test_setRuntimeSafety_builtin.zig:11:11: 0x10855f8 in test.@setRuntimeSafety (test)
x += 1;
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x1074feb in mainTerminal (test)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x107314a in callMain (test)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x1072c1d in _start (test)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/fedda1f87f11ec581f9d78378c471f74/test --seed=0x5af059f2
Примечание: планируется заменить @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.
Эта функция возвращает количество байтов, необходимое для хранения T в памяти. Результат является константой времени компиляции, зависящей от целевой платформы.
Этот размер может включать байты-заполнители. Если бы в памяти находились два элемента T подряд, заполнение представляло бы собой смещение в байтах между элементом с индексом 0 и элементом с индексом 1. Для целых чисел подумайте, следует ли использовать @sizeOf(T) или @typeInfo(T).int.bits.
Эта функция измеряет размер во время выполнения. Для типов, использование которых во время выполнения запрещено, таких как comptime_int и type, результатом будет 0.
Обратите внимание, что свёртки .Add и .Mul для целочисленных типов выполняются с циклическим переполнением; при применении к типам с плавающей точкой сохраняется ассоциативность операции, если только режим плавающей точки не установлен в .optimized.
test_reduce_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
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 expectEqual(@Vector(4, bool), @TypeOf(result));
const is_all_true = @reduce(.And, result);
try comptime expectEqual(bool, @TypeOf(is_all_true));
try expectEqual(false, is_all_true);
}
Командная строка
$ 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
Преобразует значение перечисления или объединения в строковый литерал, представляющий его имя.
Если перечисление является неполным и значение тега не соответствует имени, вызывается недопустимое поведение с проверкой безопасности.
@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
Эта функция вставляет платформозависимую инструкцию trap/jam, которую можно использовать для аварийного завершения программы. Она может быть реализована явной генерацией недопустимой инструкции, которая может вызвать исключение недопустимой инструкции. В отличие от @breakpoint(), выполнение после этой точки не продолжается.
Вне области видимости функции эта встроенная функция вызывает ошибку компиляции.
Эта функция отбрасывает биты целочисленного типа, в результате чего получается целочисленный тип меньшего или такого же размера. Возвращаемый тип выводится автоматически.
Эта функция всегда отбрасывает значащие биты целого числа независимо от порядка байтов на целевой платформе.
Вызов @truncate для числа, выходящего за диапазон целевого типа, является корректным и допустимым:
$ zig test test_truncate_builtin.zig
1/1 test_truncate_builtin.test.integer truncation...OK
All 1 tests passed.
Используйте @intCast для преобразования чисел, которые гарантированно помещаются в целевой тип.
@EnumLiteral
@EnumLiteral() type
Возвращает тип «литерал перечисления», доступный только во время компиляции. Это тип непреобразованных литералов перечисления. Значения этого типа могут быть приведены к любому перечислению с соответствующим полем.
@Int
@Int(comptime signedness: std.builtin.Signedness, comptime bits: u16) type
Возвращает целочисленный тип с заданными знаковостью и разрядностью.
Эта функция возвращает строковое представление типа в виде массива. Оно эквивалентно строковому литералу с именем типа. Возвращаемое имя типа является полным: родительское пространство имён включается в имя типа, а части разделяются точками.
@TypeOf
@TypeOf(...) type
@TypeOf — это специальная встроенная функция, которая принимает любое ненулевое количество выражений в качестве параметров и возвращает тип результата, используя разрешение одноранговых типов.
Выражения вычисляются, однако гарантируется отсутствие у них побочных эффектов во время выполнения:
test_TypeOf_builtin.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "no runtime side effects" {
var data: i32 = 0;
const T = @TypeOf(foo(i32, &data));
try comptime expectEqual(i32, T);
try expectEqual(0, data);
}
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
Это то же самое, что и синтаксис инициализации объединения, за исключением того, что имя поля является значением, известным на этапе компиляции, а не токеном идентификатора.
Возвращает индекс рабочей группы в текущем вызове ядра для измерения 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 вызывают то, что называется «недопустимым поведением» (IB). Если недопустимое поведение обнаружено во время компиляции, Zig выдаёт ошибку компиляции и отказывается продолжать работу. Если недопустимое поведение не обнаружено во время компиляции, оно относится к одной из двух категорий.
Некоторые виды недопустимого поведения проверяются на безопасность: это означает, что компилятор вставляет «проверки безопасности» во все места, где недопустимое поведение может возникнуть во время выполнения, чтобы определить, вот-вот ли оно произойдёт. Если это так, «проверка безопасности» завершается неудачно и вызывает панику.
Все остальные виды недопустимого поведения являются непроверяемыми, то есть компилятор не может вставить для них проверки безопасности. Если во время выполнения возникает непроверяемое недопустимое поведение, может произойти что угодно: обычно это приводит к сбою, но оптимизатор может заставить непроверяемое недопустимое поведение делать что угодно, например вызывать произвольные функции или повреждать произвольные данные. Это похоже на концепцию «неопределённого поведения» в некоторых других языках. Обратите внимание, что непроверяемое недопустимое поведение всегда приводит к ошибке компиляции при вычислении на этапе компиляции, поскольку компилятор Zig способен выполнять более сложные проверки во время компиляции, чем во время выполнения.
Большинство видов недопустимого поведения проверяются на безопасность. Однако для оптимизации проверки безопасности по умолчанию отключены в режимах оптимизации ReleaseFast и ReleaseSmall. Проверки безопасности также можно включать или отключать для каждого блока отдельно, переопределяя значение по умолчанию для текущего режима оптимизации с помощью @setRuntimeSafety. Когда проверки безопасности отключены, проверяемое на безопасность недопустимое поведение ведёт себя как непроверяемое недопустимое поведение; то есть его вызов может привести к любому поведению.
При сбое проверки безопасности обработчик паники Zig по умолчанию завершает программу с трассировкой стека, например:
test_illegal_behavior.zig
test "safety check" {
unreachable;
}
Командная строка
$ zig test test_illegal_behavior.zig
1/1 test_illegal_behavior.test.safety check...thread 926313 panic: reached unreachable code
/home/andy/src/zig/doc/langref/test_illegal_behavior.zig:2:5: 0x1234e2c in test.safety check (test_illegal_behavior.zig)
unreachable;
^
/home/andy/src/zig/lib/compiler/test_runner.zig:291:25: 0x11ef546 in mainTerminal (test_runner.zig)
if (test_fn.func()) |_| {
^
/home/andy/src/zig/lib/compiler/test_runner.zig:73:28: 0x11eed62 in main (test_runner.zig)
return mainTerminal(init);
^
/home/andy/src/zig/lib/std/start.zig:686:88: 0x11eb6c6 in callMain (std.zig)
if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11eb0d1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
error: the following test command terminated with signal ABRT:
/home/andy/src/zig/.zig-cache/o/072ba49a7ed3af767b4ac6cae5d991d5/test --seed=0x56efd66b
$ zig test test_comptime_reaching_unreachable.zig/home/andy/src/zig/doc/langref/test_comptime_reaching_unreachable.zig:5:14: error: reached unreachable code
if (!ok) unreachable; // assertion failure
^~~~~~~~~~~
/home/andy/src/zig/doc/langref/test_comptime_reaching_unreachable.zig:2:11: note: called at comptime here
assert(false);
~~~~~~^~~~~~~
$ zig build-exe runtime_index_out_of_bounds.zig
$ ./runtime_index_out_of_bounds
thread 931962 panic: index out of bounds: index 5, len 5
/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:7:13: 0x11d58de in foo (runtime_index_out_of_bounds.zig)
return x[5];
^
/home/andy/src/zig/doc/langref/runtime_index_out_of_bounds.zig:2:18: 0x11d580a in main (runtime_index_out_of_bounds.zig)
const x = foo("hello");
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Преобразование отрицательного числа в беззнаковое целое
$ zig build-exe runtime_invalid_cast.zig
$ ./runtime_invalid_cast
thread 933932 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/runtime_invalid_cast.zig:6:27: 0x11d580f in main (runtime_invalid_cast.zig)
const unsigned: u32 = @intCast(value);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Чтобы получить максимальное значение беззнакового целого числа, используйте std.math.maxInt.
$ zig build-exe runtime_invalid_cast_truncate.zig
$ ./runtime_invalid_cast_truncate
thread 930996 panic: integer does not fit in destination type
/home/andy/src/zig/doc/langref/runtime_invalid_cast_truncate.zig:6:22: 0x11d5810 in main (runtime_invalid_cast_truncate.zig)
const byte: u8 = @intCast(spartan_count);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
$ zig test test_comptime_overflow.zig/home/andy/src/zig/doc/langref/test_comptime_overflow.zig:3:10: error: overflow of integer type 'u8' with value '256'
byte += 1;
~~~~~^~~~
Эти встроенные функции возвращают кортеж, содержащий информацию о наличии переполнения (в виде u1) и биты результата операции, которые могли переполниться:
$ zig test test_comptime_shlExact_overflow.zig/home/andy/src/zig/doc/langref/test_comptime_shlExact_overflow.zig:2:15: error: overflow of integer type 'u8' with value '340'
const x = @shlExact(@as(u8, 0b01010101), 2);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ zig test test_comptime_division_by_zero.zig/home/andy/src/zig/doc/langref/test_comptime_division_by_zero.zig:4:19: error: division by zero here causes illegal 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 928583 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_division_by_zero.zig:7:17: 0x11d5820 in main (runtime_division_by_zero.zig)
const c = a / b;
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
$ zig test test_comptime_remainder_division_by_zero.zig/home/andy/src/zig/doc/langref/test_comptime_remainder_division_by_zero.zig:4:19: error: division by zero here causes illegal 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 931002 panic: division by zero
/home/andy/src/zig/doc/langref/runtime_remainder_division_by_zero.zig:7:17: 0x11d5820 in main (runtime_remainder_division_by_zero.zig)
const c = a % b;
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
$ zig test test_comptime_divExact_remainder.zig/home/andy/src/zig/doc/langref/test_comptime_divExact_remainder.zig:4:15: error: exact division produced remainder
const c = @divExact(a, b);
^~~~~~~~~~~~~~~
$ zig test test_comptime_unwrap_null.zig/home/andy/src/zig/doc/langref/test_comptime_unwrap_null.zig:3:35: error: unable to unwrap null
const number = optional_number.?;
~~~~~~~~~~~~~~~^~
$ zig test test_comptime_invalid_error_code.zig/home/andy/src/zig/doc/langref/test_comptime_invalid_error_code.zig:2:23: error: integer value '12345' represents no error
_ = @errorFromInt(12345);
^~~~~
$ zig test test_comptime_invalid_enum_cast.zig/home/andy/src/zig/doc/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);
^~~~~~~~~~~~~~~
/home/andy/src/zig/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.zig/home/andy/src/zig/doc/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 927819 panic: incorrect alignment
/home/andy/src/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:9:64: 0x11d6176 in foo (runtime_incorrect_pointer_alignment.zig)
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: 0x11d4bde in main (runtime_incorrect_pointer_alignment.zig)
if (foo(bytes) != 0x11111111) return error.Wrong;
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d51cc in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(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.zig/home/andy/src/zig/doc/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;
~^~~~~~
/home/andy/src/zig/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 932701 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: 0x11d58ce in bar (runtime_wrong_union_field_access.zig)
f.float = 12.34;
^
/home/andy/src/zig/doc/langref/runtime_wrong_union_field_access.zig:10:8: 0x11d580e in main (runtime_wrong_union_field_access.zig)
bar(&f);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Эта проверка безопасности недоступна для объединений extern или packed.
Чтобы изменить активное поле объединения, присвойте значение всему объединению, например:
$ zig test test_comptime_out_of_bounds_float_to_integer_cast.zig/home/andy/src/zig/doc/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 935571 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: 0x11d583f in main (runtime_out_of_bounds_float_to_integer_cast.zig)
const int: i32 = @intFromFloat(float);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Недопустимое преобразование указателя с нулевым адресом
Это происходит при преобразовании указателя с адресом 0 в указатель, которому адрес 0 недопустим. Например, указатели C, опциональные указатели и указатели allowzero допускают нулевой адрес, а обычные указатели — нет.
$ zig build-exe runtime_invalid_null_pointer_cast.zig
$ ./runtime_invalid_null_pointer_cast
thread 931960 panic: cast causes pointer to be null
/home/andy/src/zig/doc/langref/runtime_invalid_null_pointer_cast.zig:4:23: 0x11d581a in main (runtime_invalid_null_pointer_cast.zig)
const ptr: *i32 = @ptrCast(opt_ptr);
^
/home/andy/src/zig/lib/std/start.zig:685:59: 0x11d5121 in callMain (std.zig)
if (fn_info.params.len == 0) return wrapMain(root.main());
^
/home/andy/src/zig/lib/std/start.zig:190:5: 0x11d4b81 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
Память
Язык Zig не управляет памятью за программиста. Именно поэтому в Zig нет среды выполнения и код Zig без проблем работает во множестве сред, включая программное обеспечение реального времени, ядра операционных систем, встраиваемые устройства и серверы с низкой задержкой. Вследствие этого программисты на Zig всегда должны уметь ответить на вопрос:
Как и в Zig, в языке программирования C управление памятью выполняется вручную. Однако, в отличие от Zig, в C есть аллокатор по умолчанию — malloc, realloc и free. При компоновке с libc Zig предоставляет этот аллокатор через std.heap.c_allocator. Однако по соглашению в Zig нет аллокатора по умолчанию. Вместо этого функции, которым требуется выделение памяти, принимают параметр Allocator. Аналогично, некоторые структуры данных принимают параметр 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.DebugAllocator. Однако рекомендуется следовать руководству Выбор аллокатора.
Выбор аллокатора
Выбор аллокатора зависит от ряда факторов. Ниже приведена блок-схема, которая поможет вам принять решение:
Вы пишете библиотеку? В этом случае лучше принимать Allocator в качестве параметра и позволить пользователям вашей библиотеки самим выбирать аллокатор.
Вы компонуете программу с libc? В этом случае std.heap.c_allocator, скорее всего, будет подходящим выбором, по крайней мере для основного аллокатора.
Ограничено ли максимальное количество байтов, которое вам потребуется, числом, известным во время comptime? В этом случае используйте std.heap.FixedBufferAllocator.
Является ли ваша программа приложением командной строки, которое выполняется от начала до конца без принципиального циклического шаблона (например, основного цикла видеоигры или обработчика запросов веб-сервера), так что имеет смысл освободить всю память сразу в конце? В этом случае рекомендуется использовать следующий подход: cli_allocation.zig
При использовании аллокатора такого типа не нужно вручную освобождать память. Вся память освобождается одновременно при вызове arena.deinit().
Относится ли выделение памяти к циклическому шаблону, например к основному циклу видеоигры или обработчику запросов веб-сервера? Если всю память можно освободить сразу в конце цикла — например, после полной отрисовки кадра видеоигры или обработки запроса веб-сервера, — то std.heap.ArenaAllocator станет отличным кандидатом. Как показано в предыдущем пункте, это позволяет освобождать целые арены памяти одновременно. Также учтите, что если можно установить верхнюю границу объёма памяти, то std.heap.FixedBufferAllocator можно использовать для дополнительной оптимизации.
Вы пишете тест и хотите убедиться, что error.OutOfMemory обрабатывается правильно? В этом случае используйте std.testing.FailingAllocator.
Вы пишете тест? В этом случае используйте std.testing.allocator.
Наконец, если ни один из перечисленных вариантов не подходит, вам нужен аллокатор общего назначения. В режиме Debug доступен std.heap.DebugAllocator — функция, которая принимает comptimeструктуру с параметрами конфигурации и возвращает тип. Обычно вы создаёте ровно один такой аллокатор в главной функции, а затем передаёте его или подаллокаторы различным частям приложения.
Если вы компилируете в режиме ReleaseFast, std.heap.smp_allocator — надёжный выбор аллокатора общего назначения.
Также можно рассмотреть возможность реализации собственного аллокатора.
Где находятся байты?
Строковые литералы, такие как "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.zig/home/andy/src/zig/doc/langref/test_string_literal_to_slice.zig:6:9: error: expected type '[]u8', found '*const [5:0]u8'
foo("hello");
^~~~~~~
/home/andy/src/zig/doc/langref/test_string_literal_to_slice.zig:6:9: note: cast discards const qualifier
/home/andy/src/zig/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, если их значение известно во время comptime, хранятся в глобальной секции константных данных. В глобальной секции константных данных также хранятся переменные времени компиляции.
Объявления var внутри функций хранятся в стековом кадре функции. После возврата из функции любые указатели на переменные в её стековом кадре становятся недействительными ссылками, а их разыменование приводит к непроверяемому недопустимому поведению.
Объявления var на верхнем уровне или внутри объявлений struct хранятся в глобальной секции данных.
Место хранения памяти, выделенной с помощью allocator.alloc или allocator.create, определяется реализацией аллокатора.
TODO: переменные локального потока
Сбой выделения памяти в куче
Многие языки программирования при сбое выделения памяти в куче безусловно завершают работу с аварией. По соглашению программисты на Zig не считают это удовлетворительным решением. Вместо этого error.OutOfMemory обозначает сбой выделения памяти в куче, и библиотеки Zig возвращают этот код ошибки, если сбой выделения памяти в куче не позволил успешно завершить операцию.
Некоторые утверждают, что обрабатывать сбой выделения памяти в куче бессмысленно, поскольку в некоторых операционных системах, например Linux, по умолчанию включено чрезмерное выделение памяти. В этом рассуждении есть множество проблем:
Функция чрезмерного выделения памяти есть лишь в некоторых операционных системах.
В Linux она включена по умолчанию, но её можно настроить.
Windows не использует чрезмерное выделение памяти.
Во встраиваемых системах чрезмерного выделения памяти нет.
В любительских операционных системах чрезмерное выделение памяти может быть как доступно, так и недоступно.
В системах реального времени не только отсутствует чрезмерное выделение памяти, но и максимальный объём памяти для каждого приложения обычно определяется заранее.
Одна из главных целей создания библиотеки — повторное использование кода. Если библиотека правильно обрабатывает сбои выделения памяти, её можно использовать в большем числе ситуаций.
Хотя некоторые программы стали зависеть от включённого чрезмерного выделения памяти, его существование стало причиной бесчисленных катастроф с точки зрения удобства использования. Когда система с включённым чрезмерным выделением памяти, например Linux с настройками по умолчанию, почти исчерпывает память, она зависает и становится непригодной к использованию. В этот момент OOM Killer выбирает приложение для завершения на основе эвристик. Это недетерминированное решение часто приводит к завершению важного процесса и зачастую не позволяет вернуть систему к рабочему состоянию.
Рекурсия
Рекурсия — основополагающий инструмент моделирования в программном обеспечении. Однако у неё есть часто упускаемая из виду проблема: неограниченное выделение памяти.
Если вкратце, сейчас рекурсия работает так, как и следовало ожидать. Хотя код Zig пока не защищён от переполнения стека, планируется, что будущая версия Zig обеспечит такую защиту, при этом от кода Zig потребуется определённая степень взаимодействия.
Время жизни и владение
Программист на Zig отвечает за то, чтобы к указателю не обращались после того, как память, на которую он указывает, становится недоступной. Обратите внимание, что срез является разновидностью указателя, поскольку ссылается на другую область памяти.
Чтобы избежать ошибок при работе с указателями, полезно следовать нескольким соглашениям. Как правило, если функция возвращает указатель, её документация должна пояснять, кто является «владельцем» указателя. Это помогает программисту определить, когда следует освободить память, на которую он указывает, если такое освобождение вообще требуется.
Например, в документации функции может быть сказано: «возвращённая память принадлежит вызывающему коду». В таком случае код, вызывающий функцию, должен определить, когда освободить эту память. Вероятно, в такой ситуации функция принимает параметр Allocator.
Иногда время жизни указателя может быть сложнее. Например, срез std.ArrayList(T).items остаётся действительным до следующего изменения размера списка, например при добавлении новых элементов.
В документации API функций и структур данных необходимо тщательно описывать семантику владения указателями и времени их жизни. Владение определяет, кто отвечает за освобождение памяти, на которую указывает указатель, а время жизни определяет момент, когда эта память становится недоступной (иначе возникнет недопустимое поведение).
Переменные времени компиляции
Доступ к переменным времени компиляции можно получить, импортировав пакет "builtin", который компилятор делает доступным для каждого исходного файла Zig. Он содержит константы времени компиляции, такие как текущая целевая платформа, порядок байтов и режим выпуска.
Компиляция Zig разделена на модули. Каждый модуль — это набор исходных файлов Zig, один из которых является корневым исходным файлом модуля. Каждый модуль может зависеть от любого количества других модулей, образуя ориентированный граф (циклические зависимости между модулями допускаются). Если модуль A зависит от модуля B, любой исходный файл Zig в модуле A может импортировать корневой исходный файл модуля B с помощью @import и имени модуля. По сути, модуль служит псевдонимом для импорта исходного файла Zig (который может находиться в совершенно другой части файловой системы).
Простая программа на Zig, скомпилированная с помощью zig build-exe, состоит из двух основных модулей: модуля с вашим кодом, известного как «главный» или «корневой», и стандартной библиотеки. Ваш модуль зависит от модуля стандартной библиотеки под именем «std», что позволяет писать @import("std")! Фактически каждый модуль в компиляции Zig — включая саму стандартную библиотеку — неявно зависит от модуля стандартной библиотеки под именем «std».
У «корневого модуля» (того, который вы указали в примере с zig build-exe) есть особое свойство. Как и стандартная библиотека, он неявно доступен всем модулям (включая себя), но уже под именем «root». Таким образом, @import("root") всегда будет эквивалентно @import вашего «главного» исходного файла (который часто, но не обязательно, называется main.zig).
Структуры исходных файлов
Каждый исходный файл Zig неявно является объявлением struct; можно представить, что содержимое файла буквально окружено struct { ... }. Это означает, что на верхнем уровне файла разрешены не только объявления, но и поля:
TopLevelFields.zig
//! Because this file contains fields, it is a type which is intended to be instantiated, and so
//! is named in TitleCase instead of snake_case by convention.
foo: u32,
bar: u64,
/// `@This()` can be used to refer to this struct type. In files with fields, it is quite common to
/// name the type here, so it can be easily referenced by other declarations in this file.
const TopLevelFields = @This();
pub fn init(val: u32) TopLevelFields {
return .{
.foo = val,
.bar = val * 10,
};
}
Такие файлы можно создавать так же, как и любые другие типы struct. На тип корневой структуры файла можно ссылаться внутри этого файла с помощью @This.
Обнаружение файлов и объявлений
В Zig важно, подвергается ли какая-либо часть кода семантическому анализу; по сути, заглядывает ли в неё компилятор. Анализируемый код определяется тем, какие файлы и объявления «обнаруживаются» из определённой точки. Процесс «обнаружения» основан на простом наборе рекурсивных правил:
Если анализируется вызов @import, анализируется импортируемый файл.
Если анализируется тип (включая файл), анализируются все объявления comptime и export внутри него.
Если анализируется тип (включая файл), компиляция выполняется для тестирования, а модуль, содержащий тип, является корневым модулем компиляции, то анализируются также все объявления test внутри него.
Если анализируется ссылка на именованное объявление (то есть его использование), анализируется объявление, на которое ссылаются. Порядок объявлений не имеет значения: ссылка может находиться до или после объявления, на которое она ссылается, и даже в другом файле.
Вот и всё! Эти правила определяют, как обнаруживаются файлы и объявления Zig. Осталось понять, с чего начинается этот процесс.
Ответ: с корня стандартной библиотеки. Каждая компиляция Zig начинается с анализа файла lib/std/std.zig. Этот файл содержит объявление comptime, которое импортирует lib/std/start.zig, а тот, в свою очередь, использует @import("root") для ссылки на «корневой модуль». Поэтому файл, указанный вами в качестве корневого исходного файла главного модуля, фактически тоже является корневым, поскольку стандартная библиотека всегда ссылается на него.
Часто требуется убедиться, что определённые объявления — особенно объявления test или export — обнаружены. С учётом приведённых выше правил для этого обычно используют @import внутри блока comptime или test:
force_file_discovery.zig
comptime {
// This will ensure that the file 'api.zig' is always discovered (as long as this file is discovered).
// It is useful if 'api.zig' contains important exported declarations.
_ = @import("api.zig");
// We could also have a file which contains declarations we only want to export depending on a comptime
// condition. In that case, we can use an `if` statement here:
if (builtin.os.tag == .windows) {
_ = @import("windows_api.zig");
}
}
test {
// This will ensure that the file 'tests.zig' is always discovered (as long as this file is discovered),
// if this compilation is a test. It is useful if 'tests.zig' contains tests we want to ensure are run.
_ = @import("tests.zig");
// We could also have a file which contains tests we only want to run depending on a comptime condition.
// In that case, we can use an `if` statement here:
if (builtin.os.tag == .windows) {
_ = @import("windows_tests.zig");
}
}
const builtin = @import("builtin");
Специальные корневые объявления
Поскольку корневой исходный файл корневого модуля всегда доступен через @import("root"), библиотеки, в том числе стандартная библиотека Zig, иногда используют его как место, где программа предоставляет этой библиотеке некоторую «глобальную» информацию. Стандартная библиотека Zig ищет в этом файле несколько объявлений.
При создании исполняемого файла важнее всего найти в этом файле точку входа программы. Чаще всего это функция с именем main, которую std.start вызывает после выполнения важных действий по инициализации.
В качестве альтернативы наличие объявления с именем _start (например, pubconst _start = {};) отключит логику std.start по умолчанию, позволяя экспортировать из корневого исходного файла низкоуровневую точку входа по необходимости.
entry_point.zig
/// `std.start` imports this file using `@import("root")`, and uses this declaration as the program's
/// user-provided entry point. It can return any of the following types:
/// * `void`
/// * `E!void`, for any error set `E`
/// * `u8`
/// * `E!u8`, for any error set `E`
/// Returning a `void` value from this function will exit with code 0.
/// Returning a `u8` value from this function will exit with the given status code.
/// Returning an error value from this function will print an Error Return Trace and exit with code 1.
pub fn main() void {
std.debug.print("Hello, World!\n", .{});
}
// If uncommented, this declaration would suppress the usual std.start logic, causing
// the `main` declaration above to be ignored.
//pub const _start = {};
const std = @import("std");
Если компиляция Zig выполняется с компоновкой с libc, функция main может быть объявлением exportfn, сигнатура которого соответствует сигнатуре функции C main:
В определённых ситуациях std.start может использовать и другие объявления точек входа, например wWinMain или EfiMain. Подробности об этих объявлениях см. в логике lib/std/start.zig.
Стандартная библиотека также ищет в корневом исходном файле корневого модуля объявление с именем std_options. Если оно присутствует, ожидается, что это будет структура типа std.Options; она позволяет программе настраивать некоторые функции стандартной библиотеки, например реализацию std.log.
std_options.zig
/// The presence of this declaration allows the program to override certain behaviors of the standard library.
/// For a full list of available options, see the documentation for `std.Options`.
pub const std_options: std.Options = .{
// By default, in safe build modes, the standard library will attach a segfault handler to the program to
// print a helpful stack trace if a segmentation fault occurs. Here, we can disable this, or even enable
// it in unsafe build modes.
.enable_segfault_handler = true,
// This is the logging function used by `std.log`.
.logFn = myLogFn,
};
fn myLogFn(
comptime level: std.log.Level,
comptime scope: @EnumLiteral(),
comptime format: []const u8,
args: anytype,
) void {
// We could do anything we want here!
// ...but actually, let's just call the default implementation.
std.log.defaultLog(level, scope, format, args);
}
const std = @import("std");
Стандартная библиотека Zig ищет в корневом исходном файле корневого модуля объявление с именем panic. Если оно присутствует, ожидается, что это будет пространство имён (тип-контейнер) с объявлениями, предоставляющими разные обработчики паники.
Базовую реализацию этого пространства имён см. в std.debug.simple_panic.
Переопределить способ вывода сообщений обработчиком паники, сохранив при этом форматированные сообщения о паниках безопасности, включённые по умолчанию, можно с помощью std.debug.FullPanic:
Система сборки Zig предоставляет кроссплатформенный способ без зависимостей для описания логики, необходимой для сборки проекта. При использовании этой системы логика сборки проекта задаётся в файле build.zig с помощью API системы сборки Zig, предназначенного для объявления и настройки артефактов сборки и других задач.
Вот некоторые задачи, с которыми помогает система сборки:
Выполнение задач параллельно и кэширование результатов.
Зависимость от других проектов.
Предоставление пакета, от которого могут зависеть другие проекты.
Создание артефактов сборки с помощью компилятора Zig. Это включает сборку исходного кода Zig, а также исходного кода C и C++.
Получение настроенных пользователем параметров и их использование для настройки сборки.
Предоставление конфигурации сборки в виде значений comptime посредством файла, который можно импортировать в коде Zig.
Кэширование артефактов сборки, чтобы избежать ненужного повторения этапов.
Запуск артефактов сборки или установленных в системе инструментов.
Запуск тестов и проверка того, что вывод при выполнении артефакта сборки соответствует ожидаемому значению.
Запуск zig fmt для всей кодовой базы или её части.
Пользовательские задачи.
Чтобы воспользоваться системой сборки, выполните команду zig build --help, чтобы просмотреть справку по использованию командной строки. В ней будут указаны параметры конкретного проекта, объявленные в скрипте build.zig.
Хотя Zig не зависит от C и, в отличие от большинства других языков, не зависит от libc, Zig признаёт важность взаимодействия с существующим кодом на C.
Zig предоставляет несколько способов взаимодействия с C.
Примитивные типы C
Для этих типов гарантирована совместимость с ABI языка C, и их можно использовать как любые другие типы.
c_char
c_short
c_ushort
c_int
c_uint
c_long
c_ulong
c_longlong
c_ulonglong
c_longdouble
Для взаимодействия с типом C void используйте anyopaque.
Функция @cImport принимает выражение в качестве параметра. Это выражение вычисляется во время компиляции и используется для управления директивами препроцессора и включения нескольких файлов .h:
Выражение @cImport
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. Переведённый файл выводится в stdout.
-I: указать каталог для поиска включаемых файлов. Этот флаг можно использовать несколько раз. Эквивалент флага -I clang. Текущий каталог не включается по умолчанию; чтобы добавить его, используйте -I..
-D: определить макрос препроцессора. Эквивалент флага -D clang.
-cflags [flags] --: передать clang произвольные дополнительные флаги командной строки. Примечание: список флагов должен завершаться --
-target: целевая тройка для переведённого кода Zig. Если целевая платформа не указана, будет использоваться текущая целевая платформа хоста.
Важно! При переводе кода C с помощью zig translate-c необходимо использовать тот же тройной код -target, который будет использоваться при компиляции переведённого кода. Кроме того, необходимо убедиться, что используемые флаги -cflags, если они есть, совпадают с флагами cflags, используемыми кодом в целевой системе. Неверные значения -target или -cflags могут привести к ошибкам разбора в clang или Zig либо к трудноуловимым несовместимостям ABI при компоновке с кодом C.
@cImport и zig translate-c используют одну и ту же базовую функциональность перевода C, поэтому с технической точки зрения они эквивалентны. На практике @cImport удобно использовать для быстрого и простого доступа к числовым константам, typedef и типам записей без дополнительной настройки. Если нужно передать clang флаги cflags или отредактировать переведённый код, рекомендуется использовать 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): writing C import source to /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h
--zig-integration -x c -isystem /home/andy/src/zig/lib/compiler/aro/include -fno-PIE -fPIC -gdwarf-4 -gdwarf32 -fno-lto -MD -MV -MF /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h.d -fhosted -nostdinc -D__GLIBC_MINOR__=39 -isystem /home/andy/src/zig/lib/include -isystem /nix/store/1k8f0lkj10gsbb1fh0idr1423l37yj69-glibc-2.40-218-dev/include -O0 -target x86_64-linux.6.18.9...6.18.9-gnu.2.39 -mcpu=znver4-16bit_mode-32bit_mode-3dnow-3dnowa+64bit+adx+aes+allow_light_256_bit-amx_avx512-amx_bf16-amx_complex-amx_fp16-amx_fp8-amx_int8-amx_movrs-amx_tf32-amx_tile-amx_transpose+avx-avx10_1-avx10_2+avx2+avx512bf16+avx512bitalg+avx512bw+avx512cd+avx512dq-avx512er+avx512f-avx512fp16+avx512ifma-avx512pf+avx512vbmi+avx512vbmi2+avx512vl+avx512vnni-avx512vp2intersect+avx512vpopcntdq-avxifma-avxneconvert-avxvnni-avxvnniint16-avxvnniint8+bmi+bmi2-branch_hint+branchfusion-bsf_bsr_0_clobbers_result-ccmp-cf-cldemote+clflushopt+clwb+clzero+cmov-cmpccxadd+crc32+cx16+cx8-egpr-enqcmd-ermsb+evex512+f16c-false_deps_getmant-false_deps_lzcnt_tzcnt-false_deps_mulc-false_deps_mullq-false_deps_perm-false_deps_popcnt-false_deps_range-fast_11bytenop+fast_15bytenop-fast_7bytenop+fast_bextr+fast_dpwssd-fast_gather-fast_hops+fast_imm16+fast_lzcnt+fast_movbe+fast_scalar_fsqrt+fast_scalar_shift_masks-fast_shld_rotate-fast_variable_crosslane_shuffle+fast_variable_perlane_shuffle+fast_vector_fsqrt-fast_vector_shift_masks-faster_shift_than_shuffle+fma-fma4+fsgsbase+fsrm+fxsr+gfni-harden_sls_ijmp-harden_sls_ret-hreset-idivl_to_divb+idivq_to_divl-inline_asm_use_gpr32+invpcid-kl-lea_sp-lea_uses_ag-lvi_cfi-lvi_load_hardening-lwp+lzcnt+macrofusion+mmx+movbe-movdir64b-movdiri-movrs+mwaitx-ndd-nf-no_bypass_delay-no_bypass_delay_blend-no_bypass_delay_mov-no_bypass_delay_shuffle+nopl-pad_short_functions+pclmul-pconfig+pku+popcnt-ppx-prefer_128_bit-prefer_256_bit-prefer_mask_registers-prefer_movmsk_over_vtest-prefer_no_gather-prefer_no_scatter-prefetchi-prefetchwt1+prfchw-ptwrite-push2pop2-raoint+rdpid+rdpru+rdrnd+rdseed-retpoline-retpoline_external_thunk-retpoline_indirect_branches-retpoline_indirect_calls-rtm+sahf+sbb_dep_breaking-serialize-seses-sgx+sha-sha512+shstk-slow_3ops_lea-slow_incdec-slow_lea-slow_pmaddwd-slow_pmulld-slow_shld-slow_two_mem_ops-slow_unaligned_mem_16-slow_unaligned_mem_32-sm3-sm4+smap+smep-soft_float+sse+sse2+sse3+sse4_1+sse4_2+sse4a-sse_unaligned_mem+ssse3-tagged_globals-tbm-tsxldtrk-tuning_fast_imm_vector_shift-uintr-use_glm_div_sqrt_costs-use_slm_arith_costs-usermsr+vaes+vpclmulqdq+vzeroupper-waitpkg+wbnoinvd-widekl+x87-xop+xsave+xsavec+xsaveopt+xsaves-zu /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h -o /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.zig
info(compilation): processing dep file at /home/andy/src/zig/.zig-cache/tmp/34a9bf8b6692601c/cimport.h.d
info(compilation): renaming tmp/34a9bf8b6692601c to o/590f08cd17612e54b9a188a9dec151be
$ ./verbose_cimport_flag
cimport.h содержит файл для перевода (сформированный вызовами @cInclude, @cDefine и @cUndef), cimport.h.d — список файлов-зависимостей, а cimport.zig содержит результат перевода.
Некоторые конструкции C невозможно перевести на Zig — например, goto, структуры с битовыми полями и макросы со склеиванием токенов. Zig использует понижение, чтобы продолжать перевод при наличии непереводимых сущностей.
Понижение бывает трёх видов: opaque, extern и @compileError. Структуры и объединения C, которые невозможно корректно перевести, будут преобразованы в opaque{}. Функции, содержащие непрозрачные типы или непереводимые конструкции кода, будут понижены до объявлений extern. Таким образом, непереводимые типы по-прежнему можно использовать как указатели, а непереводимые функции — вызывать, если компоновщику известна скомпилированная функция.
@compileError используется, когда определения верхнего уровня (глобальные переменные, прототипы функций, макросы) невозможно перевести или понизить. Поскольку Zig использует ленивый анализ объявлений верхнего уровня, непереводимые сущности не вызовут ошибку компиляции в вашем коде, пока вы действительно не используете их.
Средство перевода C пытается по возможности преобразовать макросы, похожие на функции, в эквивалентные функции Zig. Поскольку макросы C работают на уровне лексических токенов, не все макросы C можно перевести на Zig. Макросы, которые невозможно перевести, будут понижены до @compileError. Обратите внимание: код C, который использует макросы, переводится без дополнительных проблем (поскольку 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 преобразуется в необязательный указатель, при нулевом адресе возникает проверяемое на безопасность недопустимое поведение.
Допускает нулевой адрес. На целевых платформах, отличных от freestanding, разыменование нулевого адреса является проверяемым на безопасность недопустимым поведением. Опциональные указатели 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;
}
if (builtin.cpu.arch == .s390x) {
// https://github.com/ziglang/zig/issues/21350#issuecomment-3543006475
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 — экспорт библиотеки с ABI C, к которой могут обращаться другие языки программирования. Ключевое слово 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 без дополнительной настройки.
Freestanding
Для хост-сред, таких как веб-браузер и nodejs, собирайте исполняемый файл, указав целевую ОС freestanding. Ниже приведён пример запуска кода Zig, скомпилированного в WebAssembly, с помощью nodejs.
Более интересный пример — получение из среды выполнения списка предварительно открытых каталогов. Теперь стандартная библиотека поддерживает это с помощью std.fs.wasi.Preopens:
Целевая платформа — это компьютер, на котором будет запускаться исполняемый файл. Она определяется архитектурой ЦП, набором включённых функций ЦП, операционной системой, минимальной и максимальной версиями операционной системы, ABI и версией ABI.
Zig — язык программирования общего назначения, то есть он предназначен для создания оптимального кода для широкого набора целевых платформ. Команда zig targets выводит сведения обо всех целевых платформах, известных компилятору.
Если компилятору не передан параметр целевой платформы, по умолчанию выбирается хост-компьютер, а значит, полученный исполняемый файл не подойдёт для переноса на другой компьютер. Чтобы перенести исполняемый файл на другой компьютер, компилятору нужно сообщить требования целевой платформы с помощью параметра -target.
Стандартная библиотека Zig (@import("std")) содержит кроссплатформенные абстракции, благодаря которым один и тот же исходный код можно использовать на многих целевых платформах. Одни фрагменты кода более переносимы, чем другие. В целом код Zig чрезвычайно переносим по сравнению с кодом на других языках программирования.
Для работы кроссплатформенных абстракций Zig на каждой платформе требуются собственные реализации. Эти реализации находятся на разных стадиях готовности. Каждый помеченный выпуск компилятора сопровождается примечаниями к выпуску с полной таблицей поддержки целевых платформ.
Руководство по стилю
Эти соглашения о написании кода не проверяются компилятором, однако они включены в эту документацию и поставляются вместе с компилятором, чтобы служить ориентиром для тех, кто захочет сослаться на авторитетный источник согласованных правил оформления кода на Zig.
Избегайте избыточности в именах
Избегайте следующих слов в именах типов:
Value
Data
Context
Manager
State
utils, misc или чьи-либо инициалы
Всё является значением, все типы — данными, всё представляет собой контекст, а вся логика управляет состоянием. Слова, применимые ко всем типам, ничего не сообщают.
Желание использовать слова «utilities», «miscellaneous» или чьи-либо инициалы говорит о неспособности классифицировать сущности или, что бывает чаще, о чрезмерной классификации. Такие объявления могут находиться в корне нуждающегося в них модуля, без отдельного пространства имён.
Избегайте избыточных имён в полных пространствах имён
Компилятор присваивает каждому объявлению полное пространство имён, формируя древовидную структуру. Выбирайте имена с учётом полного пространства имён и избегайте избыточных сегментов в именах.
В этом примере «json» повторяется в полном пространстве имён. Решение — удалить Json из JsonValue. В этом примере у нас есть пустая структура с именем json, однако помните, что файлы также являются частью полного пространства имён.
Этот пример представляет собой исключение из правила, описанного в разделе Избегайте избыточности в именах. Значение типа сведено к его сути: это значение json. Более конкретное имя было бы неверным.
Не используйте подчёркивания в начале имён
В некоторых языках программирования принято добавлять подчёркивания в начало идентификаторов _like_this, чтобы избежать конфликтов с ключевыми словами или совпадений имён либо указать дополнительные метаданные, связанные с использованием идентификатора, например приватность, наличие сложных инвариантов данных, исключение из правил семантического версионирования или значение отражения типов в определённом контексте.
В Zig нет приватных полей, и это руководство по стилю не рекомендует делать вид, будто они есть. Вместо этого поля следует тщательно именовать в соответствии с их семантикой, а в документации указывать, как использовать поля, не нарушая инварианты данных. Если к полю применяются иные правила семантического версионирования, чем ко всему остальному, это исключение следует отметить в комментарии документации.
Что касается отражения типов, использование системы типов менее подвержено ошибкам и более удобно в сопровождении, чем попытки придать смысл именам полей.
Подчёркивание не помогает объяснить различие между двумя одинаковыми во всём остальном именами. Если их невозможно перепутать, это руководство рекомендует использовать более развёрнутые имена во внешних областях видимости и более краткие — во внутренних.
Открывающие фигурные скобки размещайте в той же строке, если только не требуется перенос.
Если список содержит более двух элементов, размещайте каждый элемент на отдельной строке и не забывайте, что в конце можно поставить дополнительную запятую.
Длина строки: стремитесь к 100 символам; руководствуйтесь здравым смыслом.
Имена
В общих чертах: camelCaseFunctionName, TitleCaseTypeName, snake_case_variable_name. Точнее:
Если x — это struct без полей, которое никогда не предполагается создавать, то x считается «пространством имён» и должно быть snake_case.
Если x — это type или псевдоним type, то x должно быть TitleCase.
Если x можно вызвать, а возвращаемый тип x — type, то x должно быть TitleCase.
Если x можно вызвать и при этом не выполняется предыдущее условие, то x должно быть camelCase.
В противном случае x должно быть snake_case.
Акронимы, инициальные аббревиатуры, имена собственные и любые другие слова, для которых в письменном английском существуют правила капитализации, подчиняются тем же соглашениям об именовании, что и любые другие слова. Эти соглашения распространяются даже на акронимы длиной всего в две буквы.
Имена файлов делятся на две категории: типы и пространства имён. Если файл (неявно являющийся структурой) содержит поля верхнего уровня, его следует именовать как любую другую структуру с полями, используя 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 PrimitiveTypeAlias = f32;
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 (включая комментарии) запрещены некоторые кодовые точки:
LF (значение байта 0x0a, кодовая точка U+000a, '\n') — это символ конца строки в исходном коде Zig. Это значение байта завершает каждую строку исходного кода Zig, кроме последней строки файла. Рекомендуется завершать непустые исходные файлы пустой строкой, то есть последним байтом должен быть 0x0a (LF).
Перед каждым LF может непосредственно стоять один CR (значение байта 0x0d, кодовая точка U+000d, '\r'), образуя окончание строки в стиле Windows, однако это не рекомендуется. Обратите внимание: в многострочных строках последовательности CRLF при компиляции в программу Zig будут преобразованы в LF. В любом другом контексте 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 можно использовать для указания выравнивания указателя. Его также можно использовать после объявления переменной или функции, чтобы указать выравнивание указателей на эту переменную или функцию.
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.
volatile можно использовать, чтобы указать, что загрузка или сохранение через указатель имеет побочные эффекты. Он также может изменить выражение встроенного ассемблера, обозначив наличие у него побочных эффектов.
Выражение while можно использовать для многократной проверки булева выражения, опционального значения или выражения объединения ошибок и прекращения цикла, когда это выражение соответственно принимает значение false, null или ошибку.
В Zig контейнером называется любая синтаксическая конструкция, выступающая в качестве пространства имен для объявлений переменных и функций. Контейнеры также являются определениями типов, которые можно инстанцировать. Структуры, перечисления, объединения, непрозрачные типы и даже сами исходные файлы Zig являются контейнерами.
Хотя контейнеры (за исключением исходных файлов Zig) заключают определения в фигурные скобки, их не следует путать с блоками или функциями. Контейнеры не содержат операторов.