
Не думаю, что за мою 45-летнюю карьеру какой-то другой язык удивлял меня сильнее, чем Zig. Могу с уверенностью сказать, что Zig — это не только новый язык программирования, но и, на мой взгляд, совершенно новый способ написания программ. Задача этого языка — далеко не только замена C или C++.
В этой статье я расскажу о фичах, показавшихся мне самыми привлекательными в Zig, а также вкратце опишу их. Моя цель — представить простые фичи, которые позволят программистам быстро приступить к работе с языком. Однако имейте в виду, что у него есть ещё множество других фич, влияющих на его популярность в нашей отрасли.
Компилятор
Он компилирует C и выполняет кросскомпиляцию
Наверно, самое невероятное достоинство компилятора Zig — способность компиляции кода на C. Оно связано с возможностью кросскомпиляции кода для запуска его на архитектуре, отличающейся от архитектуры машины, на которой он компилировался изначально, и это уже само по себе уникальная особенность. Уже лишь эти фичи, изначально доступные в языке, невероятно сильно повлияли на отрасль. Впрочем, мы будем в первую очередь говорить о том, как писать программы на Zig, и почему следует выбрать его вместо какого-то другого языка.
Установка компилятора
Установить компилятор достаточно просто. На странице скачивания Zinglang можно найти компилятор во множестве разных форматов для разных процессоров и операционных систем:

Например, в случае Windows 10 следует выбрать zip-файл x86_64 и скопировать его распакованное содержимое в нужную папку, скажем, в Program Files. Я изменил имя корневой папки на zig-windows-x86_64, потому что благодаря этому можно просто скопировать другую версию компилятора без необходимости изменения пути в переменной окружения Path.
Дальше нужно добавить путь к этой корневой папке в переменную окружения Path в «Дополнительных свойствах системы» (нажать на 1-4, вставить путь в 5 и нажать на OK в 6-8):

Сразу после указания пути к корневой папке Zig уже можно использовать компилятор в режиме CLI. Благодаря своей простоте это рекомендуемый способ использования.
Собираем программу «Hello World!» на Zig
Рекомендуется собирать программу «Hello World!» по инструкциям из раздела «Getting Started» сайта руководства. Также там представлена альтернативная процедура установки, в том числе для MacOS и Linux (крайне рекомендуется использовать «Installing manually»). Также рекомендуется изучить раздел «Language Basics» для более глубокого понимания языка Zig.
Основные концепции и команды
В приведённых ниже разделах будет представлено общее описание языка Zig. Их задача — позволить программисту научиться быстро приступить к программированию на Zig, освоив лишь некоторые базовые концепции и команды.
Далее представлено краткое описание того, как собирать программы и тестовые модули.
В конце приводится более глубокая информация о низкоуровневом программировании на Zig с более подробным описанием примеров.
Объявление переменных
Объявления переменных на Zig потенциально могут состоять из трёх частей. Первая часть содержит степень доступности (pub или ничего), за ней идёт слово var или const, а затем имя переменной. Вторая часть, отделённая от первой двоеточием, содержит объявление типа переменной. Третья часть — это инициализация переменной. В Zig обязательны только первая и третья части, что довольно непривычно для людей, программирующих на Java или C. Можно задаться вопросом, как же компилятор определяет тип переменной. В данном случае тип выводится из инициализации.
var sum : usize = 0; // объявление переменной из трёх частей
Переменные, для которых степень видимости не указана явным образом как pub, являются локальными для модуля, то есть доступ к ним невозможен снаружи файла исходников, в котором она объявлена (точно так же, как для переменных static в C). Не рекомендуется объявлять переменные, как pub, и рекомендуется создавать в модуле лишь небольшое количество функций, объявленных, как pub, чтобы снизить связанность и повысить целостность. Функции pub ведут себя, как API модуля.
Struct, анонимные struct и тестовые блоки
В Zig символ .{, закрываемый } — это литерал анонимной struct. Анонимные struct в основном используются для инициализации элементов другой структуры или для создания новой структуры с инициализированными элементами. .{ } — это пустой литерал анонимной struct. Слово struct, за которым идёт {, закрывающийся } — это объявление struct. Можно инициализировать переменную с типом, действующую в качестве псевдонима типа. Обратите внимание на тестовый блок, позволяющий компилировать и исполнять тесты без необходимости исполняемого файла.

Битовые поля
Битовые поля (bitfield) — это объявляемые поля с типами, имеющие конкретные размеры в packed struct. Указатели могут указывать на конкретное битовое поле следующим образом:

Циклы For
Синтаксис Zig понятнее, чем у C, только там, где обычно используется [0..8], в нём указывается открытый интервал: [0..9). Преимущество Zig: объявление типа i, её инициализация, проверка и инкремент выполняются автоматически.

Массивы
[_] определяет массив неизвестного размера. За ним должен следовать тип элементов (в данном случае u8, unsigned byte) и его инициализация между { и }. В показанном ниже примере инициализация гласит, что это массив неизвестного размера ([_]), в котором каждый элемент имеет тип u8 (unsigned byte) и инициализирован нулями ({0} ** 81). Стоит отметить, что размер также выводится из показателя повторения (81) инициализации ({0}).
var grid = [_]u8{0} ** 81;
На иллюстрации ниже показано тестовое окружение с циклом, взаимодействующим с массивом и складывающим его элементы. Конструкция try expect проверяет корректность sum.
Слово byte — это не тип и не зарезервированное слово в Zig. Здесь это переменная для хранения каждого из элементов массива на каждом шаге цикла. Переменная, объявленная между двумя | с массивом между скобками цикла for, всегда считается имеющей тот же тип, что и элементы массива.
Также стоит отметить, что usize — это естественный unsigned integer для платформы. Это значит, что на 64-битных машинах он имеет размер u64, а на 32-битных — u32.

Указатели на множественные элементы в Zig
Указатели на массивы могут использовать арифметику указателей, только если указатель явным образом объявлен как указатель на множественные элементы, в данном случае [*]const i32. Обратите внимание, что массив является const, то есть не может быть изменён, но указатель является var.

Разыменование указателей
В случае привязки к адресу отдельной позиции массива указатель невозможно изменять арифметикой указателей. В данном случае, const предотвращает лишь изменение значения другой прямой привязкой к адресу. Для разыменования указателей в Zig используется ptr.*, как показано ниже:

Break с метками
Zig может выполнять множество операций на этапе компиляции. Например, давайте инициализируем массив. Здесь применяется break с меткой. Блок помечен : после своего имени init и значение возвращается из блока при помощи break.

