diff --git a/src/ch04.md b/src/ch04.md index 638aec8..4abebdc 100644 --- a/src/ch04.md +++ b/src/ch04.md @@ -286,8 +286,352 @@ pub const User = struct { ## Немутабельные параметры функций +Уже не раз явно говорилось, что по умолчанию Zig передаёт в функцию копию +значения. Но вскоре мы увидим, что реальность немного более изощрённа +(подсказка - как насчёт более сложных значений с вложенными объектами?) + +Даже если мы будем придерживаться относительно простых типов, правда в +том, что компилятор Zig может передавать параметры так, как ему покажется +более разумным, при условии, что будут соблюдены намерения программиста. +В нашей изначальной `levelUp`, где параметр имел тип `User`, Zig мог +передать как копию значения, так и ссылку на оригинальную переменную +`main.user`, лишь бы было гарантировано, что эта функция ни при каких +обстоятельствах не сможет её изменить. Да, мы хотели её изменить, но то, +что тип параметра `User` (а не `*User`), говорит компилятору о том, что +менять её нельзя. + +Такая свобода позволяет компилятору использовать наиболее оптимальную +стратегию, основываясь на типе параметра. Значения небольших по размеру +типов, как наш `User`, можно без особых затрат передать в виде копии. +Более "крупные" значения, *возможно*, дешевле передать в виде ссылки. Zig +может использовать любой подход, лишь бы, как уже было сказано, +соблюдались бы намерения разработчика. В некоторой степени это возможно +как раз благодаря тому, что параметры функций являются константами. + +Возможно, ВЫ задались вопросом - а каким образом передача по ссылке может +быть медленнее, чем по значению, даже для маленьких структур? Более ясно +мы поймём это далее, но суть в том, что обращение к полям (`user.power`, +например) через указатель добавляет некоторые накладные расходыи поэтому +компилятору приходится оценивать временные затраты на копирование и на +косвенный (то есть через указатель) доступ к полям. + ## Указатель на указатель -## Вложенные указатели +Ранее мы видели, как располагается переменная `user` в памяти. +Если мы передаём ссылку, то картина будет примерно такая: + +``` +main: +user -> ------------ (id: 1043368d0) <--- + | 1 | | + ------------ (power: 1043368d8) | + | 100 | | + ------------ | + | + ............. empty space | + ............. or other data | + | +levelUp: | +user -> ------------- (*User) | + | 1043368d0 |---------------------- + ------------- +``` + +Внутри функции `levelUP` `user` это указатель на структуру `User`. +Значение этого парамметра это адрес переменной `main.user`. Но это не +просто адрес, это ещё и тип (`*User`). Не имеет значения, говорим ли об +указателях или нет - переменные связывают информацию о типе с адресом. +Единственная особенность с указателями состоит в том, что если мы пишем +`user.power`, Zig, зная, что `user` это указтель, автоматически +проследует по этому адресу. Отметим также, что в других языках (C/C++, +например) для доступа к значениям через указатель используется особый +синтаксис. + +Важно понимать, что переменная `user` в функции `levelUp` сама по себе +тоже располагается в памяти по некоторому адресу. Давайте напечатаем вот +так: + +```zig +fn levelUp(user: *User) void { + std.debug.print("{*}\n{*}\n", .{&user, user}); + user.power += 1; +} +``` + +Этот фрагмент напечатает адрес, по которому расположен параметр `user`, в +котором, в свою очередь, содержится адрес переменной `user`, которая +определена в функции `main`. Иными словами, если `user` это `*User`, то +`&user` это `**User`, то есть указатель на указатель на `User`. + +У многоуровнего косвенного доступа (то есть через несколько указателей) +есть свои применения, но прямо сейчас нам это не понадобится. Цель +этого раздела состояла в том, чтобы показать, что указатели не есть +что-то особенное, они, как и другие переменные, имеют значение +(адрес каких-то других переменных) и тип. + +## Указатели в структурах + +До сих пор тип `User` был у нас вполне простой, он содержал в себе +2 целых числа. Визуализировать его представление в памяти достаточно +просто и, когда мы говорим о копировании, не возникает никаких +неоднозначностей. Но что будет происходить, если тип `User` станет +более сложным, например, будет содержать какой-то указатель? + +```zig +pub const User = struct { + id: u64, + power: i32, + name: []const u8, +}; +``` + +Здесь мы добавили поле `name`, которое является срезом. Как мы помним, +срез это пара указатель + длина (количество элементов). Если мы проинициализируем +поле `name` значением `Goku`, то как будет выглядеть экземпляр +такой структуры в памяти? + +```zig +user -> ------------- (id: 1043368d0) + | 1 | + ------------- (power: 1043368d8) + | 100 | + ------------- (name.len: 1043368dc) + | 4 | + ------------- (name.ptr: 1043368e4) + ------| 1182145c0 | + | ------------- + | + | ............. empty space + | ............. or other data + | + ---> ------------- (1182145c0) + | 'G' | + ------------- + | 'o' | + ------------- + | 'k' | + ------------- + | 'u' | + ------------- +``` + +Новое поле (`name`) это срез, который состоит из указателя (`ptr`) и +длины (`len`). Они располагаются в памяти по порядку, наряду с другими +полями. На 64-х битной платформе как указатель, так и длина имеют размер +8 байт. Значение поля `name.ptr` это адрес какого-то другого места в +памяти. + +Типы могут гораздо более сложными, чем в нашем примере по мере того, как +мы будем увеличивать количество уровней вложенности. Однако, вне +зависимости от степени сложности, ведут они себя одинаково. Например, +если мы вернёмся к нашему изначальному коду, где функция `levelUp` +принимает аргумент `User`, то есть работает с копией, то будет выглядеть +картинка размещения в памяти при условии, что мы добавили в структуру +срез (поле `name`)? + +Ответ такой - функция получит так называемую поверхностную копию (shallow copy). +Или, выражаясь иначе, будут скопированы только непосредственно (а не косвенно, +то есть через указатели) адресуемые ячейки памяти. Может показаться, что +`levelUp` получит какую-то "недоделанную" копию переменной `user`, но помните, +что указатель (как `user.name.ptr`) имеет значение (равное адресу) и копия +этого адреса, очевидно есть тот же самый адрес: + +``` +main: user -> ------------- (id: 1043368d0) + | 1 | + ------------- (power: 1043368d8) + | 100 | + ------------- (name.len: 1043368dc) + | 4 | + ------------- (name.ptr: 1043368e4) + | 1182145c0 |------------------------- +levelUp: user -> ------------- (id: 1043368ec) | + | 1 | | + ------------- (power: 1043368f4) | + | 100 | | + ------------- (name.len: 1043368f8) | + | 4 | | + ------------- (name.ptr: 104336900) | + | 1182145c0 |------------------------- + ------------- | + | + ............. empty space | + ............. or other data | + | + ------------- (1182145c0) <--- + | 'G' | + ------------- + | 'o' | + ------------- + | 'k' | + ------------- + | 'u' | + ------------- +``` + +Из этой картинки понятно, что поверхностная копия будет работать как +надо. Поскольку значением указателя является адрес, копирование этого +значения даёт тот же самый адрес. Это имеет серьёзные последствия в плане +возможности поменять значения переменных. Наша функция не может поменять +поля, которые в `main.user` доступны непосредственно, потому что работает +с их копиями, но у неё есть доступ к тому же `name` (через указатель), +поэтому вопрос - может ли она его изменить? В конкретно этом случае - +нет, поскольку `name` это `[]const u8` (константа). Плюс к этому, наше +значение (`Goku`) это буквальное значение, которое всегда иммутабельно. +Однако, не очень сильно изменив код, мы может добиться того, чтобы можно +было изменить поле `name` в функции `levelUp`: + +```zig +const std = @import("std"); + +pub fn main() void { + var name = [4]u8{'G', 'o', 'k', 'u'}; + var user = User{ + .id = 1, + .power = 100, + // slice it, [4]u8 -> []u8 + .name = name[0..], + }; + levelUp(user); + std.debug.print("{s}\n", .{user.name}); +} + +fn levelUp(user: User) void { + user.name[2] = '!'; +} + +pub const User = struct { + id: u64, + power: i32, + // []const u8 -> []u8 + name: []u8 +}; +``` + +Этот код напечатает "Go!u". Нам тут пришлось изменить тип поля `name` с +`[]const u8` на `[]u8` и вместо строкового литерала (который в принципе +нельзя модифицировать) использовать массив и срез от него. Некоторые из +вас, возможно, увидят здесь некоторую нестыковочку. Передача по значению +не даёт функции возможности изменять поля, доступные непосредственно, но +не запрещает изменять поля, доступные по ссылке. Если мы действительно +хотели, чтобы функция не могла менять имя, нам бы тогда следовало описать +как и раньше, то есть как `[]const u8`. а не как `[]u8`. + +В некоторых языках всё это реализуется по другому, но многие языки +работают именно так (ну, или близко к этому). Хотя всё это может +показаться понятным лишь посвящённым, тем не менее, это является основой +для повседневного программирования. Хорошие новости в том, что Вы можете +освоить эту "эзотерику" используя простые примеры и отрывки кода - по +мере того, как сложность остальных частей системы растёт, основы остаются +неизменными и не становятся более сложными. ## Рекурсивные структуры + +Иногда Вам могут потребоваться, чтобы структура была рекурсивной. +Давайте добавим в структуру `User` необязательное поле `manager`. +Мы также создадим 2-х пользователей и назначим одного из них +менеджером другого: + +```zig +const std = @import("std"); + +pub fn main() void { + const leto = User{ + .id = 1, + .power = 9001, + .manager = null, + }; + + const duncan = User{ + .id = 1, + .power = 9001, + .manager = leto, + }; + + std.debug.print("{any}\n{any}", .{leto, duncan}); +} + +pub const User = struct { + id: u64, + power: i32, + manager: ?User, +}; +``` + +Увы, этот код не пройдёт компиляцию, компилятор пожалуется на то, что +структура `User` зависит сама от себя. Так происходит потому, что размеры +всех типов должны быть известны во время компиляции. + +У нас не было такой проблемы, когда мы добавили поле `name`, несмотря на +то, что имена, вообще говоря, могут иметь разную длину. Тут вопрос не в +размерах значений, а в размерах самого типа. Компилятору необходимо такое +знание для того, чтобы делать всё то, о чём мы говорили выше, например, +генерировать код для доступа к полям структуры, исходя из его смещения +относительно начала структуры. Поле `name` было у нас срезом, то есть +имело вполне определённый размер, по 8 байт на указатель и на длину, +всего 16 байт. + +Возможно, Вы подумаете, что это будет проблемой с любым необязательным +полем или объединением. Однако, как необязательные значения, так и +объединения имеют заранее известный максимальный размер и Zig может его +использовать для своих дел. Тут дело в том, что рекурсивная структура не +имеет верхнего предела для своего размера, поскольку уровень рекурсии +потенциально может быть какой угодно, хоть 2, хоть миллион. Это число +варьировалось бы от пользователя к пользователю, поэтому размер структуры +`User` был бы неизвестен во время компиляции. + +Собственно, мы уже видели, как решить нашу проблему (на примере `name`): +используйте указатель. Указатели **всегда** имеют размер, равный размеру +`usize`. На 32-х битной платформе это 4 байта, на 64-х битной - 8 байт. +Точно так же, как само имя "Goku" не хранилось внутри структуры (хранился +только срез размером строго 16 байт), так ж и тут, вместо вкладывания +структуры `User` в неё саму будет использоваться указатель, который также +имеет известный размер: + +```zig +const std = @import("std"); + +pub fn main() void { + const leto = User{ + .id = 1, + .power = 9001, + .manager = null, + }; + + const duncan = User{ + .id = 1, + .power = 9001, + // changed from leto -> &leto + .manager = &leto, + }; + + std.debug.print("{any}\n{any}", .{leto, duncan}); +} + +pub const User = struct { + id: u64, + power: i32, + // changed from ?const User -> ?*const User + manager: ?*const User, +}; +``` + +Может быть, Вам в Вашей практике рекурсивные структуры никогда и не понадобятся, +но мы тут говорили не о моделировании данных, а об указателях, размещении данных +в памяти для лучшего понимания того, с чем имеет дело компилятор. + +Несколько слов в заключение этой главы. Многие разработчики программного +обеспечения, даже весьма опытные, могут порой испытывать трудности (как +правило, временные) в применении указателей. Есть в них что-то +ускользающее от внимания, они не такие "конкретные", как, скажем, числа, +строки или структуры. Чтобы двигаться дальше в изучении Zig, не +требуется, чтобы всё, что касается указателей, было бы кристально ясным +для Вас. Однако, оттачивание мастерства в применении указателей того +стоит, и не только при изучении конкретно Zig. Всевозможные детали могут +быть скрыты в таких языках, как Ruby, Python и JavaScript (и в меньше +степени в таких, как C#, Java и Go), но от этого они никуда не +деваются. Их понимание влияет на то, как именно Вы пишете код и как этот +код будет выполняться. Так что не жалейте времени, упражняйтесь с +примерами, добавляйте печать значений и адресов различных сущностей; чем +больше Вы исследуете, тем вопросы, связанные с указателями и их +применением, будут становиться дл Вас всё более и более ясными.