On branch main
modified: src/ch04.md
This commit is contained in:
346
src/ch04.md
346
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), но от этого они никуда не
|
||||
деваются. Их понимание влияет на то, как именно Вы пишете код и как этот
|
||||
код будет выполняться. Так что не жалейте времени, упражняйтесь с
|
||||
примерами, добавляйте печать значений и адресов различных сущностей; чем
|
||||
больше Вы исследуете, тем вопросы, связанные с указателями и их
|
||||
применением, будут становиться дл Вас всё более и более ясными.
|
||||
|
||||
Reference in New Issue
Block a user