Этот синтаксис может показаться запутанным. 0.. означает бесконечный диапазон, начинающийся с нуля. В for переменные pt и i инициализуются адресом массива initial_value и нулём. В течение цикла инкремент обеих выполняется автоматически, и цикл останавливается сразу после последней позиции массива. Также обратите внимание, как разыменовывается указатель pt: pt.*.
Любопытен и способ объявления массива в блоке с меткой. Массив называется initial_value и содержит 10 позиций типа Point (объявленного позже). В Zig переменные объявляются обязательно. Явным образом можно отказаться от инициализации при помощи зарезервированного слова undefined.
Функции в Zig
Функции в Zig объявляются при помощи fn; по умолчанию они являются статическими (не могут импортироваться в другие файлы) в файле, где были объявлены, за исключением случаев, когда перед fn идёт pub. Функцию можно «встроить». Указателям функций предшествует const, а за ними идёт прототип функции.

ООП в Zig
Struct могут иметь функции. Ниже реализован простой стек. Этот стек объявляется и используется только внутри модуля, где он был определён; он получает доступ к другими структурам данных, не относящимся к нашему примеру. Стек может содержать не больше 81 элемента (как указано в объявлении stk), каждый с типом StkNode. Учтите, что операторов ++ и -- в Zig нет. Вместо них следует использовать эквиваленты += и –=. Указатель стека — это просто integer, используемый в качестве индекса массива stk.
Обратите внимание, что указатель self (self — это не зарезервированное имя, а стандартное наименование) не передаётся в явном виде как параметр, как можно было бы ожидать. Косвенно подразумевается, что это указатель на экземпляр стека, из которого вызывается функция. В примере ниже извлечение из стека можно вызвать при помощи stack.pop();. В этом случае указатель self соответствует указателю на stack. Этот указатель частично эквивалентен this в Java или C++.
Функция init() — это конструктор стека.
Обратите также внимание, что функции pop и push «встроены».

Сборка и исполнение программ на Zig
Сборка исполняемого файла
Для сборки исполняемой программы необходима функция main, которая, как и в программах на Java или C, обозначает точку входа в программу. Если эта функция существует, множество модулей можно скомпилировать в код исполняемого файла. Простая программа может содержать функцию main в том же файле, что и остальная часть программы. Во многих случаях также можно вставить функцию main в конец модуля, чтобы создать исполняемый файл для отдельной отладки модуля. После завершения отладки эту функцию можно закомментировать.
В таких ситуациях можно использовать следующую команду для компиляции program.zig и генерации исполняемой программы (в Windows это program.exe):
zig build-exe -O ReleaseFast program.zig
Чтобы избежать опечаток, команду можно сохранить в пакетный файл.
Исполнение тестовых блоков в модуле
Наверно, это самая лучшая фича Zig как языка программирования. Это окружение обычно используется для тестирования, но в нём можно и прототипировать.
Блок в Zig схож с блоком в C или Java, то есть это некий код между { и }. Тестовый блок — это блок, начинающийся с test "message" { и заканчивающийся на }, где "message" — это строка, содержащая сообщение, отображаемое при выполнении теста (в данном случае только слово message).
Тестовые блоки исполняются независимо от исполняемого файла. Готовый исполняемый файл не включает в себя тесты. Тестовые блоки в конкретном module.zig компилируются вместе со всем кодом этого файла и выполняются следующей командой:
zig test module.zig
Пример
В качестве реального примера покажем тестовый блок из модуля example.zig:
test " => testing set and print functions" {
set(
"800000000003600000070090200" ++
"050007000000045700000100030" ++
"001000068008500010090000400"
);
std.debug.print("\n" ++
"===================\n" ++
"Input Grid\n" ++
"===================\n",
.{}
);
print();
}
Обратите внимание, что в example.zig нет функции main, а потому он не может сгенерировать исполняемый файл, но его тестовый блок можно выполнить следующей командой:
zig test example.zig
Как и написано в сообщении, показанный выше тестовый блок тестирует функции set и print. Как видно из кода, set передаёт в качестве параметра строку десятичных цифр, за которой следует print (выводящий заголовок «Input Grid» ), за которым следует вызов функции print.
Результат выполнения будет следующим:

Вывод в Zig
Давайте обратим внимание на строку std.debug.print. Это вызов функции print в debug.zig в стандартной библиотеке Zig std. Первый параметр — это форматирующая строка, а вторая — анонимная struct (перед которой стоит точка), содержащая список переменных, отображаемых при помощи форматирующей строки. Так как в форматирующей строке формат отсутствует, struct пуста. Так и выполняется форматированный вывод. В данном случае он по умолчанию отобразится в stderr.
Всё это выглядит, как код на C, но здесь есть фундаментальное отличие. В C функция printf динамически интерпретирует форматирующую строку во время исполнения, а в Zig можно работать с литеральной строкой и списком переменных во время компиляции. Поначалу этот принцип трудно уяснить. Многое можно исполнять во время компиляции.
Отладка исполняемого файла
Работа с отладчиком обычно непроста, если в IDE не интегрирован отладчик (например, как в IDE Java наподобие Eclipse и Intellij IDEA) или отсутствуют интегрированные комплекты разработки (наподобие w64devkit для C/C++).
Большое неудобство при работе с отладчиками заключается в том, что необходимо интегрировать символы, что не только раздувает код бесполезной для программы информацией, но и требует компиляции в режиме отладки, при которой генерируется гораздо менее эффективный исполняемый код. Люди, имеющие опыт в сложных системах, знают, что это может быть очень длительным процессом.
Для устранения подобных проблем в Zig есть довольно удобный «хак»:
Встроенная @breakpoint
Когда исполняемый файл запускается в отладчике, эта встроенная функция останавливает программу в точке, где в исходный код вставлен @breakpoint();. Это полезная фича для отладки оптимизированного кода на Zig без необходимости символов.
Достаточно лишь трассировать переменные, за которыми нужно наблюдать, при помощи std.debug.print сразу после @breakpoint(); Благодаря этому мы сможем знать, каковы значения переменных в данный момент.
Пример
В качестве примера сгенерируем исполняемый файл для модуля debug_example.zig, содержащего следующую функцию main:
pub fn main() !void {
set(
"800000000003600000070090200" ++
"050007000000045700000100030" ++
"001000068008500010090000400"
);
}
Чтобы дважды выполнить сравнение с результатами example.zig, параметр, передаваемый функции set в этой main , совпадает со строкой, передаваемой set в тестовом блоке в example.zig, но на этот раз в функцию set вставляется следующий код:
if (c != 0) {
print();
std.debug.print(
"Current digit {}\nposition in string {}\n" ++
"line {}\ncolumn {}\ncode {b}\n",
.{c, k, i, j, code}
);
@breakpoint();
}
Можно сгенерировать исполняемый файл debug_example.exe при помощи следующей команды:
zig build-exe debug_example.zig
Дальше мы используем отладчик для вызова debug_example.exe. В этом случае я использовал gdb — отладчик C/C++ из w64devkit, но это мог быть любой отладчик для исполняемых программ. Внутри gdb необходимо запустить программу при помощи команды r и после этого нажать Enter. Обратите внимание, что программа вывела сетку с содержимым, а также переменными, остановившись в ожидаемом месте. Если затем ввести команду c и нажать на Enter, то продолжится трассировка содержимого сетки и переменных. После этого можно просто продолжать нажимать Enter (при этом повторяется последняя команда, то есть в данном случае c). После многократного нажатия на Enter до завершения программы значения в сетке будут соответствовать значениям, показанным в тестовом блоке в example.zig, потому что оба примера передают set одинаковый параметр.

Низкоуровневое программирование на Zig
Ознакомившись с введением и примерами, вы уже можете приступать к написанию общих программ на Zig. Ниже представлен более глубокий анализ интересных низкоуровневых фич языка, уже использованных в примерах.
Смысл показанных выше примеров заключается в создании модуля, задающего (инициализирующего) и отображающего сетку 9x9. В этой матрице будет храниться сетка судоку, то есть в ней будут содержаться только десятичные цифры. Инициализация сетки должна гарантировать, что сетка удовлетворяет условиям игры в судоку, то есть не содержит ошибок.
В то же время это будет отличная возможность показать низкоуровневые фичи Zig (по крайней мере, самые выдающиеся), и наши примеры хорошо для этого подходят.
Гипотеза, лежащая в основе этих примеров, заключается в том, чтобы представить цифру сетки в качестве бита в позиции, заданного её значением. Это представление достаточно удобно для определения того, присутствует ли уже цифра в сетке (это основное правило сеток судоку). При всём этом, она будет зашифрована так, что её можно использовать только внутри модуля.
Представление матрицы
Чтобы значения легко понимались человеком, цифры хранятся в матрице как стандартные целые u8. Хоть входная сетка в примерах задаётся в строковом формате, символы ASCII преобразуются в целые u8. Хранение цифр в сетке организовано линейно, строка за строкой, в массиве с 81 позицией, называемом в примерах grid:
var grid = [_]u8{0} ** 81; // Линейно хранящаяся сетка судоку
Для проверки корректности сетки необходимо получать доступ к элементам по соответствующей строке и столбцу. Иными словами, необходимо получать доступ к элементам в матрице. Стратегия заключается в создании массива указателей с 9 позициями, каждая из которых указывает на начало каждой строки сетки. Блоки кода в принципе не могут возвращать значение, но в Zig это возможно благодаря break с метками:
const matrix = fill9x9: { // массив матрицы, обеспечивающий доступ
var m : [9][*]u8 = undefined; // к элементу сетки, как матрицы,
var pt : [*]u8 = &grid; // то есть: элемент = matrix[i][j]
for (0..9) |i| { //
m[i] = pt; // хранит указатели на каждую строку
pt += 9; // в каждой позиции матрицы
} //
break :fill9x9 m; // инициализирует матрицу с помощью m
};
В конце цикла m возвращается снаружи блока при помощи команды break :fill9x9 m;. Следует учесть, что fill9x9 соответствует имени метки, расположенной сразу после начала блока.
Если i и j — это строка и столбец элемента, то к любому элементу сетки можно получить доступ при помощи следующей записи:
element = matrix[i][j]
Представление десятичных цифр в виде битов
Ключевая концепция, используемая здесь — это замена целочисленной десятичной цифры i целочисленным code:
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
Иными словами, единственный бит code, которому присваивается 1 — это бит в позиции i-1, если i находится между 1 и 9, в противном случае все биты code равны нулю.
Значения code для каждой цифры
В таблице ниже показаны отношения между цифрами и их двоичным представлением:

Вычисление code в Zig
Значение code вычисляется в функции set при помощи оператора сдвига влево, только если c не равно нулю:
code = @as(u9,1) << (c-1);
Чтобы операция могла скомпилироваться, и результат операции мог быть привязан к переменной, в Zig константы должны иметь нужный размер. В данном случае, code имеет объявленный тип u9. Это ещё одно фундаментальное качество Zig — возможность наличия переменных с произвольным битовым размером. Как видно из таблицы выше, максимальное значение code равно 256, а для его представления нужно не менее 9 битов. Встроенная функция @as позволяет привести константу 1 к типу u9.
Представление сетки при помощи битовых полей
Представив цифры в виде битов, можно отобразить сетку гораздо более простыми способами.
Сетка битовых полей строк
Массив lines отражает всю сетку, представляя каждую строку 9-битным integer, где каждый бит представляет десятичную цифру, которую можно представить в строке:
var lines = [_]u9{0} ** 9;
Благодаря этому, просто получая доступ к строке i элемента в сетке 9x9, можно понять, присутствует ли конкретная цифра в этой строке, выполнив побитовое и ( & ) c code цифры:
lines[i] & code
Если результат этой операции равен нулю, то цифра ещё не представлена в строке i. Противоположное означает наличие дубликата.
Сетка битовых полей столбцов
Массив columns отражает всю сетку, представляя каждый столбец как 9-битный integer:
var columns = [_]u9{0} ** 9;
Благодаря этому для проверки наличия конкретной цифры в столбце j достаточно простого доступа к этому массиву с этим столбцом элемента в сетке 9x9. Для этого нужно всего лишь выполнить побитовое и ( & ) с code цифры:
columns[i] & code
Если результат этой операции равен нулю, это означает, что цифры ещё нет в столбце j . Противоположное означает наличие дубликата.
Правила судоку
Предположим, что у нас есть пустая сетка судоку, как это было, когда мы заполняли в примерах сетку входной строкой. Новая цифра, вставляемая в пустой элемент, не должна присутствовать во всей строке, столбце или ячейке, содержащей новый элемент.
Допустим, что эта сетка уже инициализирована:

Ячейка — это каждая из девяти сеток 3x3, ограниченных жирными линиями. Здесь важно понять, что каждый элемент в сетке 9x9 имеет уникальную строку, столбец и ячейку, содержащую этот элемент.
Например, первая ячейка сетки содержит значения 3, 5, 6, 8 и 9. Следовательно, в ней отсутствуют значения 1, 2, 4 и 7. Предположим, что мы хотим поместить значение 7 в одно из пустых мест первой ячейки. Очевидно, что нельзя поместить его в единственный пустой элемент первой строки, потому что в этой строке уже присутствует 7. Нельзя её разместить и в единственном пустом месте первого столбца, потому что 7 уже есть в этом столбце. Можно поместить 7 только в один из двух пустых элементов второй строки. Но мы не можем знать точно, какой из них подходящий.
Давайте теперь изучим вторую ячейку, содержащую значения 1, 5, 7 и 9. Мы видим, что единственный возможный элемент в этой ячейке, куда можно поместить 8 — первая строка в пустой позиции справа от значения 7.
Массивы lines и columns проверяют наличие дубликатов в строках и столбцах. Значит, требуется новый массив для проверки дубликатов в ячейках.
Сетка битовых полей ячеек
Массив cells отражает всю сетку, представляя каждую ячейку в виде integer из 9 бит:
var cells = [_]u9{0} ** 9; // все элементы каждой ячейки
И здесь всё становится сложнее. Нельзя получить доступ к cells непосредственно при помощи строки или столбца. Было бы проще, если бы мы могли получать доступ к cells, как к матрице 3x3. Это можно сделать имитацией того, что мы сделали для матрицы 9x9, то есть заполнив массив cell следующим образом:
const cell = fill3x3: { // массив cell обеспечивает доступ
var m : [3][*]u9 = undefined; // к элементам ячейки, как к матрице,
var pt : [*]u9 = &cells; // cell[cindx[i]][cindx[j]]
for (0..3) |i| { //
m[i] = pt; // сохраняет указатель на каждую строку
pt += 3; // в каждой позиции ячейки
} //
break :fill3x3 m; // инициализирует ячейку с помощью m
}; //
Но теперь нужно определить строку и столбец в матрице cell по строке и столбцу элемента в исходной сетке 9x9. Можно использовать целочисленное деление, чтобы поделить строку и столбец на 3 и получить индексы, но известно, что операция деления медленная. Два деления усугубляют ситуацию. Можно использовать следующий массив, чтобы представить результат деления:
const cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 };
Благодаря этому, просто получив доступ к этой матрице со строкой i и столбцом j элемента в сетке 9x9, можно понять, присутствует ли конкретная цифра в ячейке этого элемента, выполнив побитовое и ( & ) c code цифры:
cell[cindx[i]][cindx[j]] & code
Если результат этой операции равен нулю, то цифра ещё не присутствует в ячейке, в противном случае есть дубликат.
Проверка дубликатов элемента
Полная проверка наличия дубликатов элемента выполняется сочетанием побитового или ( | ) всех предыдущих элементов в той же строке, столбце и ячейке с последующим побитовым и ( & ) с code элемента:
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
Если результат равен нулю, значит, элемент пока не существует в этой строке, столбце или ячейке. Если результат ненулевой, то программа останавливает работу, потому что пытается выполнить команду unreacheable. Это простейший способ явного обозначения ошибки исполнения в Zig. Обратите внимание, что код в функции set также выводит подробности о том, где произошла ошибка.
Например, если в строке, передаваемой set, заменить '0' сразу после первой '8' на '5', то при тестировании example.zig будет выведена следующая ошибка:

Это вызвано тем, что в столбце 1 уже есть значение 5 в строке 3, как и написано в сообщении об ошибке. Причина ошибки заключается в панике, вызванной попаданием в недостижимый код в функции set.
Изменение структур данных
В функции set двойной цикл for строка за строкой копирует каждый новый элемент из входной строки s в сетку, как показано ниже (в переменной k хранится индекс нового входящего символа в строке s):
for ( 0..9 ) |i| {
line = matrix[i];
for ( 0..9 ) |j| {
c = @intCast(s[k]-'0');
if (c != 0) {
code = @as(u9,1) << (c-1);
⋮ // остальная часть кода
}
line[j] = c;
k+= 1;
}
}
Символ преобразуется в u4 (переменная c) вычитанием из него '0'. Если новый элемент, вставляемый в сетку, не равен нулю ( c != 0 ), то code (вычисленный командой сдвига влево) копируется в каждую из сеток выполнением побитового или ( |= ) с соответствующей сеткой:
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
Никакой явной проверки того, что значение c находится в интервале от 1 до 9, не требуется, потому что при выполнении операции сдвига произойдёт переполнение. Например, заменив '0' сразу после первой '8' входной строки на ':' в строке, передаваемой set, мы получим при тестировании example.zig следующую ошибку:

Если заменить тот же '0' на '/', то возникнет схожая ошибка исполнения. Программа будет работать, только если все значения находятся в интервале от 1 до 9, то есть если входная сетка содержит только десятичные цифры.
В вебе во многих сетках судоку '0' также обозначается как '.'. Именно поэтому в функции set есть следующая строка:
if (s[k] == '.') c = 0;
Это позволяет удобно обойти операцию сдвига, потому что значение c равно нулю.
Прототипирование и устойчивость
Принудительные ошибки, показанные в двух предыдущих разделах, демонстрируют важные фичи Zig, которые могли бы оказаться незамеченными. Одна из них — это устойчивость Zig. Как мы видели, в случае операции сдвига не допускается никакое ошибочное поведение, и ситуация перехватывается во время исполнения.
Кто-то может подумать, что все усилия по развитию Zig направлены на эффективность, но это типичный случай того, как производительностью жертвуют в пользу устойчивости. Если ваша главная цель — производительность, это может показаться спорным выбором. Например, в C потеря бита при операции сдвига — это проблема программиста, зато это повышает производительность определённой ассемблерной команды.
Ещё одна фича, продемонстрированная пару разделов назад — возможность использования тестовых блоков для прототипирования. Потенциал этой фичи огромен, хотя в нашем примере она используется лишь для отладки определённых ситуаций в случаях ошибок.
Уже сами по себе эти фичи демонстрируют большую мощь, редко встречающуюся в языках программирования, и в особенности в компилируемых.
Заключение
Всё это довольно неожиданно и заставляет меня задуматься о том, что множество преимуществ, которые ранее встречались только в интерпретируемых языках, постепенно мигрируют в компилируемые языки с целью обеспечения повышенной производительности.
Схожесть Zig с интерпретируемыми языками потрясает, и в особенности его концепция исполнения во время компиляции, к сожалению, рассмотренная в этой статье недостаточно подробно. Этот аспект Zig, с одной стороны, делает его уникальным и мощным, с другой стороны, усложняет его освоение.
В основном я сделал упор на знакомство с основами и простыми аспектами Zig, благодаря которым он настолько крут; однако существуют и другие довольно впечатляющие аспекты.
Показанные в статье примеры — это упрощённые версии более сложных программ для решения судоку, написанных также на Java и C. Документация в репозитории подробно объясняет большинство структур и алгоритмов, использованных для решения задачи.
Комментарии (92)

andreymal
12.11.2025 13:47программа останавливает работу, потому что пытается выполнить команду
unreacheable. Это простейший способ явного обозначения ошибки исполнения в Zig.При компиляции в режиме ReleaseFast программа НЕ останавливает работу, в результате чего полностью ломается и начинает творить дичь
Из этого можно сделать неутешительный вывод, что сам автор оригинала не знает даже Zig (не говоря уже о R*кхе-кхе*)

andreymal
12.11.2025 13:47Никакой явной проверки того, что значение
cнаходится в интервале от1до9, не требуется, потому что при выполнении операции сдвига произойдёт переполнение.И здесь то же самое: в режиме ReleaseFast переполнение НЕ проверяется, из-за чего sudoku.zig падает с сегфолтом, а example.zig пишет «All 1 tests passed» и не заморачиваясь печатает мусор вместо цифр в Input Grid

kez
12.11.2025 13:47Не думаю, что за мою 45-летнюю карьеру какой-то другой язык удивлял меня сильнее, чем Zig. Могу с уверенностью сказать, что Zig — это не только новый язык программирования, но и, на мой взгляд, совершенно новый способ написания программ.
Иронично что после такого громкого начала идёт перечиследние довольно обыденных вещей, которыми обладает большое количество современных языков.
Откровенно не понимаю, почему эта статья собрала так много голосов на хакер ньюсе.
В зиге действительно есть интересные концепты, хотелось бы сфокусироваться на них.

SkylineXXX
12.11.2025 13:47Я тоже ждал, когда же о чудесах начнется, но все было скучно. Словно ещё один из тысяч языков.

Octagon77
12.11.2025 13:47Чем хороша статья которая перевод? Во первых, не надо цепляться к тексту, как это делается местами выше - разговор то с переводчиком. Во вторых, перевод обычно с буржуинского, а буржуины - мастера искусства продавать. Часто слышу я про Zig, но именно на этот раз я как-то сразу
yay zigи оно мне
5 extra/cargo-zigbuild 0.20.1-1 (1.1 MiB 3.5 MiB) Compile Cargo project with zig as linker 4 extra/zigbee2mqtt 2.6.3-1 (39.4 MiB 196.8 MiB) A Zigbee to MQTT bridge 3 extra/zls 0.15.0-2 (1.2 MiB 3.5 MiB) A language server for Zig 2 extra/zig0.14 0.14.1-2 (20.1 MiB 159.2 MiB) a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software 1 extra/zig 0.15.2-2 (23.8 MiB 186.4 MiB) a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software ==> Packages to install (eg: 1 2 3, 1-3 or ^4) ==> 1 3И тут обращает на себя внимание пакет 5 - я не знаю зачем менять в Cargo линковщик, но Zig точно оказался как химия - широко простёр руки свои в дела человеческие. Поэтому я сразу за телефон и в Termux - многократно уже убедился, что всё интересненькое в пакетах Termux есть, а постылого ничего и нету, рекомендую лайфхак. И там zig да zls как с куста
Так что
$ zig version 0.15.2 $Продано, можно поздравлять автора и переводчика. В Termux, правда, 0.15.1.

domix32
12.11.2025 13:47я не знаю зачем менять в Cargo линковщик,
ну, PR с build.zig во всякие проекты уже достиг уровня меметичности. С учётом того, что zig имеет свой компилятор C видимо удобнее иметь собственный линковщик и наверняка ещё и сборку ускоряет в средах с FFI. Ну, а резонность появления такая же как и у ld-gold, mold, wild - они просто эффективнее занимаются линковкой. Тем более что этот линкощик будет как сабкоманда для карго и никому не мешает.

NeoCode
12.11.2025 13:47Любой новый язык программирования это всегда интересно. А в случае с zig интересно зачем они придумали нестандартный синтаксис составных литералов
.{ }, чем общепринятый последнее время JSON-like синтаксис не устроил?
tenzink
12.11.2025 13:47По стилю почти тоже самое, что и designated initializers, которые есть в C/C++

domix32
12.11.2025 13:47речь видимо за
std.debug.print( "Current digit {}\nposition in string {}\n" ++ "line {}\ncolumn {}\ncode {b}\n", .{c, k, i, j, code} );что-то старого типа питоньего
"{} {}".format(x,y). Видимо до нормальных f-string в zig не добрались - не зря ж оно ещё не 1.0
kez
12.11.2025 13:47Имхо это наименьшая из проблем зига. Раст прожил несколько лет без захвата внешних переменных в
println!и в целом всем было +- всё равно.
domix32
12.11.2025 13:47Собственно, потому оно до сих пор и существует в таком виде - лучше чем prinf синтаксис и ладно.

kez
12.11.2025 13:47Не понял, про какой "вид" речь? Растовый println поддерживает захват переменных с определённой версии
let local_variable = "some"; println!("format {local_variable} arguments");Но это всё вкусовщина, что в питоне, что в расте, что в зиге.

vsting
12.11.2025 13:47А почему Zig, а не V? Zig сложнее и местами смахивает на JS.
V быстрее, проще установка, современный синтаксис, горячая перезагрузка, трансляция в Си, подключение Сишного кода и библиотек прямо в коде на V. Вот язык на который стоит делать обзоры.

Panzerschrek
12.11.2025 13:47Судя по статье, Zig - весьма посредственный язык. Да, пару интересных плюшек там добавили, но не более того. Наибольшая его проблема заключается в том, что он зачем-то сознательно разрабатывается как небезопасный язык (с точки зрения доступа к памяти), хотя уже давно известно, что эту безопасность на уровне языка обеспечить возможно и это не ухудшает производительность и почти не ухудшает эргономику использования.

A_Anonimov
12.11.2025 13:47не ухудшает производительность
Крестовые shared pointer'ы все таки имеют оверхед.
почти не ухудшает эргономику использования
А рустовые ошибки проверки заимствования возникают в самый неподходящий момент.

Panzerschrek
12.11.2025 13:47При чём тут shared_ptr? Он к безопасности памяти отношения не имеет.
ошибки проверки заимствования возникают в самый неподходящий момент
Как раз подходящий. В Rust ошибки вылезают на этапе сборки, если программист где-то накосячил, что даёт возможность из по-быстрому исправить. А в языках без безопасности ошибки возникают во время выполнения, часто очень странным и тяжело-диагностируемым способом.

