В последнее время в блоге автора выходило несколько публикаций о Zig (http.zig, log.zig и websocket.zig). Автор полагает, что ему в этой области ещё учиться и учиться, и часто сталкивается с вещами, которые остаются для него удивительными или непонятными. Пожалуй, автор такой не один, поэтому было бы полезно разобрать причуды Zig.
Возможно, для многих читателей некоторые из этих вещей просто очевидны.
1 - const Self = @This()
Если вы начнёте просматривать исходный код Zig, то достаточно скоро увидите:
const Self = @This();
Начнём с того, что, если функция начинается с @, например @This(), то это встроенная функция – такие функции предоставляются компилятором. @This() возвращает тип самого глубокого экземпляра структуры/перечисления/объединение. Например, следующий код выводит в консоль «true»:
const Tea = struct { const Self = @This(); }; pub fn main() !void { // о синтаксисе .{} поговорим позже! // выводит "true" std.debug.print("{}\n", .{Tea == Tea.Self}); }
Такой код часто используется в методе для указания получателя:
const Tea = struct { const Self = @This(); fn drink(self: *Self) void { ... } }
Но, при всей употребительности, такой вариант применения — поверхностный. Мы могли с тем же успехом написать: fn drink(self: *Tea) void {...}.
Такой вариант по-настоящему полезен в случаях, когда у нас есть анонимная структура:
fn beverage() type { return struct { full: bool = true, const Self = @This(); fn drink(self: *Self) void { self.full = false; } }; } pub fn main() !void { // beverage() возвращает тип, экземпляр которого создаётся при помощи {} // "full" по умолчанию имеет значение "true", поэтому здесь нам не обязательно его указывать var b = beverage(){}; std.debug.print("Full? {}\n", .{b.full}); b.drink(); std.debug.print("Full? {}\n", .{b.full}); }
Этот код выведет в консоль «true», а затем «false».
Этот пример надуманный: зачем же нам может понадобиться здесь анонимная структура? Не должна, но именно на таком подходе основана реализация дженериков в Zig. Для работы с дженериками нам понадобится код, похожий на вышеприведённый (плюс передача типа type для нашей функции). Поэтому нам понадобится @This(), чтобы ссылаться на анонимную структуру изнутри анонимной структуры.
Файлы — это структуры
В Zig файлы — это структуры.
Допустим, нам нужна структура Tea. Мы могли бы создать файл под названием «tea.zig» и добавить следующий код:
// содержимое tea.zig pub const Tea = struct{ full: bool = true, const Self = @This(); pub fn drink(self: *Self) void { self.full = false; } };
Тогда вызывающая сторона может использовать нашу структуру Tea примерно так:
const tea = @import("tea.zig"); ... var t = tea.Tea{};
Либо с такими косметическими изменениями:
// если мы будем использовать эту структуру Tea только из tea.zig, // то, может быть, предпочтительнее будет поступить так. const Tea = @import("tea.zig").Tea; ... var t = Tea{};
Поскольку файлы — это структуры, наш Tea, фактически, вложен в неявно созданную структуру-файл. В качестве альтернативы полное содержимое tea.zig может быть таким:
full: bool = true, const Self = @This(); pub fn drink(self: *Self) void { self.full = false; }
Что можно импортировать так:
const Tea = @import("tea.zig");
Выглядит странно, но, если вообразить, что содержимое обёрнуто в pub const default = struct { ... }; то смысл просматривается. При первой встрече с таким кодом можно всерьёз запутаться.
3 – Соглашения об именованиях
Вообще:
Функции записываются в верблюжьем регистре (camelCase)
Типы записываются в Паскаль-регистре (PascalCase)
Переменные записываются в змеином регистре (lowercase_with_underscores)
Основное исключение из этого правила — функции, возвращающие типы (чаще всего используются с дженериками). Они записываются в PascalCase.
Имена файлов, как правило, записываются в змеином регистре (lowercase_with_underscore). Но те файлы, которые предоставляют тип напрямую (как в нашем последнем чайном примере), следуют тому же соглашению об именовании, что и типы. Следовательно, файл должен был бы называться «Tea.zig».
Таких правил придерживаться легко, но они более цветистые, чем вы могли привыкнуть.
4 — .{...}
В коде Zig то и дело попадается .{...}. Это анонимная структура. Следующий код компилируется и выводит в консоль "keemun"::
pub fn main() !void { const tea = .{.type = "keemun"}; std.debug.print("{s}\n", .{tea.type}); }
На самом деле, в этом примере 2 анонимные структуры. Первая — та, которую мы присвоили переменной tea. Другая — это второй параметр, который мы передали print: т.e. {tea.type}. Вторая версия — это особый тип анонимной структуры с неявными именами полей. Имена полей — «0», «1», «2», ... в Zig это называется «кортеж». Можно проверить неявные имена полей, обратившись к ним напрямую:
pub fn main() !void { const tea = .{"keemun", 10}; std.debug.print("Type: {s}, Quality: {d}\n", .{tea.@"0", tea.@"1"}); }
Синтаксис @"0" необходим, поскольку 0 и 1 не являются стандартными идентификаторами (т.е., они не начинаются с буквы) и, следовательно, должны заключаться в кавычки.
Также можно встретить синтаксис с .{...} в тех случаях, когда структура может быть выведена. Как правило, такое происходит в функции init некоторой структуры:
pub const Tea = struct { full: bool, const Self = @This(); fn init() Self { // тип структуры выводится по возвращаемому типу функции return .{ .full = true, }; } };
Также обратите внимание, какой здесь параметр функции:
var server = httpz.Server().init(allocator, .{});
Второй параметр httpz.Config , и он выводим средствами Zig. В Zig требуется, чтобы каждое поле было инициализировано, но в httpz.Config заданы значения по умолчанию для каждого поля, поэтому пустой инициализатор структуры вполне подойдёт. Также можно явно указать одно или более полей:
var server = httpz.Server().init(allocator, .{.port = 5040});
В Zig .{...} словно сообщает компилятору: сделай, чтобы это поместилось.
5 — .field = значение
В вышеприведённом коде мы пользовались .full = true и .port = 5040. Именно так задаются поля, когда инициализируется структура. Не знаю, намеренно ли это было сделано, но, в принципе, согласуется с тем, как вообще устанавливаются поля.
Думаю, следующий пример демонстрирует, почему синтаксис .field = value резонен:
var tea = Tea{.full = true}; // эй, посмотрите, ну ведь похоже! tea.full = false;
6 — Приватные поля в структурах
Что касается полей структур — известно, что они всегда публичные. Структуры и функции по умолчанию приватные, и существует опция сделать их публичными. Но поля структур могут быть только публичными. Рекомендуется документировать допустимые/правильные варианты использования каждого поля.
Не хотелось бы пускаться в этом посте в лишние разглагольствования, но такая ситуация уже привела к вполне ожидаемым проблемам, а в мире 1.x их количество, скорее всего, только возрастёт.
7 — const *tmp
До версии Zig 0.10 первая строка этого кода:
const r = std.rand.DefaultPrng.init(0).random(); std.debug.print("{d}\n", .{r.uintAtMost(u16, 1000)});
была бы эквивалентна
var t = std.rand.DefaultPrng.init(0); const r = t.random();
Но в 0.10 и выше первая из приведённых строк эквивалентна:
const t = std.rand.DefaultPrng.init(0); const r = t.random();
Обратите внимание, что t превратилась из var в const. Эта разница важна, так как для random() требуется изменяемое значение. Иными словами, код в оригинальном виде более работать не будет. Вы получите ошибку, в которой будет сообщено, что программа рассчитывала на *rand.Xoshiro256, а вместо этого нашла *const rand.Xoshiro256. Чтобы этот код заработал, его оригинальный вариант нужно разделить и явно ввести временную переменную как var:
var t = std.rand.DefaultPrng.init(0); const r = t.random();
8 — comptime_int
В Zig есть мощная фича «comptime», позволяющая разработчикам делать многие вещи во время компиляции. Логично предположить, что выполнение во время компиляции применимо только с теми данными, которые известны во время компиляции. Для поддержки таких операций в Zig предусмотрены типы comptime_int и comptime_float. Рассмотрим следующий пример:
var x = 0; while (true) { if (someCondition()) break; x += 2; }
Этот код не скомпилируется. Тип x выводится как comptime_int, поскольку значение 0 известно во время компиляции. Проблема здесь заключается в том, что comptime_int обязано быть const. Конечно же, если изменить объявление на const x = 0; то мы получим другую ошибку, так как в данном случае попытаемся приплюсовать 2 к const.
Решение: явно определить x как usize (или другой целочисленный тип для времени выполнения, например, u64):
var x: usize = 0;
9 — std.testing.expectEqual
Возможно, первый написанный вами тест приведёт к удивительной ошибке компиляции. Рассмотрим код:
fn add(a: i64, b: i64) i64 { return a + b; } test "add" { try std.testing.expectEqual(5, add(2, 3)); }
Если я покажу вам сигнатуру expectEqual, можете ли вы объяснить, почему она не скомпилируется?
pub fn expectEqual(expected: anytype, actual: @TypeOf(expected)) !void
Может быть, это и сложно уловить, но «фактическое» значение принудительно приводится к тому же типу, что и «ожидаемое». Приведённый выше тест на «сложение» не скомпилируется, поскольку i64 невозможно принудительно привести к comptime_int.
Есть простое решение — поменять параметры:
test "add" { try std.testing.expectEqual(add(2, 3), 5); }
И это работает, и многие так поступают. Основной недостаток такого подхода в том, что в сообщении об отказе перемешиваются ожидаемые и фактические значения.
Вот как правильно решается этот случай: ожидаемое значение приводится к фактическому типу, с использованием встроенного @as():
test "add" { try std.testing.expectEqual(@as(i64, 5), add(2, 3)); }
Возвращаемое значение get равно ?[]const u8, а это опциональная (она же сводимая к нулю) строка. Но ожидаемое значение [верно] равно нулю, и ?[]const u8 невозможно принудительно привести к null. Чтобы исправить это, необходимо принудительно привести null к ?[]const u8:
try std.testing.expectEqual(@as(?[]const u8, null), m.get("teg"));
10 — Затенение
В документации Zig постулировано, что «Идентификаторам никогда не разрешается «скрывать» другие идентификаторы, прибегая к одноимённости». Поэтому если в верхней части файла у вас есть const reader = @import("reader.zig");, то в том же файле больше не может быть ничего под названием reader.
Приходится творчески подходить к выдумыванию новых переменных, так, чтобы они не затеняли уже имеющихся (что зачастую означает пользоваться всё более мудрёными именами).
