Переменные, константы и типы данных
Эта глава посвящена фундаментальным концепциям программирования, которые присутствуют почти в каждом языке, но в Zig они реализованы с учетом строгой типизации и упрощения кода. Мы подробно разберём, как объявлять переменные и константы, какие правила их использования действуют в Zig, а также какие встроенные базовые типы данных предоставляет язык.
Эта информация станет основой для дальнейшего изучения Zig и поможет вам писать более безопасный, предсказуемый и эффективный код.
Переменные и константы
В языке Zig переменные и константы используются для хранения данных. Zig поддерживает строгую типизацию и требует явной инициализации переменных перед использованием, что предотвращает многие ошибки на этапе компиляции. Это делает код более предсказуемым и безопасным, так как разработчики не могут случайно использовать неинициализированные переменные.
Объявление переменных
Переменные в языке программирования Zig объявляются с использованием ключевого слова var
. Это означает, что их значение можно изменять в процессе выполнения программы, что делает их удобными для хранения и обработки данных.
При создании переменных в Zig принято использовать так называемую “змеиную нотацию” (snake_case). Это означает, что все слова в имени переменной пишутся с маленькой буквы и разделяются символом подчеркивания (_). Такой стиль именования улучшает читаемость кода и делает его более понятным.
В общем виде объявление переменной выглядит так:
var имя_переменной: тип = значение;
Здесь:
- var — ключевое слово, указывающее, что создается изменяемая переменная.
- имя_переменной — уникальное название переменной, следующее правилам именования.
- тип — тип данных, который будет храниться в переменной. В Zig типизация строгая, поэтому его необходимо указывать явно.
- значение — начальное значение, присваиваемое переменной.
Давайте рассмотрим несколько примеров:
var age: i32 = 0;
var name: []const u8 = "Алиса";
var is_active: bool = true;
В данном примере мы определили три переменных с типами целого числа, строки и булевой переменной, но о типах мы поговорим позднее, а пока давайте остановимся только на определении переменных.
В языке Zig можно объявлять сразу несколько переменных в одной строке, что помогает сделать код более компактным и удобным, особенно если эти переменные логически связаны между собой. Это достигается с помощью последовательного объявления переменных через запятую. Однако важно учитывать, что каждая переменная должна иметь свой тип, если он явно указывается.
Рассмотрим конкретный пример, в котором объявляются три переменные — длина, ширина и площадь, которые могут использоваться, например, для хранения параметров прямоугольника:
var length: u32 = 10, width: u32 = 5, area: u32 = length * width;
Здесь сразу три переменные объявлены в одной строке, что делает код более лаконичным. Однако, несмотря на такую возможность, разработчикам рекомендуется использовать этот подход с осторожностью, чтобы не ухудшить читаемость кода, особенно если переменные имеют сложные и длинные имена.
При объявлении переменной в языке Zig не всегда необходимо явно указывать ее тип. Это возможно благодаря механизму вывода типа (type inference), который автоматически определяет тип переменной на основе ее начального значения. Это делает код более кратким и удобочитаемым, особенно в простых случаях.
var age = 30; // Zig выведет тип переменной как i32
var pi = 3.14159; // Zig выведет тип переменной как f64
var message = "Привет, мир!"; // Zig выведет тип переменной как []const u8
Однако важно понимать, что в некоторых ситуациях такой подход может привести к неожиданным результатам. Например, если присвоить переменной числовое значение без явного указания типа, Zig может интерпретировать его как comptime_int или comptime_float (число, вычисляемое во время компиляции). Если попытаться изменить потом такую переменную, то мы получим ошибку компиляции:
const std = @import("std");
pub fn main() !void {
var age = 0;
std.debug.print("Age: {d}, type: {}\n", .{ age, @TypeOf(age) });
age += 1;
}
# zig build run
run
└─ run vars
└─ zig build-exe vars Debug native 1 errors
src/main.zig:4:9: error: variable of type 'comptime_int' must be const or comptime
var age = 0;
^~~
src/main.zig:4:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type
В этом случае Zig автоматически рассматривает переменную age как comptime_int (число, вычисляемое во время компиляции), а такие переменные не могут изменяться во время выполнения программы.
Такое поведение может быть непривычным для разработчиков, знакомых с другими языками программирования. Однако оно соответствует философии Zig, согласно которой намерения программиста должны быть четко выражены. Чтобы избежать подобных ошибок, рекомендуется явно указывать тип переменной, особенно в больших проектах, где важна предсказуемость кода.
Если мы хотим, чтобы переменная age
могла изменяться во время выполнения программы, необходимо явно указать ее тип:
const std = @import("std");
pub fn main() !void {
var age: i32 = 0;
std.debug.print("Age: {d}, type: {}\n", .{ age, @TypeOf(age) });
age += 1;
}
$ zig build run
Age: 0, type: i32
Важно отметить, что вывод типа переменной может выполнятся даже в более сложных случаях, когда тип переменной может быть выведен из двух переменных разного типа. Давайте рассмотрим следующий код:
const std = @import("std");
pub fn main() void {
const age: u8 = 25;
const child_salary: u8 = 100;
const adult_salary: u16 = 2000;
const total_salary = if (age < 18) child_salary else adult_salary;
std.debug.print("Тип total_salary: {}\n", .{@TypeOf(total_salary)});
}
Данный код выведет:
Тип total_salary: u16
Как мы видим Zig проанализировал наш код и вывел тип переменной total_salary
как u16
. Для печати типа переменной мы использовали встроенную функцию @TypeOf. О ней мы поговорим позднее, а пока нам достаточно знать, что эта функция возвращает тип переменной. Итак, почему же мы видим что тип нашей переменной u16
? Это происходит потому, что тип переменной total_salary
зависит от значения переменной age
, которая имеет тип u8
. В зависимости от значения age
, переменная total_salary
может быть либо u8
, либо u16
. Однако Zig не может определить тип переменной total_salary
без явного значения переменной age
, поэтому он выполняет расширение типа до максимального, который вместит все возможные значения.
Изменяемость переменных
Как уже упоминалось переменные изменяемы и Вы можете менять значение переменной сколько угодно раз, но при этом вы не можете изменять тип переменной. Следующий код при компиляциии выведет ошибку:
const std = @import("std");
pub fn main() void {
var x: i32 = 5;
x = 10;
x += 3;
var y: i32 = 5;
y = 3.14;
}
Выведет:
run
└─ run simple
└─ zig build-exe simple Debug native 1 errors
src/main.zig:9:9: error: fractional component prevents float value '3.14' from coercion to type 'i32'
y = 3.14;
^~~~
Язык программирования Zig обладает строгой системой типов и не выполняет неявное приведение типов, когда это нельзя сделать безопасно. О том, когда такое приведение может быть выполнено, мы поговорим при обсуждении типов данных, а пока вам надо запомнить, что если вам нужно работать с переменными разных типов, вам придется явно указывать их тип или использовать приведение типов вручную.
В языке Zig каждая переменная, которую вы объявляете в своей программе, предполагается изменяемой. Это означает, что вы должны хотя бы один раз изменить её значение в ходе выполнения программы.
Если же переменная остается неизменной, компилятор Zig обнаружит это еще на этапе компиляции и выдаст ошибку. Он предложит вам заменить объявленую переменную на константу (const), поскольку в таком случае её использование будет более корректным. Это помогает писать более чистый и безопасный код, исключая ненужные изменяемые переменные.
Такой подход заставляет разработчика осознанно выбирать между изменяемыми (var) и неизменяемыми (const) значениями, что делает код более предсказуемым и понятным:
const std = @import("std");
pub fn main() void {
var y: i32 = 5;
std.debug.print("Y: {}\n", .{y});
}
$ zig build
install
└─ install hello
└─ zig build-exe hello Debug native 1 errors
src/main.zig:6:9: error: local variable is never mutated
var y: i32 = 5;
^
src/main.zig:6:9: note: consider using 'const'
Инициализация переменных
В языке программирования Zig вы не можете использовать переменную без ее инициализации. Прежде чем использовать такую переменную в коде, её необходимо явно инициализировать. Если этого не сделать, компилятор Zig выдаст ошибку во время компиляции, предотвращая возможные проблемы, связанные с использованием неинициализированных данных.
const std = @import("std");
pub fn main() void {
var x: i32;
std.debug.print("Значение x: {}\n", .{x});
}
В результате мы получим ошибку:
src/main.zig:4:15: error: expected '=', found ';'
var x: i32; // Объявление переменной без инициализации
^
error: the following command failed with 1 compilation errors:
В этом примере переменная x
объявлена, но ей не присвоено начальное значение. Из-за этого компилятор Zig выдаст ошибку и не позволит скомпилировать программу. Чтобы избежать таких ситуаций, Zig требует явного задания начального значения, так как, если по какой-то причине ваш код использует эту переменную, пока она не инициализирована, у вас будет неопределенное поведение и серьезные ошибки в вашей программе. Это довольно важное свойства языка, которое например отличает его от языка Go, где переменные получают начальное значение в зависимости от типа.
Переменные вычисляемые во время компиляции
Zig предлагает интересную возможность — переменные, которые вычисляются во время компиляции, но при этом могут использоваться в местах, где обычные константы не подходят. Это позволяет выполнять сложные вычисления еще до запуска программы, что может значительно ускорить её выполнение. Давайте рассмотрим пример:
fn generateFibonacci(comptime n: usize) [n]u64 {
comptime var fibs: [n]u64 = undefined;
fibs[0] = 1;
fibs[1] = 1;
comptime var i: usize = 2;
while (i < n) : (i += 1) {
fibs[i] = fibs[i - 1] + fibs[i - 2];
}
return fibs;
}
const fib10 = generateFibonacci(10);
Здесь функция generateFibonacci
создает массив из n чисел Фибоначчи. Так как аргумент n
объявлен как comptime, Zig вычислит значения еще во время компиляции. Переменная fib10
будет содержать первые 10 чисел Фибоначчи, и во время выполнения программы они уже будут готовы, что избавляет от необходимости рассчитывать их в реальном времени. Более подробно о коде выполняемом на этапе компиляции мы поговорим позднее, а пока просто отметим, что инициализация переменных и констант может быть в виде довольно сложных выражений.
Константы
В языке Zig константы отличаются от переменных тем, что их значение неизменно и известно еще на этапе компиляции. Это делает код более предсказуемым и безопасным. Для объявления константы используется ключевое слово const
, а общий синтаксис выглядит следующим образом:
const имя_константы: тип = значение;
Давайте рассмотрим несколько примеров:
const gravity: f64 = 9.81;
const greeting = "Welcome to Zig!";
Если вы попытаетесь изменить значение константы после её объявления, компилятор выдаст ошибку:
const std = @import("std");
pub fn main() void {
const gravity: f64 = 9.81;
gravity = 10.0;
}
Выведет:
src/main.zig:6:5: error: cannot assign to constant
gravity = 10.0; // Ошибка! Константы нельзя изменять.
^~~~~~~
error: the following command failed with 1 compilation errors:
Также как и с переменными, при объявлении константы мы можем опустить указание типа, позволив компилятору вывести его за нас:
const GREETING = "Hello, World!"; // Zig выведет тип как *const [13:0]u8
const MAX_THREADS = 8; // Zig выведет тип как comptime_int
Не беспокойтесь, если тип MAX_THREADS (comptime_int) выглядит незнакомым — мы разберем это подробнее позже, когда будем говорить о вычислениях во время компиляции.
Одна из мощных особенностей Zig заключается в том, что все константы вычисляются во время компиляции. Это означает, что вы можете использовать сложные выражения или даже вызовы функций, если они могут быть вычислены во время компиляции. При этом константы вычисляемые на этапе компиляции имеют “ленивую” инициализацию, что означает, что их значение вычисляется только при первом обращении к ним. Давайте рассмотрим пример:
const BUFFER_SIZE = 1024 * 1024; // 1 MB
fn comptime_sqrt(x: f64) f64 {
return @sqrt(x);
}
const SQRT_2 = comptime_sqrt(2.0);
Здесь константа SQRT_2
вычисляется с помощью функции comptime_sqrt
. Так как значение заранее известно, Zig выполнит расчет во время компиляции, и программа просто получит готовый результат.
Использование переменных и констант
В языке Zig каждая объявленая переменная или константа обязательно должна использоваться в коде. Это означает, что если вы объявите переменную, но нигде её не примените — компилятор выдаст ошибку. Это правило помогает поддерживать чистоту кода и предотвращает накопление неиспользуемых переменных, которые могут запутывать разработчика и делать программу сложнее для понимания.
Если переменная или константа не участвует в выражениях и не передается в функции, компилятор Zig сообщит об этом:
const std = @import("std");
pub fn main() void {
const unused_var = 42;
}
Выведет:
src/main.zig:4:11: error: unused local constant
const unused_var = 42;
^~~~~~~~~~
error: the following command failed with 1 compilation errors:
Иногда при написании кода вы можете столкнуться с ситуацией, когда временно удалили часть кода, и Zig начал предупреждать о неиспользуемых переменных. Удаление этих переменных может привести к значительным изменениям в коде. В Zig есть способ избежать таких предупреждений – достаточно присвоить переменную специальной переменной _:
const age = 15;
_ = age;
Когда переменная присваивается _
, Zig считает её ненужной и уничтожает. Это позволяет избежать ошибки компиляции, если переменная не будет использоваться далее в коде. После того как вы присвоили значение _
, использовать переменную снова нельзя. Попытка обратиться к ней приведет к ошибке компиляции
const std = @import("std");
pub fn main() void {
const age = 15;
_ = age;
std.debug.print("{d}\n", .{age + 2});
}
Данный код выведет ошибку:
src/main.zig:5:9: error: pointless discard of local constant
_ = age;
^~~
src/main.zig:7:32: note: used here
std.debug.print("{d}\n", .{age + 2});
^~~
Затенение переменных
Во многих языках программирования можно объявить новую переменную с тем же именем, что и у существующей переменной. В этом случае говорят, что первая переменная «затеняется» второй, то есть вторая переменная — это то, что увидит компилятор, когда вы будете использовать имя переменной. По сути, вторая переменная затеняет первую, принимая любое использование имени переменной на себя до тех пор, пока либо она сама не будет затенена, либо не закончится область видимости.
const x = 10;
{
var x = 20; // Это затеняет внешнюю переменную x
assert(x == 20);
}
assert(x == 10); // Здесь x ссылается на внешнюю переменную
В данном примере, во вложенном блоке переменная x
затеняет переменную x
из внешнего блока. Это означает, что в блоке {}
переменная x
будет иметь значение 20
, а во внешнем блоке переменная x
будет иметь значение 10
.
В языке Zig затенение переменных запрещено, поэтому приведенный код не скомпилируется. Это может показаться непривычным для тех, кто использует такую возможность в других языках программирования. Однако такое ограничение введено, чтобы избежать ошибок и путаницы в коде. Идеология Zig заключается в упрощении и предсказуемости поведения кода, поэтому затенение переменных здесь не допускается.
Комментарии
Прежде чем перейти к рассмотрению типов данных, давайте поговорим о комментариях в Zig. Комментарии в Zig используются для добавления пояснений и описаний к коду, чтобы сделать его более понятным и удобным для чтения. В Zig есть два типа комментариев: однострочные и документирующие.
Для того чтобы добавить в код однострочный комментарий, используйте символ //
. Все, что находится после этого символа, будет игнорироваться компилятором и не будет влиять на выполнение программы.
const x = 42; // Это однострочный комментарий
В Zig нет поддержки многострочных комментариев (/* */
) как в других языках программирования, поэтому если вам нужно оставить многострочный комментарий, вы можете использовать несколько однострочных комментариев.
Документирующие комментарии необходимы для описания функций, методов и других элементов кода, чтобы другие разработчики могли понять, что делает этот код и как его использовать. Для того чтобы добавить документирующий комментарий, используйте символы ///
перед строкой кода, который вы хотите описать:
/// Эта константа представляет возраст человека
const age = 15;
Документирующие комментарии можно добавлять не везде, а только перед определениями функций, структур и переменных. Если попытатся добавить документирующий комментарий в середине выражения или просто вставить в код в пустое пространство, то вы получите ошибку компиляции:
const std = @import("std");
pub fn main() void {}
/// End of file
Данный код выведет:
src/main.zig:5:1: error: unattached documentation comment
/// End of file
^~~~~~~~~~~~~~~
Для того, чтобы сгенерировать документацию по вашему проекту необходимо передать ключ -femit-docs
при компиляции, тогда компилятор создаст для вас папку docs, куда поместит сгенерированную документацию.
Типы данных
Zig — строготипизированный язык программирования, предоставляющий широкий набор встроенных типов данных. Каждое значение в Zig принадлежит к определенному типу данных, который говорит о том, что это за данные, и таким образом язык понимает, как с ними работать. Мы рассмотрим основные скалярные типы данных, которые используются в Zig, а также специальные типы void
, nonreturn
и null
. Скалярный тип представляет одно значение. В языке Zig есть четыре первичных скалярных типа: целочисленный тип, тип чисел с плавающей точкой, булев тип и символьный тип. Возможно, вы встречались с ними в других языках программирования.
Целочисленные типы
Целочисленный тип (integer) — это число без дробной части. Zig поддерживает как знаковые, так и беззнаковые целочисленные типы различных разрядностей. Они определяются с помощью iN (знаковые) и uN (беззнаковые), где N — количество бит в числе. Так например u32
объявление типа указывает, что значение, с которым оно связано, должно быть целым числом без знака (типы целых чисел со знаком начинаются с i вместо u), которое занимает 32 бита памяти. Помимо стандартных привычных всем типов данных таких как u8
, i8
, u16
, i16
, u32
, i32
, u64
, i64
, u128
и i128
Zig также предоставляет типы u1
, i1
, u2
, i2
, u4
, i4
, i47
, u47
, т.е. Zig умеет работать с целыми числами произвольной длины (в битах). Конечно, вам, возможно, такие типы данных никогда не понадобятся, но, вероятно, есть случаи, когда это может быть полезно.
Примеры целочисленных типов:
const a: i32 = -100; // Знаковое 32-битное число
const b: u8 = 255; // Беззнаковое 8-битное число
Каждый вариант со знаком может хранить числа от $ -2 ^ {n - 1} $ до $ 2 ^ {n - 1} - 1 $ включительно, где n — количество битов, которые использует этот вариант. Таким образом, i8 может хранить числа от $ -(2 ^ 7 ) $ до $ 2 ^ 7 - 1 $, что равно значениям от -128 до 127. Варианты без знака могут хранить числа от 0 до $ 2 ^ n - 1 $, поэтому u8 может хранить числа от 0 до $ 2 ^ 8 - 1 $, что равно значениям от 0 до 255.
В языке Zig также существуют архитектурно-зависимые числовые типы — usize
(беззнаковый) и isize
(знаковый). Эти типы предназначены для работы с размерами, индексами и адресами памяти, причем их размер зависит от разрядности архитектуры компьютера, на котором выполняется программа:
- На 64-битной архитектуре (x86_64, ARM64) usize и isize будут 64-битными (8 байт).
- На 32-битной архитектуре (x86, ARM32) их размер будет 32 бита (4 байта).
- На 16-битных или других системах их размер будет соответствовать разрядности адресации памяти.
Типы usize
и isize
делают код более портируемым и оптимизированным для конкретной архитектуры:
var a: usize = 10; // На компьютере с архитектурой x86_x64 размерность типа будет 64 бита
var b: isize = -10;
В Zig нет поддержки литеральных суффиксов, как в других языках программирования, таких как C или C++. Вы всегда должны явно указывать тип переменной либо использовать приведение типов вручную. Однако в Zig есть поддержка префиксов для обозначения различных систем счисления в целочисленных литералах (0x, 0o, 0b). Рассмотрим пример:
const decimal = 42; // Десятичное число
const hex = 0xFF; // Шестнадцатеричное число (255 в десятичной системе)
const octal = 0o77; // Восьмеричное число (63 в десятичной системе)
const binary = 0b1010; // Двоичное число (10 в десятичной системе)
Числовые литералы также могут использовать _
в качестве визуального разделителя для облегчения чтения числа, например 1_000_000, который будет иметь такое же значение, как если бы было задано 1000000.
const large_number: u64 = 1_000_000_000; // 1 миллиард
const binary: u8 = 0b1100_1010; // 202 в десятичной системе
const hex: u32 = 0xFF_FF_FF_FF; // 4294967295 (максимум для u32)
const float: f64 = 3.1415_9265_3589; // Число π с разделением
Когда Вы работает с целочисленными типами определенной размерности, важно учитывать, что может произойти целочисленное переполнение. Допустим, имеется переменная типа u8
, которая может хранить значения от 0 до 255. Если попытаться изменить переменную на значение вне этого диапазона, например, 256, произойдёт целочисленное переполнение. По умолчанию обычные арифметические операции (например, +
, -
, *
, /
) вызывают ошибку компиляции или панику во время выполнения, если происходит переполнение. Однако Zig также предоставляет оберточные операции (%=
) и операции с насыщением (+|=
, -|=
, *|=
) для управления поведением переполнения.
const std = @import("std");
pub fn main() void {
var x: u8 = 255;
x += 1; // Ошибка: переполнение во время выполнения!
}
При использовании обычных арифметических операторов поведение зависит от того, в каком режиме скомпилирована ваша программа. Если вы скомпилировали программу в режиме debug, то при переполнении операции вызовут ошибку компиляции или панику во время выполнения. В режиме release компилятор может оптимизировать код, и поведение становится неопределённым.
Если используется оператор %=
(например, x +%= 1;), то происходит модульное переполнение, при котором значение “перекручивается” к нулю.
const std = @import("std");
pub fn main() void {
var x: u8 = 255;
x +%= 1; // Теперь x станет 0, без ошибки
std.debug.print("x = {}\n", .{x});
}
Выведет:
x = 0
Если нужно избежать переполнения и ограничить значение на границах диапазона, можно использовать операторы +|=
, -|=
, *|=
:
const std = @import("std");
pub fn main() void {
var y: u8 = 250;
y +|= 10; // y станет 255, а не переполнится в 0
std.debug.print("y = {}\n", .{y});
}
Выведет:
y = 255
Это полезно, например, для работы с цветами, где ограничение диапазона 0–255.
Числа с плавающей запятой
Также в Zig есть два примитивных типа для чисел с плавающей запятой, представляющих собой числа с десятичной точкой. Типы с плавающей точкой в Zig - это f32
и f64
, размер которых составляет 32 бита и 64 бита соответственно. Все типы с плавающей запятой являются знаковыми.
Вот пример, демонстрирующий числа с плавающей запятой в действии:
const std = @import("std");
pub fn main() void {
const pi: f32 = 3.14159;
const avogadro: f64 = 6.02214076e23;
std.debug.print("pi = {d:.5}, avogadro = {e}\n", .{pi, avogadro});
}
# zig build run
pi = 3.14159, avogadro = 6.02214076e23
Числа с плавающей запятой представлены в соответствии со стандартом IEEE-754. Тип f32
является плавающей запятой одинарной точности, а f64
- двойной точности.
В стандартной библиотеки Zig вы также можете найти специальные значения для чисел с плавающей запятой, такие как бесконечность и NaN (Not a Number):
const std = @import("std");
pub fn main() void {
const inf: f32 = std.math.inf(f32);
const nan: f32 = std.math.nan(f32);
std.debug.print("inf = {d}, nan = {d}\n", .{inf, nan});
}
# zig build run
inf = inf, nan = nan
Числовые операции
Zig поддерживает основные математические операции, привычные для всех типов чисел: сложение, вычитание, умножение, деление и остаток.
Операция | Оператор | Пример (u8 и i8) |
---|---|---|
Сложение | + | x + y |
Вычитание | - | x - y |
Умножение | * | x * y |
Деление | / | x / y |
Остаток | % | x % y |
Целочисленное деление (/
) в Zig всегда проверяется на деление на ноль и не выполняет автоматическое округление.
const std = @import("std");
pub fn main() void {
const a: i32 = 10;
const b: i32 = 3;
std.debug.print("10 / 3 = {}\n", .{a / b}); // Выведет 3 (без округления)
const x: i32 = 10;
const y: i32 = 0;
const result = x / y; // Ошибка компиляции: division by zero
}
Данный код не скомпилируется, так как Zig на этапе компиляции увидит что происходит деление на ноль и выдаст ошибку. Однако в более сложных случаях, код может скомпилироватся и привести к делению на ноль во время выполнения. В этом случае для приложения собранного в режиме debug деление на ноль вызовет панику, для приложения собранного в режиме release деление на ноль приведет к неопределенной ситуации.
Zig также поддерживает операции деления с округлением с помощью функций @divFloor
, @divTrunc
, @divExact
:
Функция | Описание |
---|---|
@divTrunc(a, b) | Отбрасывает дробную часть (стандартное поведение / ) |
@divFloor(a, b) | Округляет вниз (к наименьшему целому) |
@divExact(a, b) | Делит без остатка, но вызывает ошибку, если a % b != 0 |
Пример:
std.debug.print("divTrunc(10, 3) = {}\n", .{@divTrunc(10, 3)}); // 3
std.debug.print("divFloor(10, 3) = {}\n", .{@divFloor(10, 3)}); // 3
std.debug.print("divFloor(-10, 3) = {}\n", .{@divFloor(-10, 3)}); // -4
Битовые операции
Zig также поддерживает стандартный набор битовых операций, таких как &
, |
, ^
, <<
, >>
, ~
и т.д.
Сдвиг влево сдвигает все биты числа на указанное количество позиций влево. Это эквивалентно умножению на $ 2 ^ n $. Симметрично сдвиг вправо сдвигает все биты числа на указанное количество позиций вправо. Это эквивалентно делению на $ 2 ^ n $.
const std = @import("std");
pub fn main() void {
const a: u8 = 0b00001111; // 15
const b = a << 2; // Сдвиг на 2 позиции влево
std.debug.print("b = {b:0>8}\n", .{b}); // 0b00111100 (60)
}
В Zig по разному происходит операция сдвига для знаковых и беззнаковых типов данных. Так, если мы сдвигаем беззнаковый тип, то свободные биты заполняются 0, а если знаковый, то они заполняются знаковым битом.
const a: u8 = 0b10000000;
const b = a >> 4; // 0b00001000 (8)
const a: i8 = -128; // 0b10000000 (-128)
const b = a >> 3; // 0b11111000 (-16)
В Zig запрещены сдвиги на количество бит больше, чем размер типа. Например следующий код приведет к ошибке:
const a: u8 = 0b00000001;
const b = a << 8; // Ошибка! u8 имеет только 8 бит.
Стандартные битовые операции &
, |
, ^
, ~
в Zig ведут себя аналогично другим языкам:
const a: u8 = 0b1100_1100;
const b: u8 = 0b1010_1010;
const result = a & b; // 0b1000_1000 (0x88)
const a: u8 = 0b1100_1100;
const b: u8 = 0b1010_1010;
const result = a | b; // 0b1110_1110 (0xEE)
const a: u8 = 0b1100_1100;
const b: u8 = 0b1010_1010;
const result = a ^ b; // 0b0110_0110 (0x66)
const a: u8 = 0b1100_1100;
const result = ~a; // 0b0011_0011 (0x33)
Также Вы можете совершать операции над отдельными битами:
Операция | Код | Применение |
---|---|---|
Маскирование | x & mask | Извлечение битов |
Установка бита | `x | (1 « n)` |
Сброс бита | x & ~(1 « n) | Выключение флага |
Инверсия бита | x ^ (1 « n) | Переключение флага |
Проверка бита | (x & (1 « n)) != 0 | Определение состояния |
const flags: u8 = 0b0000_0000;
const new_flags = flags | (1 << 3); // Установить 3-й бит
const flags: u8 = 0b0000_1000;
const new_flags = flags & ~(1 << 3); // Сбросить 3-й бит
const flags: u8 = 0b0000_1000;
const is_set = (flags & (1 << 3)) != 0;
Логический тип данных
Как и в большинстве других языков программирования, логический тип в Zig имеет два возможных значения: true
и false
. Значения логических типов имеют размер в один байт. Логический тип в Zig задаётся с помощью bool
. В отличие от языка C Zig не приводит 0 и 1 (или любые другие числа) к булевому типу bool
автоматически и требуется явное приведение типов, с использованием функций @intToBool
или @boolToInt
. Например:
const is_ready: bool = true;
const is_empty: bool = false;
const flag: bool = 1; // Ошибка: expected type 'bool', found 'comptime_int'
std.debug.print("flag = {}\n", .{flag});
const flag: bool = @intToBool(1); // А вот так будет работать
std.debug.print("flag = {}\n", .{flag});
Символьный тип данных
Zig не имеет отдельного типа char
, но символы могут быть представлены как 8-битные или 21-битные числа (соответствующие кодам ASCII или Unicode). Например:
const std = @import("std");
pub fn main() !void {
const letter: u8 = 'A';
std.debug.print("{}\n", .{letter});
const emoji: u21 = '😻';
std.debug.print("{u}\n", .{emoji});
}
Здесь использование u
при выводе символа emoji
говорит ог том, что мы хотим вывести Unicode символ.
Тип void и noreturn
В Zig существуют два специальных типа: void
и noreturn
, которые используются для разных целей. Тип void
означает, что функция не возвращает полезного значения, но успешно выполняется.
const std = @import("std");
pub fn sayHello() void {
std.debug.print("Hello, Zig!\n", .{}); // Только побочный эффект
}
pub fn main() void {
sayHello(); // Вызываем функцию, она ничего не возвращает
}
Тип noreturn
означает, что функция никогда не завершится “нормально”. Она либо вызовет панику, либо зациклится, либо завершит программу.
pub fn crash() noreturn {
@panic("Something went wrong!"); // Завершает программу
}
pub fn infiniteLoop() noreturn {
while (true) {} // Функция никогда не завершится
}
pub fn main() void {
crash(); // Никогда не вернёт управление
infiniteLoop(); // Никогда не вернёт управление
}
Наиболее часто Вы будете встречать тип void, так как функции которые не возвращают значения довольно распространенная практика. А вот тип noreturn
вы возможно никогда и не встретите.
Опциональный тип данных
В Zig также есть опциональные типы (?T), которые могут содержать либо T, либо null
. Опциональный тип удобен, когда вы не знаете какое начальное значение присвоить переменной в момент ее объявления. В этом случае, вы можете использовать null
как значение по умолчанию, а инициализировать переменную позднее.
const std = @import("std");
pub fn main() void {
const maybe_number: ?i32 = null; // Может быть числом или null
std.debug.print("maybe_number = {?}\n", .{maybe_number});
}
Перед использованием опционального значения нужно проверить, не равно ли оно null
. Есть два способа сделать такую проверку. Наиболее частый это использование условного оператора if
:
if (maybe_number) |num| {
std.debug.print("Число: {}\n", .{num});
} else {
std.debug.print("Значение отсутствует!\n", .{});
}
В этом примере если значение maybe_number
не равно null
, то сработает первый блок нашего условного оператора и значение будет переданно в параметре num
, если же maybe_number
равно null
, то сработает второй блок и будет выведено сообщение “Значение отсутствует!”. Есть также более компактная форма с использованием orelse
:
std.debug.print("Число: {}\n", .{num orelse 0});
Второй вариант проверки это принудительно разыменовать используя конструкцию .?
, но если значение null
, программа завершится с ошибкой.
const value: i32 = maybe_number.?; // ⚠️ Паника, если null!
Приведение и вывод типов
В языке программирования Zig существует два способа приведения типов: явное и неявное. Эти принципы важны для обеспечения безопасности типов и контроля над типами данных, что делает код более предсказуемым и безопасным.
Явное приведение типов
Явное приведение типов в Zig осуществляется с помощью специальных операторов приведения, таких как @intCast
, @intFromFloat
, @floatFromInt
, @intFromBool
, и т.д. Это приведение выполняется во время выполнения программы и используется, когда программист явно указывает, что он хочет преобразовать один тип в другой. Например:
const x: u32 = 42;
const y: u8 = @intCast(x); // Явное приведение типа u32 в u8
Здесь мы явно приводим значение типа u32
в тип u8
с помощью функции @intCast
. В Zig это может быть полезно, когда вам нужно преобразовать типы данных, например, из более широких в более узкие. Если значение типа не вмещается в новый тип, то произойдет паника.
Если Вам все же необходимо сконвертировать более широкий тип в более узкий без паники, то можно использовать функцию @truncate
. Эта функция выполняет усечение значения до указанного типа, не вызывая паники. Эта функция усекает биты из целочисленного типа, в результате чего получается целочисленный тип меньшего или того же размера.
const std = @import("std");
pub fn main() void {
const value: u32 = 2000;
const truncatedValue: u8 = @truncate(value);
std.debug.print("Усеченное значение: {d}\n", .{truncatedValue});
}
Данный код выведет:
Усеченное значение: 208
Функция @intFromFloat
выполнит конвертацию числа с плавающей запятой (f16, f32, f64) в целочисленный (iN, uN) тип путём усечения. Если при этом число выйдет за пределы, то произойдет паника. Функция @floatFromInt
произведет обратное преобразования, но оно в отличии от предыдущего не может привести к панике.
В языке Zig для явного приведения типа во время компиляции есть оператор @as
. Общий синтаксис оператора @as
выглядит так:
@as(Целевой тип, значение)
- Целевой тип: тип, к которому вы хотите привести значение.
- значение: выражение или переменная, которую вы хотите привести.
Этот оператор позволяет привести значение к определенному типу. Однако важно помнить, что при использовании @as
необходимо учитывать, что если приведение невозможно или приводит к ошибке (например, из-за переполнения или потери данных), компилятор выдаст ошибку.
const std = @import("std");
pub fn main() void {
const a: u32 = 42;
const b: u8 = @as(u8, a); // Приведение u32 к u8
std.debug.print("Значение b: {}\n", .{b}); // Вывод значения b
const a: u32 = 500;
const b: u8 = @as(u8, a); // Ошибка: 500 не помещается в u8
std.debug.print("Значение b: {}\n", .{b});
}
Когда вы используете @as
, вы явно сообщаете компилятору, что приведение безопасно в вашем контексте, и если оно не может быть выполнено, компилятор сообщит об ошибке.
Неявное приведение типов
Неявное приведение типов в Zig происходит автоматически в ситуациях, где компилятор сам может безопасно привести один тип в другой без явного указания программистом. Однако важно заметить, что Zig по умолчанию старается избегать неявных приведений типов. Это сделано для того, чтобы избежать потенциальных ошибок, связанных с неожиданными преобразованиями типов.
Пример неявного приведения:
const a: u8 = 5;
const b: u32 = a; // Неявное приведение типа u8 в u32
В этом примере u8
автоматически приводится к u32
без явного указания программиста, потому что u32
— это более широкий тип, и такая операция безопасна. Однако если бы типы были несовместимы или приведение могло привести к потере данных, компилятор бы выдал ошибку.
Вывод типа
В языке программирования Zig можно использовать функцию @TypeOf
, чтобы получить тип переменной или выражения на этапе компиляции. Это полезно, когда нужно получить тип для отладки, или для использования в метапрограммировании (например, для создания универсальных функций или шаблонов).
Пример:
const std = @import("std");
const foo = 42; // переменная типа i32
const bar = 3.14; // переменная типа f64
const foo_type = @TypeOf(foo); // получаем тип переменной foo
const bar_type = @TypeOf(bar); // получаем тип переменной bar
pub fn main() void {
std.debug.print("Тип переменной foo: {}\n", .{foo_type});
std.debug.print("Тип переменной bar: {}\n", .{bar_type});
}
# zig build run
Тип переменной foo: i32
Тип переменной bar: f64
В приведенном примере мы получаем типы переменных foo и bar с помощью @TypeOf
и выводим их через std.debug.print
. Более подробно мы рассмотрим использование @TypeOf
в следующих главах, но так как на данном этапе иногда полезно узнать, что за тип неявно вывел Zig Вы можете узнать это используя @TypeOf
.
Типы этапа компиляции
В zig есть два специальных типа используемых на этапе компиляции comptime_int
и comptime_float
.Эти типы вычисляются на этапе компиляции и не имеют фиксированной разрядности (например, i32
, u64
). Они автоматически преобразуется в нужный тип.
Пример присвоение comptime_int:
const std = @import("std");
pub fn main() void {
const x = 10; // x — comptime_int
const y: i32 = x; // Автоматическое преобразование в i32
std.debug.print("Тип переменной foo: {}\n", .{@TypeOf(x)});
std.debug.print("Тип переменной foo: {}\n", .{@TypeOf(y)});
}
Выведет:
Тип переменной x: comptime_int
Тип переменной y: i32
В данном пример число 10 является comptime_int
, но Zig автоматически приводит его к i32
.
Пример использования comptime_int для вычислений на этапе компиляции:
const x = 5 * 10; // x = comptime_int
const y: u8 = x + 2; // Авто-приведение в u8
Более подробно мы коснемся типов этапа компиляции, когда будем изучать универсальные типы и функции.