Siemargl
12.11.2025 13:47нам то не гони (с)
let s = String::from("hello"); my_func(s); println! ("{}", s) ; // нельзя, АПАСНА
Panzerschrek
12.11.2025 13:47Это пример ущербного дизайна Rust, из-за которого некоторые типы молча перемещаются. К безопасности памяти это решение дизайна языка не имеет никакого отношения.
Чинится это, впрочем, весьма тривиально. Надо или s.clone() добавить, или сигнатуру функции my_func изменить.
tenzink
12.11.2025 13:47Не соглашусь про ущербность дизайна. Этот пример как раз таки понятен для любого плюсовика и заставляет определиться забирает ли владение
my_funcили нет. По сути защищает от использования после того, как из объекта помували данные. И в C++ это было бы полезно, но не завезлиstd::string s{"hello"}; my_func(std::move(s)); // `my_func` забирает владение std::print("{}", s); // здесь компилятор C++ молчит, а зряпоэтому здесь самое правильное определиться и сделать так, чтобы my_func владение не забирала
std::string s{"hello"}; my_func(s); // `my_func` принимает const std::string& std::print("{}", s); // всё ok
Panzerschrek
12.11.2025 13:47Перемещение по-умолчанию некоторых типов и проверка на обращение к перемещённым переменным строго говоря ортогональны друг другу. Вполне возможно было бы сделать так, чтобы значения по умолчанию копируются, а для перемещения нужно использовать отдельный оператор языка, при этом сохранив проверку на обращение к объекту, который перемещён. Но авторы Rust решили так не делать и сделали любое копирование явным, видимо из боязни, что неявное копирование может в некоторых случаях к скрытому ухудшению производительности вести.
В C++ просто перемещения в строгом понимании вообще нету. std::move ничего не перемещает и только преобразует один вид ссылки в другой. Вся логика перемещения реализована через конструкторы перемещения или операторы перемещающего присваивания. При этом объект, подвергнутый "перемещению" все ещё существует, деструктор его будет вызван и ряд методов для него будут работать (но не все, там всё хитро с этим).
andreymal
12.11.2025 13:47Не влияющая на суть вкусовщина, лично мне не влом написать .clone() где надо, я бы не стал называть это «ущербным дизайном»

Siemargl
12.11.2025 13:47ну я вот привёл тривиальный пример. там действительно НАДО clone()?
а в реальном коде такой подход превращает простые алгоритмы в гейпорно с импотентами (слабыми ссылками в тч).
реальную же пользу в плане безопасности требование единственности ссылки даёт только в многопоточке

andreymal
12.11.2025 13:47там действительно НАДО clone()?
Да, потому что если строка окажется чуть длиннее чем 5 байт (в моей личной практике строки по несколько мегабайт это норма), её копирование будет ОЧЕНЬ дорогим

Siemargl
12.11.2025 13:47так не надо копировать. все просто.
работа же идёт с константами - компилятору это видно, и более того явно прописано согласно синтаксису раста

andreymal
12.11.2025 13:47Вы целенаправленно сказали компилятору выделить память в куче и скопировали в неё строковый литерал, управление кучей работает естественно в рантайме, никакими константами здесь и не пахнет

Siemargl
12.11.2025 13:47и что дальше, я вот не могу добиться
там не написано my_func(mut s), и в вызове принт тоже

andreymal
12.11.2025 13:47Там написано my_func(s), что означает полную передачу владения строкой (и связанным с ней куском памяти в куче). Мне нужно пересказывать учебник по основам Rust или о чём мы тут вообще?

Siemargl
12.11.2025 13:47мы об
ущербном дизайне Rust
Как верно сказал @Panzerschrek
И ответа "зачем", я доже проблеска не вижу

andreymal
12.11.2025 13:47Зачем что? Про перемещение вместо копирования я уже ответил, про то, что в вашем коде нет констант, я уже тоже разъяснил. Что ещё вам непонятно?

Siemargl
12.11.2025 13:47зачем при константном вызове теряется владение.
в общем я понимаю что это разговор бесполезный
панцер как разработчик компилятора больше в теме

andreymal
12.11.2025 13:47Ещё раз — String::from никогда не был константным. Выделение памяти в куче подразумевает системный вызов к ОС, который не может быть константным даже теоретически. Что вам здесь непонятно?

Siemargl
12.11.2025 13:47Ещё раз — String::from никогда не был константным. Выделение памяти в куче подразумевает системный вызов к ОС, который не может быть константным даже теоретически.
я совсем забыл, что в Расте let дает константу по умолчанию
так что s ещё и константа
прекрасный уровень понимания =)

andreymal
12.11.2025 13:47Константу даёт const, let никогда не был константой. Вам серьёзно стоит прочитать учебник по основам Rust

dimykus
12.11.2025 13:47let (без mut) - дает неизменяемую переменную, что с т.з. плюсовика вполне себе константа.

andreymal
12.11.2025 13:47с т.з. плюсовика
То есть человека, не читавшего учебник по основам Rust?)
let не константа в любом случае, независимо от наличия или отсутствия mut

Siemargl
12.11.2025 13:47Local variables are immutable unless declared otherwise. For example: let mut x = ....
Rust reference 13.2

andreymal
12.11.2025 13:47И где здесь хотя бы одно упоминание константы?
Откройте определение константы в том же Rust reference

Siemargl
12.11.2025 13:47В трактовке Раста константные переменные не существуют. п 6.9
Так что единственный вариант константных переменных, как в большинстве других языков это объявленные let, именуемые тут иммутабельными. Сути не меняет.
Не надо заниматься софистикой.

unC0Rr
12.11.2025 13:47работа же идёт с константами
А что должно произойти, когда вызываемая функция положит эту "константу" куда-нибудь и вернётся, а вызвавшая решит, что строка больше не нужна и освободит память?

Siemargl
12.11.2025 13:47это уже хороший пример
сразу конечно ничего не будет, а в "куда-нибудь" будет невалидная [константная] ссылка.
надо компилятору следить за её лайфтаймом. если локальная - помрёт при возврате из функции, а вот в глобальную нельзя отдавать.

andreymal
12.11.2025 13:47Ну вот Rust и следит, выполняя перемещение
Ссылка на кучу по определению не может быть константной, прекращайте уже

unC0Rr
12.11.2025 13:47в "куда-нибудь" будет невалидная [константная] ссылка
Почему ссылка, мы же саму строку туда сохраняем?
надо компилятору следить за её лайфтаймом.
В Расте уделили внимание тому, что бы не ломать контракт функции, не меняя сигнатуру, а только код. Поэтому лайфтаймы параметров выводятся только исходя из сигнатуры функции или проставляются вручную.
Но вообще, всё проще: если функции нужно владение строкой, то лучше принимать String, если владение не нужно, то &str. А там уже вызывающий код пусть сам определит как ему оптимально поступать: клонировать или отдавать владение готовой строки.

Siemargl
12.11.2025 13:47Почему ссылка, мы же саму строку туда сохраняем?
Мы сохраняем объект типа строка, размером в 24 байта. А вот текстовые данные строки лежат в динамической памяти, на которые ссылка может и принести потом проблем

unC0Rr
12.11.2025 13:47Это называется указатель. Раст даёт гарантии, что "потом проблем" не будет, отсюда и вытекает то решение с перемещением, которое сейчас есть.

Siemargl
12.11.2025 13:47Вообще-то не даёт 100% гарантии. Хотя контроль хороший.
Чтобы этот тонкий момент обойти, утечка памяти в Расте проблемой нарушения памяти (memory safety) не считается =)

Siemargl
12.11.2025 13:47Не хочет за вас гуглить. Варианты редкие но есть, тут на Хабре уже обсуждалось
Впрочем в первоисточнике вот

