Как стать автором
Обновить

10 причуд Zig

Время на прочтение7 мин
Количество просмотров5.9K
Автор оригинала: Karl Seguin

В последнее время в блоге автора выходило несколько публикаций о Zig (http.ziglog.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.

Приходится творчески подходить к выдумыванию новых переменных, так, чтобы они не затеняли уже имеющихся (что зачастую означает пользоваться всё более мудрёными именами).

Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+6
Комментарии7

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань