#
Глава 5. Функции для эффективного программиста
Вам надоело писать один и тот же код снова и снова? Нравится ли вам азарт отладки одной и той же логики, разбросанной по разным местам в вашей кодовой базе? Если да, то эта глава может изменить ваше мнение. Добро пожаловать в мир функций в Zig — пространство, где вы можете упаковать повторяющийся код в повторно используемые, элегантные блоки, делая ваши программы эффективнее, а вашу жизнь — чуть менее хаотичной.
В этой главе мы отправимся в путешествие по тонкостям функций. Вы научитесь объявлять и определять их, превращая беспорядочные блоки кода в организованные, вызываемые единицы. Мы разберёмся с передачей параметров и обработкой возвращаемых значений, чтобы вы могли делать свои функции настолько динамичными или простыми, насколько это необходимо.
Мы изучим продвинутые возможности, такие как defer, позволяющий изящно освобождать ресурсы. По пути обсудим, как блоки помогают управлять областью видимости и как избегать распространённых ловушек вроде теневого объявления переменных.
К концу этой главы вы сможете:
- определять и вызывать собственные функции в Zig, превращая повторяющийся код в повторно используемые компоненты;
- управлять областью видимости с помощью блоков, чтобы ваши переменные не попадали туда, где им не место;
- эффективно передавать параметры и обрабатывать возвращаемые значения, делая функции гибкими и мощными;
- использовать такие возможности, как defer, для управления ресурсами.
Приготовьтесь и будьте готовы поднять свои навыки программирования на Zig на новый уровень. Кто знает? Возможно, вы даже начнёте получать удовольствие от написания функций — или хотя бы оцените здравомыслие, которое они привносят в вашу кодовую базу.
#
Технические требования
Весь код из этой главы можно найти в каталоге Chapter05 нашего репозитория GitHub.
#
Блоки — держим переменные на коротком поводке
В Zig блоки используются для ограничения области видимости объявлений переменных. Представьте их как маленькие клетки, которые не дают вашим переменным разбежаться по всей программе.
Вот простой пример:
pub fn main() void {
{
var x: i32 = 1;
x += 1;
}
// Попытка обратиться к x здесь приведёт к ошибке
// x += 1; // Ошибка: x здесь не определён
}
Внутри внутреннего блока { ... } мы объявляем переменную x. Как только мы выходим из этого блока, x перестаёт существовать. Попытка использовать x вне его области видимости — всё равно что пытаться воспользоваться просроченной библиотечной картой: ничего не выйдет.
Блоки в Zig нужны не только для области видимости; они могут выступать и как выражения, то есть возвращать значение, как видно в следующем примере:
const std = @import("std");
pub fn main() void {
const result = blk: {
const a = 10;
const b = 20;
break :blk a + b; // Блок вычисляет это выражение
};
std.debug.print("result: {}\n", .{result});
}
В этом примере мы используем помеченный блок blk, чтобы выполнить вычисление и вернуть его значение с помощью оператора break. Блок помечен как blk, что позволяет использовать break :blk для выхода из блока и передачи значения a + b (которое равно 30) как результата:
- внутри блока определяются две константы: a и b со значениями 10 и 20;
- выражение a + b вычисляется как 30, а оператор break :blk a + b завершает блок и возвращает 30 как результат блока;
- сам блок выступает как выражение, возвращающее значение, которое затем присваивается переменной result;
- наконец, с помощью std.debug.print выводится результат — 30.
Это чистый способ инкапсулировать логику внутри блока, выполнить вычисление и вернуть результат. Это как готовить на кухне: блок скрывает промежуточные шаги (a и b), но в итоге выдаёт готовый продукт (result), который и важен снаружи.
#
Затенение — Zig не играет в эти игры
В некоторых языках можно повторно объявить переменную с тем же именем во внутренней области видимости, фактически «затеняя» внешнюю. В Zig так делать нельзя. Он требует оригинальности или хотя бы последовательности в именовании переменных.
Пример:
pub fn main() void {
const pi = 3.14;
{
// var pi: i32 = 1234;
// Ошибка: нельзя переобъявить 'pi'
}
}
Zig выдаст ошибку здесь, потому что вы пытаетесь снова объявить pi во внутренней области видимости. Это помогает сделать код однозначным — в отличие от некоторых ваших соглашений по именованию.
Если вам всё же нужно использовать то же имя (и я настоятельно не советую этого делать), вы можете сделать это только в совершенно разных областях видимости:
const std = @import("std");
pub fn main() void {
{
const value = 42;
std.debug.print("value: {}\n", .{value});
}
{
const value: bool = true;
std.debug.print("value: {}\n", .{value});
}
}
Здесь value объявляется в двух разных областях видимости, и Zig это устраивает, потому что их время жизни не пересекается. Но серьёзно, подумайте о более понятных именах переменных, чтобы избежать путаницы.
Пустые блоки
Пустой блок
{}в Zig эквивалентенvoid{}и вычисляется как void:pub fn main() void { const a = {}; // a имеет тип void const b = void{}; // b тоже имеет тип void // a и b одинаковы // Здесь особо нечего делать, но теперь вы знаете }Не то чтобы у вас часто была необходимость в пустом блоке, но знать об этом полезно — как уметь складывать простыню на резинке.
Пора поднять наш код на новый уровень с помощью настоящих функций.
#
Функции
Теперь, когда мы разобрались с блоками и областями видимости, поговорим о функциях. Представьте функции как именованные блоки, которые могут принимать входные данные (параметры) и возвращать результат (возвращаемые значения). Они позволяют инкапсулировать логику и повторно использовать код без копирования и вставки, как будто на дворе 1999 год.
Вот как определяется простая функция в Zig:
fn add(a: i32, b: i32) i32 {
return a + b;
}
Эта функция add принимает два параметра типа i32 и возвращает их сумму. Это блок кода, который можно вызывать всякий раз, когда нужно сложить два числа, избавляя от необходимости каждый раз писать a + b — каким бы захватывающим это ни было.
#
Параметры — приём входных данных
Параметры функции — это входные данные, необходимые ей для выполнения задачи. В Zig параметры по умолчанию неизменяемы. Это значит, что вы не можете изменять их внутри функции, если только явно не создадите копию. Такой подход делает функции чистыми, а намерения — ясными.
Примитивные типы, такие как целые числа и числа с плавающей запятой, передаются по значению. Функция получает собственную копию данных.
Рассмотрим следующий код:
fn increment(n: i32) i32 {
// n += 1; // Ошибка: нельзя присваивать константе
var result = n + 1;
return result;
}
Попытка напрямую изменить n приведёт к ошибке, потому что n неизменяем. Вместо этого мы создаём новую переменную result, чтобы хранить изменённое значение.
Для сложных типов, таких как массивы и, как вы уже догадались — структуры, Zig может передавать их по значению или по ссылке в зависимости от того, что эффективнее. Вам не нужно об этом беспокоиться: Zig берёт детали на себя.
Примечание
Если вы задаетесь вопросом, что такое структура, не паникуйте. Мы подробно рассмотрим структуры в Главе 8 «Организация данных». А пока просто думайте о структуре как о способе группировать связанные данные.
Продолжая исследование, рассмотрим такой пример:
const Point = struct {
x: i32,
y: i32,
};
fn translate(p: Point, dx: i32, dy: i32) Point {
return Point{ .x = p.x + dx, .y = p.y + dy };
}
Мы берём точку Point, смещаем её координаты и возвращаем новую точку Point. Исходная p остаётся неизменной, что соответствует предпочтению Zig к неизменяемости, если вы явно не решите иначе.
#
Возвращаемые значения — отправка результатов вызывающему
Функции могут возвращать значения вызывающему. Тип возвращаемого значения указывается после списка параметров. Если функция ничего не возвращает, можно опустить тип возврата или указать void:
fn greet() void {
const std = @import("std");
std.debug.print("Hello, world!\n", .{});
}
Чтобы вернуть значение, используйте ключевое слово return, за которым следует нужное значение:
fn multiply(a: i32, b: i32) i32 {
return a * b;
}
Иногда нужно выйти из функции до достижения её конца. Например, если входные данные некорректны или цель достигнута раньше.
Рассмотрим функцию для деления:
fn divide(a: i32, b: i32) ?i32 {
if (b == 0) {
return null; // Деление на ноль невозможно
}
return a / b;
}
В этой функции мы проверяем, равно ли b нулю. Если да — возвращаем null, чтобы обозначить недопустимую операцию. В противном случае выполняем деление.
Посмотрим, как можно использовать эти функции в main:
pub fn main() void {
const sum = add(5, 7); // sum равно 12
const incremented = increment(10); // incremented равно 11
// Использование Point и translate (не волнуйтесь, если вы ещё не знакомы со структурами)
const point = Point{ .x = 3, .y = 4 };
const moved_point = translate(point, 5, -2);
// moved_point — это { x = 8, y = 2 }
greet(); // Выводит "Hello, world!"
const product = multiply(6, 7); // product равно 42
const division_result = divide(10, 2);
if (division_result) |result| {
// result равно 5
// Что-то делаем с результатом
} else {
// Обрабатываем деление на ноль
// Например, выводим сообщение об ошибке
}
}
В этой функции main мы вызываем ранее определённые функции и используем их результаты. Обратите внимание на обработку необязательного результата от divide с помощью оператора if.
Функции в Zig можно присваивать переменным, передавать в качестве параметров и возвращать из других функций. Они — полноправные граждане языка, как и в любом современном языке программирования.
В следующем примере мы передаём функции:
const MathOp = fn (a: i32, b: i32) i32;
fn applyOperation(op: MathOp, a: i32, b: i32) i32 {
return op(a, b);
}
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn subtract(a: i32, b: i32) i32 {
return a - b;
}
pub fn main() void {
const sum = applyOperation(add, 5, 3); // sum равно 8
const difference = applyOperation(subtract, 5, 3); // difference равно 2
std.debug.print("sum: {}, difference: {}\n", .{sum, difference});
}
Здесь applyOperation принимает функцию op в качестве параметра и применяет её к a и b. Это как дать вашей функции другую функцию — «функция в функции», если хотите.
Функции — важнейшие инструменты в арсенале программиста на Zig. Они помогают инкапсулировать логику, способствуют повторному использованию кода и поддерживают порядок в кодовой базе. Понимая работу параметров и возвращаемых значений, вы сможете писать эффективные и понятные функции.
Запомните следующее:
- Параметры по умолчанию неизменяемы! Если нужно их изменить — создавайте новые переменные.
- Zig запрещает теневое объявление переменных. Это делает код понятнее и снижает вероятность ошибок.
- Функции можно использовать как значения. Передавайте их по мере необходимости для создания гибкого и повторно используемого кода.
Теперь идите и пишите функции, которые не только работают, но и создают впечатление, что вы знаете, что делаете — или хотя бы знаете больше того, кто копирует и вставляет код из интернета без понимания.
Теперь, когда вы освоили основы функций в Zig, давайте рассмотрим некоторые продвинутые возможности, которые могут сделать ваш код чище и надёжнее. В частности, мы разберёмся с оператором unreachable и оператором defer. Не волнуйтесь — всё будет просто.
#
Оператор unreachable — утверждение невозможного
Бывало ли у вас, что вы абсолютно уверены: определённый участок кода никогда не должен выполняться? В Zig для этого есть оператор unreachable. Это как сказать компилятору: «Если мы сюда попали, значит, что-то пошло совсем не так».
В режимах Debug и ReleaseSafe достижение оператора unreachable приведёт к панике программы с сообщением «reached unreachable code». В режимах ReleaseFast и ReleaseSmall компилятор считает, что такие пути кода невозможны, и может оптимизировать их соответствующим образом.
Вот базовый пример:
pub fn main() void {
const x = 1;
const y = 2;
if (x + y != 3) {
unreachable; // Мы утверждаем, что этот код никогда не должен быть достигнут
}
// Продолжаем выполнение программы
}
В этом коде мы проверяем, что x + y не равно 3. Поскольку 1 + 2 всегда равно 3, условие ложно, и unreachable не выполняется. Если же по какой-то странной причине x + y не равно 3, программа вызовет панику в отладочных режимах, сигнализируя о невозможном.
Оператор unreachable особенно полезен, когда вы уверены, что определённый путь выполнения невозможен, но компилятор этого не понимает.
Например, рассмотрим такую функцию:
fn safeDivide(a: i32, b: i32) i32 {
if (b != 0) {
return a / b;
} else {
unreachable; // Мы никогда не должны попадать сюда с b == 0
}
}
Вы можете подумать: «Но ведь b может быть нулём!» Да, может. Но, возможно, в логике вашего приложения вы где-то гарантируете, что в safeDivide всегда передаётся ненулевой делитель. Используя unreachable, вы утверждаете: «Дойти до этой точки невозможно».
Оператор unreachable имеет тип noreturn, как и операторы break, continue и return. Это значит, что его можно использовать в выражениях там, где ожидается значение, и компилятор примет это, потому что noreturn совместим со всеми типами.
Пример:
fn getNumber(code: u8) u8 {
return switch (code) {
1 => 42,
2 => 84,
else => unreachable,
};
}
pub fn main() void {
const num = getNumber(1); // num равно 42
// Если code не 1 или 2, программа паникует на unreachable
}
В этом коде:
- выражение switch должно вернуть u8;
- для варианта else мы используем unreachable, потому что уверены: code может быть только 1 или 2;
- так как тип noreturn совместим с u8, компилятор не возражает.
Хотя unreachable — мощный инструмент, его неправильное использование может привести к неопределённому поведению, особенно в режимах ReleaseFast и ReleaseSmall, где оптимизатор исходит из предположения о невозможности таких путей.
Если есть хоть малейшая вероятность выполнения unreachable, и он всё же выполнится — поведение программы в оптимизированных сборках будет неопределённым. Используйте его с умом и только когда абсолютно уверены.
Функция стандартной библиотеки assert реализована через unreachable (см. исходный код). Вот как это выглядит:
pub fn assert(ok: bool) void {
if (!ok) unreachable;
}
Пример использования:
pub fn main() void {
assert(2 + 2 == 4); // Всё хорошо
assert(2 + 2 == 5); // Вызовет панику в отладочных режимах
}
Используя unreachable, функция assert гарантирует: если условие ложно, программа остановится, помогая отловить ошибки на этапе разработки.
К счастью, не каждое решение нужно принимать сразу — часть работы функции можно отложить с помощью оператора defer.
#
Откладывание действий
Хотели когда-нибудь поставить себе напоминание «убрать потом»? В Zig оператор defer позволяет запланировать выполнение кода при выходе из текущей области видимости, независимо от того, как именно это произойдёт. Это как сказать себе: «Я разберусь с этим, когда закончу здесь».
Вот простой пример:
pub fn main() void {
{
var resource = acquireResource();
defer releaseResource(resource);
// Используем ресурс
doSomethingWithResource(resource);
// Как бы мы ни вышли из этого блока, releaseResource будет вызван
}
// На этом этапе ресурс уже освобождён
}
В этом коде вызов releaseResource(resource) гарантированно выполнится при выходе из внутреннего блока — даже если выход будет досрочным. Это обеспечивает корректное освобождение ресурсов и предотвращает утечки.
Допустим, вы работаете с файлом:
fn readFileContents(filename: []const u8) void {
const file = openFile(filename);
defer closeFile(file);
// Читаем из файла
const contents = readAll(file);
// Делаем что-то с содержимым
}
В этом примере closeFile(file) будет вызван при выходе из функции — даже если произойдёт ошибка или будет выполнен досрочный возврат.
Если у вас несколько операторов defer, они выполняются в порядке, обратном их появлению:
pub fn main() void {
defer std.debug.print("First defer\n", .{});
defer std.debug.print("Second defer\n", .{});
std.debug.print("In main function\n", .{});
}
Этот код выведет:
In main function
Second defer
First defer
Это как стопка тарелок: последнюю положенную вы моете первой.
Перед завершением упомянем ключевое слово export. Если вы хотите, чтобы функция была видна за пределами вашего модуля Zig — например, чтобы её можно было вызвать из другого языка или включить в динамическую библиотеку — используйте export:
export fn add(a: i32, b: i32) i32 {
return a + b;
}
Помечая функцию как export, вы говорите Zig: «Сделай эту функцию доступной для внешнего связывания». Это более продвинутая тема; к ней мы ещё вернёмся. Пока просто знайте: export — ваш способ сказать миру: «Эй, посмотрите на эту функцию!»
#
Итоги
В этой главе мы погрузились в мир функций в Zig. Вы научились определять и вызывать функции, управлять областью видимости с помощью блоков, работать с параметрами и возвращаемыми значениями. Мы обсудили неизменяемость параметров функций и то, как Zig предотвращает затенение переменных, чтобы ваш код оставался понятным и (почти) без ошибок.
Мы также рассмотрели некоторые важные возможности:
- unreachable — позволяет утверждать, что определённый код никогда не должен выполняться, что помогает выявлять невозможные сценарии и оптимизировать программу.
- defer — даёт возможность запланировать выполнение кода при выходе из области видимости, гарантируя корректное освобождение ресурсов.
Понимание этих концепций позволяет писать более эффективный, поддерживаемый и надёжный код. Функции помогают инкапсулировать логику и следовать принципу DRY (Don’t Repeat Yourself — «не повторяйся»), делая кодовую базу более организованной.
В следующей главе мы сосредоточимся на тестировании кода. Теперь, когда вы умеете писать функции, важно убедиться, что они работают как задумано. Это следующий логический шаг на пути изучения Zig, который поможет вам создавать надёжное программное обеспечение и быть уверенным в качестве своего кода.
#
Закрепите материал
Вопрос 1: Какова основная цель использования функций в программировании?
- Сделать код длиннее и сложнее
- Инкапсулировать повторно используемую логику и предотвратить повторение
- Запутать других программистов
- Замедлить выполнение программы
Вопрос 2: Какое поведение по умолчанию у параметров функций в Zig относительно изменяемости?
- Они изменяемы и могут быть изменены внутри функции
- Они неизменяемы, если явно не сделаны изменяемыми
- Они изменяемы, но не могут быть переназначены
- Они не могут использоваться внутри функции
Вопрос 3: Какой оператор в Zig используется для планирования выполнения кода при выходе из текущей области видимости?
- break
- continue
- defer
- return
Вопрос 4: Что означает тип noreturn в Zig?
- Функция, возвращающая целое число
- Функция, не возвращающая значение (void)
- Пути кода, которые не возвращают управление обычным образом
- Устаревшая функция в Zig
Вопрос 5: Какое из следующих утверждений о ключевом слове unreachable верно?
- Оно позволяет затенять переменные во внутренних областях видимости
- Оно утверждает, что определённый путь кода никогда не должен выполняться
- Оно делает функцию доступной вне модуля
- Оно используется для объявления бесконечного цикла
Вопрос 6: В Zig переменные, объявленные внутри блока , доступны вне этого блока. (Верно или неверно?)
Вопрос 7: Операторы defer в Zig выполняются в порядке их объявления при выходе из области видимости. (Верно или неверно?)
Вопрос 8: Ключевое слово export используется для того, чтобы сделать функцию доступной для внешнего связывания. (Верно или неверно?)
Вопрос 9: Zig позволяет повторно объявить переменную с тем же именем в перекрывающейся области видимости. (Верно или неверно?)
Вопрос 10: Тип noreturn совместим только с функциями, возвращающими void. (Верно или неверно?)
Вопрос 11: Объясните, почему Zig не позволяет затенять переменные в перекрывающихся областях видимости.
Вопрос 12: Опишите сценарий, в котором использование оператора defer было бы полезным.
Вопрос 13: Как тип noreturn помогает при написании функций, которые могут не возвращать значение?
Вопрос 14: Что происходит при выполнении оператора unreachable в режиме отладки?
Вопрос 15: Почему важно понимать неизменяемость параметров функций в Zig?
Вопрос 16: Дана следующая программа, найдите ошибки и объясните, как их исправить:
pub fn main() void {
const pi = 3.14;
{
var pi: i32 = 1234;
// Что-то делаем с pi
}
}
Вопрос 17: Какой вывод будет у следующего фрагмента кода?
pub fn main() void {
defer std.debug.print("First defer\n", .{});
defer std.debug.print("Second defer\n", .{});
std.debug.print("In main function\n", .{});
}
Вопрос 18: Можно ли напрямую изменить параметр функции внутри функции? Приведите пример кода для обоснования ответа.
#
Ответы
Вопрос 1: b
Вопрос 2: b
Вопрос 3: c
Вопрос 4: c
Вопрос 5: b
Вопрос 6: Неверно
Вопрос 7: Неверно
Вопрос 8: Верно
Вопрос 9: Неверно
Вопрос 10: Неверно
Вопрос 11: Zig запрещает затенение переменных, чтобы избежать неоднозначности и потенциальных ошибок, возникающих из-за использования одного и того же имени для разных переменных в перекрывающихся областях видимости. Это гарантирует, что каждый идентификатор ссылается на одну конкретную переменную в своей области.
Вопрос 12: Использование defer полезно, когда нужно гарантировать, что ресурсы будут корректно освобождены или очищены при выходе из области видимости, например, закрытие файла или освобождение памяти, даже если функция завершается досрочно из-за ошибки или оператора return.
Вопрос 13: Тип noreturn позволяет использовать функции, которые не возвращают управление обычным образом, в выражениях, где ожидается значение. Он сообщает компилятору, что выполнение не продолжится после этой точки, что обеспечивает совместимость типов и анализ потока управления.
Вопрос 14: В режиме отладки выполнение unreachable приводит к панике программы с сообщением «reached unreachable code», что помогает разработчикам отлавливать логические ошибки, когда якобы невозможные пути кода всё же достигаются.
Вопрос 15: Понимание неизменяемости параметров функций важно, потому что это гарантирует, что функции не будут иметь непреднамеренных побочных эффектов, изменяя входные данные, что делает код более предсказуемым и удобным в сопровождении.
Вопрос 16: Ошибка: переменная pi переобъявляется в перекрывающейся области видимости, что Zig не допускает. Чтобы исправить, используйте другое имя переменной.
pub fn main() void {
const pi = 3.14;
{
var radius: i32 = 1234;
// Что-то делаем с radius
}
}
Вопрос 17: Вывод будет следующим:
In main function
Second defer
First defer
Вопрос 18: Нет, вы не можете напрямую изменять параметр функции, потому что параметры по умолчанию неизменяемы. Чтобы изменить значение, нужно создать новую переменную:
fn increment(n: i32) i32 {
var result = n + 1;
return result;
}