Siemargl
12.11.2025 13:47Но вообще, всё проще: если функции нужно владение строкой, то лучше принимать String, если владение не нужно, то &str. А там уже вызывающий код пусть сам определит как ему оптимально поступать: клонировать или отдавать владение готовой строки.
Дело не столько в строках, сколько в любых ссылочных типах.
Обычно есть владеющая ссылка, полученная при создании объекта, и потом этот объект кому то передается во владение, хранение итп. И потом нужно с этим объектом работать - хотя бы читать его св-ва. Для этого нужна ещё одна [читающая] ссылка, [а может и больше] , что Растом запрещено. И начинаются пляски либо с клоном в простых случаях, либо же с явными прописываниями Rc::, Arc
И то, что эти пляски нажинаются с примитивного вызова println, удобства разработки не добавляют.
Производительности, кстати, тоже. Rc и/или Arc аллоцируют в куче.
А выгода в редких случаях, например безопасного многопоточного обращения.

andreymal
12.11.2025 13:47ещё одна [читающая] ссылка, [а может и больше] , что Растом запрещено
Я так смотрю, вы до сих пор не взялись читать учебник по основам Rust
Несколько читающих ссылок очень даже разрешены, пока одновременно с ними нет mut-ссылки

andreymal
12.11.2025 13:47А перед этим никаких упоминаний ссылок нет — есть только упоминание владения, а владелец объекта может насоздавать сколько угодно читающих ссылок на объект, пока его не планируется менять
Термина «владеющая ссылка», кстати, не существует

andreymal
12.11.2025 13:47На примере вашего же кода:
let s = String::from("hello");Функция String::from выделяет память в куче, создаёт объект с указателем на кучу (вообще-то в Rust нет объектов, а есть структуры, но да ладно), автоматически становится владельцем этого объекта, но дальше передаёт владение тому, кто вызвал функцию — видимо, владельцем будет функция
main()или куда там вы пихаете свой кодИмейте в виду, что владение означает получение полных прав над объектом, то есть владелец имеет полное право, например, переместить объект из немутабельной переменной в мутабельную
let mut s = s;и менять строку на здоровье (ещё раз, немутабельные переменные это НЕ константы)my_func(s);Функция main передаёт владение объектом в функцию my_func, которая и становится владельцем и получает право делать с объектом что угодно (в том числе пихать в let mut как я показал выше)
Дальше у функции my_func есть три варианта: или передать владение кому-то ещё (видимо в глобальную переменную, потому что больше вроде некуда), или вернуть владение обратно вызывающему (тогда можно будет написать
let s = my_func(s);и владеть строкой снова будет функция main), или ничего никуда не передавать — тогда после завершения функции my_func объект останется без владельца и будет вызван деструктор (функция drop), которая освободит память в куче и сделает хранящийся в объекте указатель на кучу невалиднымprintln! ("{}", s) ;К этому моменту функция main уже перестала быть владельцем значения, хранившегося в старой переменной s, а значит хранящиеся в ней данные могут быть уже невалидны (например, указатель может указывать на освобождённую память в куче) — поэтому Rust абсолютно правильно запрещает использовать старую переменную s, предотвращая use-after-free и прочие страшные штуки

Siemargl
12.11.2025 13:47видимо, владельцем будет функция
main()или куда там вы пихаете свой код..Так владелец переменной функция?
видимо в глобальную переменную, потому что больше вроде некуда
А теперь какая функция?
переместить объект из немутабельной переменной в мутабельную let mut s = s; и менять строку на здоровье (ещё раз, немутабельные переменные это НЕ константы)
Нет, это всего лишь shadowing
Для такого поведения есть целая концепция interior mutability
Но все это все равно не относится к делу о криволапости владения.

andreymal
12.11.2025 13:47Так владелец переменной функция?
Переменная s в функции main, если конкретнее
Нет, это всего лишь shadowing
shadowing старой переменной, а новая переменная становится новым владельцем и получает право мутировать объект, да и shadowing здесь ни при чём, напишите
let mut s2 = s;и получите то же самое (вместе с невозможностью использовать старую переменную s, так как она перестала быть владельцем)Для такого поведения есть целая концепция interior mutability
Ну раз вы в курсе даже про interior mutability, то к чему были все эти свистопляски с якобы константными вызовами
криволапости владения
Я вроде только что продемонстрировал, что владение — ни разу не криволапая штука, успешно защищающая недостаточно внимательного программиста от случайных ошибок при работе с памятью (и немного даже от логических ошибок, хотя не от всех)

Siemargl
12.11.2025 13:47Я вроде только что продемонстрировал, что владение — ни разу не криволапая штука, успешно защищающая недостаточно внимательного программиста от случайных ошибок при работе с памятью (и немного даже от логических ошибок, хотя не от всех)
Она криволапая не в том смысле, что плохо работает, а в том, что доставляет слишком много неудобств.
Можно программисту отрубить обе руки, и он вообще не будет писать программы с ошибками.
Раст, условно, лишает одной руки.

unC0Rr
12.11.2025 13:47владеющая ссылка
Ссылка - это как раз просто ссылка, она не даёт владение объектом. Если владение объектом передаётся куда-то, то ссылок на сам объект в этот момент не может существовать, иначе бы они становились невалидными и нарушали гарантии языка.
Для этого нужна ещё одна [читающая] ссылка, [а может и больше] , что Растом запрещено.
Нет, не запрещено.
пляски либо с клоном в простых случаях
Нельзя подменить доступ к объекту доступом к клону объекта, потому что клон - это уже другой объект, семантика операции получается совсем другой.

Siemargl
12.11.2025 13:47Ссылка не в смысле амперсенд, а имя ссылочной переменной.
let x = Box::new(42); это тоже ссылка в общепринятой терминологии

A_Anonimov
12.11.2025 13:47При чём тут shared_ptr? Он к безопасности памяти отношения не имеет.
Как раз самое прямое отношение имеет. Весь смысл его существования - обеспечить существование подконтрольного объекта до тех пор, пока его можно увидеть, и уничтожить после.

unC0Rr
12.11.2025 13:47Он не может обеспечить ничего, как только берётся первая же ссылка на подконтрольный объект или какое-нибудь поле оного.

domix32
12.11.2025 13:47как небезопасный язык
они в этом плане идут по пути Rust - статический анализ делают частью компилятора. Учитывая, что у них ещё и свой сишный компилятор они и его улучшили. Количество кейсов когда дурной код не скомпилируется на порядок больше - заметили сколько там всяких макросов для той же конвертаций типов. При этом нет того ментального оверхэда который привносит Rust, оставаясь довольно минималистичным на уровне си.

andreymal
12.11.2025 13:47Вон выше в комментах я всего за полчаса тыкания наткнулся на целых два undefined behavior, чего в принципе достаточно, чтобы опровергнуть ваш комментарий

