On branch main

modified:   src/ch04.md
This commit is contained in:
zed
2023-11-15 15:18:25 +03:00
parent 5f7067883f
commit 55ee5ce565

View File

@@ -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), но от этого они никуда не
деваются. Их понимание влияет на то, как именно Вы пишете код и как этот
код будет выполняться. Так что не жалейте времени, упражняйтесь с
примерами, добавляйте печать значений и адресов различных сущностей; чем
больше Вы исследуете, тем вопросы, связанные с указателями и их
применением, будут становиться дл Вас всё более и более ясными.