domix32
12.11.2025 13:47Чтобы иметь UB нужно иметь какую-то спеку. Учитывая что языка ещё не 1.0 там половина языка фактически undefined.
Ну и второй момент - ReleaseFast видимо выключает какой-то набор проверок для того чтобы работалось быстрее - аналогично флагу -ffast-math в С++ отключающий комплаенс для floating point значений. Логично, что если не знаешь чем это сулит, то и результат получается соотвествующий. Так что незнаю чего вы там конкретно наопровергали.
Сразу оговорюсь, что не спец по Zig и могу ошибаться в поведении флагов компиляции и моё знание о стабильности различных компонентов языка довольно ограничено.

andreymal
12.11.2025 13:47Ну из всего этого можно сделать вывод, что Zig как минимум пока что не готов для использования
Rust в режиме release (точнее, cargo, но не суть) тоже отключает некоторые проверки, но это не приводит к UB

domix32
12.11.2025 13:47Зависит от того, что вы требуете от языка. От первого компилятора до первого стандарта Си тоже немало времени прошло и компиляторы языка под разные платформы в своё время тоже содержали кучу нюансов и багов, пока наконец не оформили ANSI C стандарт, к которому уже начали стремиться. Кода на С в проде к моменту появления стандарта было уже немало. Так чем ситуация с Zig принципиально иная? Кому-то важна стабильность языка и возможность делать формальную аттестацию по спеке и наворачивать разную верификацию, кому-то и эргономной и эффективной разработки достаточно.

Panzerschrek
12.11.2025 13:47Очень дурной подход - сначала отказаться сознательно от деструкторов, разрешить по поводу и без жонглировать ручным выделением памяти и сырыми указателями и потом затыкать дыры, порождённые таким подходом, некоторым статическим анализатором, пусть и встроенным в компилятор.

domix32
12.11.2025 13:47Очень дурной подход - сначала отказаться сознательно от деструкторов,
А когда их в Zig завозили вообще? Там либо писали собственную функцию удаления как и в Си и при помощи defer организовывали себе RAII - определённо чище чем goto cleanup, либо использовали bump аллокатор, который в конце области видимости очищали - ни тебе фрагментации, ни опасности, что что-то потечёт. Какие проблемы с сырыми указателями у вас в таком формате возникают?

Panzerschrek
12.11.2025 13:47Деструкторов и не завозили, по той причине, что авторы языка Zig - неосиляторы C++, которые воспринимают значительную часть его нововведений в штыки.
В описанном вами подходе граблей всё ещё полно и назвать его "безопасным" - язык не поворачивается. Можно забыть написать удаление ресурсов в defer. Можно дважды удалить ресурс (если он куда-то в другую функцию передаётся, и та тоже вызывает освобождение в своём defer). Можно сохранить указатель ну некую область памяти и освободить её, забыв про этот указатель и разименовав его позже.
defer - вообще костыль, который противоречит базовым принципам структурного программирования. RAII его назвать нельзя, ибо RAII - это управление ресурсами на уровне типов.
Вообще, безопасность, это не "ну вроде норм выглядит", а формальное свойство, гарантируемое дизайном языка. В Zig её нету и авторы даже не пытаются её реализовать.
domix32
12.11.2025 13:47граблей всё ещё полно и назвать его "безопасным"
Они себе и не позиционируют как безопасный. Более безопасная альтернатива Си - да, но это скорее следствие попыток внедриться в часть ниши Си.
Zig is a general-purpose programming language and toolchain for maintaining robust, optimal and reusable software.
Ну а на плюсы взгляд примерно такой же как и у Линуса. C уровнем безопасности Rust или С++ они и не соревновались никогда, просто потому, что по ресурсам они несопоставимы.
Можно забыть написать удаление ресурсов в defer.
Также как и позвать delete в с++ или позвать его на адресе из стека и double-free всё ещё проблема. Параболичность рук в среднем везде одинаково мешает.

blackyblack
12.11.2025 13:47В коде реализации стека какая-то дичь написана. Зачем там matrix? Который к тому же никак не используется.

mbait
12.11.2025 13:47Наверно, самое невероятное достоинство компилятора Zig — способность компиляции кода на C.
Когда вместо этого можно написать GCC или LLVM фронтенд? Так себе достоинство. Скорее говорит, что авторы языка выбирают свой особый путь, либо не особо в теме компиляторов.

domix32
12.11.2025 13:47Как-то странно автор стурктурировал всё. Про улучшенние рефакторинга не рассказал, про обработку ошибок и работу с опционалами тоже не упомянул.

firehacker
12.11.2025 13:47Отвратительный синтаксис; такое впечатление, что создатели новомодных языков соревнуются в том, как бы эффективнее вызвать кровотечение из глаз читающего.
Также при встрече с очередной такой статьей ещё раз проникаешься мыслью, насколько же гениальны были создатели Си, и что лаконичнее уже ничего быть не может.
Все эти fn и двоеточия даром не нужны. Особенно сильно бесят сокращения pub и fn. Это какая жалкая экономия на спичках. Так и представляю себе, как автор языка уделал всех остальных программистов мира в скорости разработки за счет экономии на буквах. Только вот есть один большой просчет. Как же это так, что у нас есть красивый и компактный fn, и при этом остался омерзительный жирный return? Должно быть ret, а ещё лучше просто r. Экономия должна быть экономной. Ну и const нужно сократить до cn.
Спасибо, но я остаюсь на Си, ваш синтаксический сахар меня не усластил.

domix32
12.11.2025 13:47насколько же гениальны были создатели Си, и что лаконичнее уже ничего быть не может.
настолько гениальны, что решили для разминки глаз ввести спиральные объявления.
Особенно сильно бесят сокращения pub и fn.
Надо было вообще как паскале сделать procedure и begin end

VladimirFarshatov
12.11.2025 13:47Ну если typedef писать лениво, то почему и нет-то?
char (signal(int, bool (*callback)(char *, int))); можно развернуть например так:
typedef bool (*Callback)(char *, int);
typedef char (SignalFunc)(int, Callback);
В итоге получаем:
SignalFunc signal;Давно забыл Си, за точность не ручаюсь, но typedef это про оно самое.
domix32
12.11.2025 13:47Главное не запутаться в скобках и кто на кого указатель крошит, ага. И не забыть потом что за void* прячется.

blackyblack
12.11.2025 13:47Ретурны вообще надо было убрать. Чтоб как в расте, какая последняя строчка, такой и ретурн.

iroln
12.11.2025 13:47Да, с ключевым словом, обозначающем функцию, прямо зоопарк в языках программирования. Навскидку вот что вспомнил:
fn, fun, func, function, def, sub, procedure

VladimirFarshatov
12.11.2025 13:4745 лет за компами .. сколько же на моей памяти было этих "убийц Си" и позже С++ .. сбился со счету. И самое смешное где их обльшая часть? )
andreymal
*шёпотом* ...Rust... *подготовка к уворачиванию от тапков*