On branch main

modified:   src/ch01.md
modified:   src/ch02.md
modified:   src/ch03.md
modified:   src/ch04.md
modified:   src/ch05.md
modified:   src/ch06.md
modified:   src/ch07.md
modified:   src/ch08.md
modified:   src/ch09.md
new file:   src/ex-ch09-05.zig
This commit is contained in:
zed
2023-11-19 19:51:36 +03:00
parent 8ead00e399
commit dfcf1d5538
10 changed files with 169 additions and 93 deletions

View File

@@ -96,10 +96,10 @@ const User = user.User;
const MAX_POWER = user.MAX_POWER;
```
Скорее всего, на данный момент у Вас больше вопросов, чем ответов. Что
Скорее всего, на данный момент у вас больше вопросов, чем ответов. Что
такое `user` в приведенном отрывке? Хотя мы ещё не знакомы с ключевым
словом `var`, но тем не менее можно спросить, а что, если вместо `const`
использовать `var`? Или. возможно, у Вас возник вопрос по поводу
использовать `var`? Или. возможно, у вас возник вопрос по поводу
использования сторонних библиотек. Это всё хорошие вопросы, но чтобы
ответить на них, нам нужно глубже изучить Zig, а пока нам придётся
обойтись тем, что мы уже знаем, а именно:
@@ -168,10 +168,10 @@ fn add(a: i64, b: i64) i64 {
Далее отметим тип `i64`. Вот некоторые другие целочисленные/вещественные
типы: `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u47`, `i47`, `u64`,
`i64`, `f32` и `f64`. Наличие в списке `u47` и `i47` это вовсе не
проверка того, что Вы ещё не спите - Zig умеет работать с целыми числами
произвольного размера (в битах). Возможно, Вы не будете использовать
проверка того, что вы ещё не спите - Zig умеет работать с целыми числами
произвольного размера (в битах). Возможно, вы не будете использовать
такие типы (`u1`, `s9` и т.п.), однако, иногда они удобны или даже
необходимы. Тип, который Вы точно будете часто использовать, это `usize`
необходимы. Тип, который вы точно будете часто использовать, это `usize`
- это целочисленный тип размером с указатель (для данной платформы) и
этот тип представляет длину/размер чего-либо.
@@ -289,9 +289,9 @@ pub const User = struct {
Использование именно такого имени (`init`) является просто соглашением,
то есть в каких-то случаях более уместными могут показаться и другие
имена, например, `open`. Если Вы не программист на C/C++, то такой
имена, например, `open`. Если вы не программист на C/C++, то такой
синтаксис инициализации полей (с точкой, `.name = name`) может показаться
Вам слегка странным, но со временем Вы к этому привыкнете.
Вам слегка странным, но со временем вы к этому привыкнете.
Когда мы создавали пользователя `Пётр`, мы объявили экземпляр `user` как
константу (`const`):
@@ -305,7 +305,7 @@ const user = User{
Это означает, что мы не можем модифицировать эту "переменную". Чтобы
иметь возможность модификации, нужно объявить экземпляр при помощи
ключевого слова `var`, а не `const`. Возможно, Вы так же заметили, что
ключевого слова `var`, а не `const`. Возможно, вы так же заметили, что
тип переменной `user` выводится из того, что в неё присваивается. Мы
могли бы написать всё явно, то есть вот так:
@@ -349,7 +349,7 @@ pub fn init(name: []const u8, power: u64) User {
Мы могли бы обойти молчанием последнюю нашего примера (`name: []const
u8`), но, поскольку пример содержит две строковые константы (`"Пётр"` и
`"{s} обладает силой {d}\n"`), то наверняка Вы любопытствуете по поводу
`"{s} обладает силой {d}\n"`), то наверняка вы любопытствуете по поводу
строк в Zig. Чтобы лучше понимать строки, нужно сначала ознакомиться с
массивами и срезами.
@@ -374,7 +374,7 @@ const c = [_]i32{1, 2, 3, 4, 5};
С другой стороны, срез - это пара указатель плюс длина, при этом длина
становится известной лишь при исполнении программы. Мы пройдёмся по
указателям далее, а пока Вы можете представлять себе срез как некое
указателям далее, а пока вы можете представлять себе срез как некое
"окно" в массиве.
Однако, не всё так просто. Рассмотрим следующий отрывок кода:
@@ -445,7 +445,7 @@ src/ex-ch01-03.zig:8:6: error: cannot assign to constant
~^~~
```
Наверняка для исправления ситуации Вам тут хочется поменять `const` на
Наверняка для исправления ситуации вам тут хочется поменять `const` на
`var` при объявлении `b`:
```zig
@@ -474,7 +474,7 @@ pub fn main() void {
```
Это работает, поскольку теперь тип среза `b` это `[]i32`, а не `[]const
i32`, как было до этого. Тут Вы справедливо возразите - но `b`-то у нас
i32`, как было до этого. Тут вы справедливо возразите - но `b`-то у нас
всё равно `const`, как же тогда это может работать?!? Но всё просто -
неизменяемость `b` относится к самому `b` (то есть к той самой паре
указатель+длина), но не к данным, на которые он показывает, а показывает
@@ -565,7 +565,7 @@ $ /opt/zig-0.11/zig run src/ex-ch01-04.zig
{ 0, 1, 0, 0 }
```
Весьма вероятно, что у Вас остался ещё один вопрос. Если `"Пётр"` это
Весьма вероятно, что у вас остался ещё один вопрос. Если `"Пётр"` это
`*const [8:0]u8`, а поле `name` это `[]const u8`, то как так получается.
что мы свободно присваиваем первое второму? Ответ простой - в этом случае
Zig делает приведение типов (coercion) автоматически. Это означает, что
@@ -602,7 +602,7 @@ std.debug.print("{s} обладает силой {d}\n", .{user.name, user.power
Тут происходит намного больше, чем может показаться на первый взгляд. За
этой с виду простой строчкой скрываются две мощных возможности Zig, о
которых нужно иметь представление, даже если Вы пока ещё не овладели ими
которых нужно иметь представление, даже если вы пока ещё не овладели ими
в совершенстве.
Первая из них это выполнение кода во время компиляции, которое
@@ -624,15 +624,15 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
Ключевое слово `comptime` перед `fmt` означает, что строка формата должна
быть известна во время компиляции. Причиной для этого является то, что
`print` во время компиляции делает различные проверки. Что это за
проверки? Ну, скажем, Вы поменяли формат на `"it's over {d}\n"`, но при
этом оставили 2 аргумента. Тогда Вы получите ошибку компиляции. Также
проверки? Ну, скажем, вы поменяли формат на `"it's over {d}\n"`, но при
этом оставили 2 аргумента. Тогда вы получите ошибку компиляции. Также
делается проверка на соответствие типов: измените формат на `"{s}'s power
is {s}\n"` и Вы тоже получите ошибку про несоответствие `u64`
is {s}\n"` и вы тоже получите ошибку про несоответствие `u64`
спецификатору `{s}`. Подобного рода проверки были бы невозможны во время
компиляции, если бы строка формата была бы неизвестна во время этой самой
компиляции, отсюда такое требование для параметра `fmt`.
Одна из ситуаций, где Вы сразу же столкнётесь с `comptime` это типы по
Одна из ситуаций, где вы сразу же столкнётесь с `comptime` это типы по
умолчанию для целочисленных констант и констант с плавающей точкой -
`comptime_int` и `comptime_float`. Например, строчка `var i = 0;` не
является корректной, компилятор в это месте скажет, что "переменная" типа
@@ -640,9 +640,9 @@ is {s}\n"` и Вы тоже получите ошибку про несоотв
помечен как `comptime`, должен работать с данными, известными во время
компиляции и, что касается целых и вещественных чисел, такие данные имеют
специальные типы, `comptime_int` и `comptime_float`, соответственно.
Однако, вряд ли Вы будете большую часть времени писать код,
Однако, вряд ли вы будете большую часть времени писать код,
предназначенный для выполнения во время компиляции, так что это не особо
полезное умолчание. То, что Вам нужно, это просто явно задать типы,
полезное умолчание. То, что вам нужно, это просто явно задать типы,
например
```zig
@@ -662,7 +662,7 @@ var j: f64 = 0;
`print`, передаётся как `anytype`. Этот тип не следует путать с чем-то
вроде `Object` из Java или `any` из Go. Здесь дело обстоит так - во время
компиляции Zig сгенерирует специализированные варианты `print` для всех
типов, которые Вы передавали `print` в Вашей программе.
типов, которые вы передавали `print` в вашей программе.
Тут возникает вопрос - а _что_, собственно, мы передаём в функцию
`print`? Мы уже видели подобную запись, когда немного говорили об

View File

@@ -8,13 +8,13 @@
## Управление потоком выполнения
Операторы управления потоком исполнения в Zig, безусловно, покажутся Вам
Операторы управления потоком исполнения в Zig, безусловно, покажутся вам
знакомыми, однако, имеется ряд моментов, касающихся их взаимодействия с
некоторыми другими аспектами языка, которые нам пока лишь предстоит
изучить. Мы начнём с краткого обзора и будем возвращаться по мере того,
как нам будут встречаться особенности в поведении инструкций управления.
Далее Вы заметите, что для логических операторов "И" и "ИЛИ" в Zig
Далее вы заметите, что для логических операторов "И" и "ИЛИ" в Zig
используются обозначения `and` и `or`, а не `&&` и `||`. Как и во многих
других языках, эти операторы влияют на ход исполнения программы,
поскольку при компиляции выражений, содержащих эти операторы, применяется
@@ -309,9 +309,9 @@ Stage.confirmed or self == Stage.err;`, но, как правило, тип оп
То, что в операторе `switch` нужно указывать все возможные варианты (ну,
если нет оборота `else`), просто замечательно сочетается с перечислениями:
если мы что-то забыли, компилятор нам подскажет. И будьте осторожны,
если у Вас в `switch` есть оборот `else` - если Вы вдруг добавите новые
если у вас в `switch` есть оборот `else` - если вы вдруг добавите новые
элементы в перечисление, то они все попадут в этот `else` и, скорей
всего, программа не будет работать так, как Вы задумывали.
всего, программа не будет работать так, как вы задумывали.
## Объединения (union)
@@ -578,7 +578,7 @@ pub fn main() !void {
вполне эквивалентны. Например, ссылки на функции с неявным набором ошибок
требуют специального типа, `anyerror`. Разработчики библиотек, возможно,
предпочитают быть более явными, так код становится самодокументирующимся.
Так или иначе, оба подхода имеют право на существование и Вы можете
Так или иначе, оба подхода имеют право на существование и вы можете
использовать оба по своему усмотрению.
Реальная ценность объединений с ошибками проявляется при обработке
@@ -634,7 +634,7 @@ try action(req, res);
```
Это полезно в частности потому, что **ошибки должны быть обработаны**. По
большей части Вы будете использовать для этого `catch` или `try`, но
большей части вы будете использовать для этого `catch` или `try`, но
объединения с ошибками также поддерживаются операторами `if` и `while`,
вполне аналогично тому, как они работают с опциональными значениями. В
случае `while`, если условие сразу же даёт ошибку, исполняется оборот

View File

@@ -34,7 +34,7 @@ fn add(a: i64, b: i64) i64 {
Первая ошибка возникла потому, что локальная константа `sum` никак после
присваивания ей значения не используется. Вторая ошибка связана с тем
обстоятельством, что `b` (второй аргумент функции `add`) тоже внутри
функции никак не используется. Однако, у Вас могут быть вполне
функции никак не используется. Однако, у вас могут быть вполне
уважительные причины реально иметь в своём коде неиспользованные
локальные переменные или параметры функций, поэтому в Zig имеется
возможность игнорировать ненужные значения путём присваивания их символу
@@ -88,10 +88,10 @@ fn read(stream: std.net.Stream) ![]const u8 {
## Соглашения об именовании
За исключением правил, диктуемых компилятором, Вы, разумеется, вольны
использовать такие соглашения об именах, какие Вам больше нравятся.
За исключением правил, диктуемых компилятором, вы, разумеется, вольны
использовать такие соглашения об именах, какие вам больше нравятся.
Однако, совершенно не вредно понимать собственные соглашения Zig,
поскольку стандартная библиотека, которой Вы, безусловно, будете
поскольку стандартная библиотека, которой вы, безусловно, будете
пользоваться, придерживается этих соглашений.
Отступы в Zig - это 4 пробела. Как правило, текстовые редакторы можно

View File

@@ -7,9 +7,9 @@
Мы начнём изучать указатели, это важная тема как сама по себе, но также и
для того, чтобы начать приучать себя смотреть на программы с точки зрения
использования памяти компьютера. Если Вы свободно обращаетесь с
использования памяти компьютера. Если вы свободно обращаетесь с
указателями, динамическим выделением/освобождением памяти, понимаете, что
такое "висячий" указатель (dangling pointer), тогда Вы можете пропустить
такое "висячий" указатель (dangling pointer), тогда вы можете пропустить
пару глав и перейти к главе 6, там есть некоторые особенности,
специфичные для Zig.
@@ -118,7 +118,7 @@ user -> ------------ (id: 1043368d0)
размер поля `id` равен 8-ми байтам, поле `power` имеет адрес, равный
адресу начала структуры плюс 8.
Чтобы Вы могли сами в этом убедиться, мы сейчас введём оператор взятия
Чтобы вы могли сами в этом убедиться, мы сейчас введём оператор взятия
адреса, он обозначается символом `&`. Как подразумевает название этого
оператора, он возвращает адрес переменной (а также может возвращать адрес
функции, то есть адрес, где начинается код функции). Не меняя
@@ -143,8 +143,8 @@ i32@7ffdb6cad4e0
```
В зависимости от Вашей аппаратной платформы, а также от ряда других
факторов, Вы можете увидеть не конкретно такие числа, но это не важно.
В зависимости от вашей аппаратной платформы, а также от ряда других
факторов, вы можете увидеть не конкретно такие числа, но это не важно.
Важно то, что адреса `user` и его первого поля `user.id` одинаковы, а
адрес 2-го поля ровно на 8 байт больше.
@@ -193,7 +193,7 @@ fn levelUp(user: User) void {
}
```
Если Вы это запустите, то увидите совершенно разные адреса:
Если вы это запустите, то увидите совершенно разные адреса:
```
$ /opt/zig-0.11/zig run src/ex-ch04-05.zig
@@ -308,7 +308,7 @@ pub const User = struct {
соблюдались бы намерения разработчика. В некоторой степени это возможно
как раз благодаря тому, что параметры функций являются константами.
Возможно, ВЫ задались вопросом - а каким образом передача по ссылке может
Возможно, вы задались вопросом - а каким образом передача по ссылке может
быть медленнее, чем по значению, даже для маленьких структур? Более ясно
мы поймём это далее, но суть в том, что обращение к полям (`user.power`,
например) через указатель добавляет некоторые накладные расходы и поэтому
@@ -520,14 +520,14 @@ pub const User = struct {
В некоторых языках всё это реализуется по другому, но многие языки
работают именно так (ну, или близко к этому). Хотя всё это может
показаться понятным лишь посвящённым, тем не менее, это является основой
для повседневного программирования. Хорошие новости в том, что Вы можете
для повседневного программирования. Хорошие новости в том, что вы можете
освоить эту "эзотерику" используя простые примеры и отрывки кода - по
мере того, как сложность остальных частей системы растёт, основы остаются
неизменными и не становятся более сложными.
## Рекурсивные структуры
Иногда Вам могут потребоваться, чтобы структура была рекурсивной.
Иногда вам могут потребоваться, чтобы структура была рекурсивной.
Давайте добавим в структуру `User` необязательное поле `manager`.
Мы также создадим 2-х пользователей и назначим одного из них
менеджером другого:
@@ -571,7 +571,7 @@ pub const User = struct {
имело вполне определённый размер, по 8 байт на указатель и на длину,
всего 16 байт.
Возможно, Вы подумаете, что это будет проблемой с любым необязательным
Возможно, вы подумаете, что это будет проблемой с любым необязательным
полем или объединением. Однако, как необязательные значения, так и
объединения имеют заранее известный максимальный размер и Zig может его
использовать для своих дел. Тут дело в том, что рекурсивная структура не
@@ -616,7 +616,7 @@ pub const User = struct {
};
```
Может быть, Вам в Вашей практике рекурсивные структуры никогда и не понадобятся,
Может быть, вам в вашей практике рекурсивные структуры никогда и не понадобятся,
но мы тут говорили не о моделировании данных, а об указателях, размещении данных
в памяти для лучшего понимания того, с чем имеет дело компилятор.
@@ -626,12 +626,12 @@ pub const User = struct {
ускользающее от внимания, они не такие "конкретные", как, скажем, числа,
строки или структуры. Чтобы двигаться дальше в изучении Zig, не
требуется, чтобы всё, что касается указателей, было бы кристально ясным
для Вас. Однако, оттачивание мастерства в применении указателей того
для вас. Однако, оттачивание мастерства в применении указателей того
стоит, и не только при изучении конкретно Zig. Всевозможные детали могут
быть скрыты в таких языках, как Ruby, Python и JavaScript (и в меньше
степени в таких, как C#, Java и Go), но от этого они никуда не
деваются. Их понимание влияет на то, как именно Вы пишете код и как этот
деваются. Их понимание влияет на то, как именно вы пишете код и как этот
код будет выполняться. Так что не жалейте времени, упражняйтесь с
примерами, добавляйте печать значений и адресов различных сущностей; чем
больше Вы исследуете, тем вопросы, связанные с указателями и их
применением, будут становиться для Вас всё более и более ясными.
больше вы исследуете, тем вопросы, связанные с указателями и их
применением, будут становиться для вас всё более и более ясными.

View File

@@ -5,7 +5,7 @@
переменными, данными и памятью. Но нам ещё нужно поговорить о том, как
происходит управление ланными и памятью. Для коротко-живущих и простых
скриптов это, как правило, не имеет особого значения. В наше время, имея
ноутбук, оснащённый 32-мя гигабайтами оперативной памяти, Вы можете
ноутбук, оснащённый 32-мя гигабайтами оперативной памяти, вы можете
запустить свою программу, которая, делая какую-то работу (например,
прочитать файл), может запросто использовать несколько сотен мегабайт
памяти, сделать что-то потрясающее и далее выйти. По выходу операционная
@@ -271,13 +271,13 @@ user`, а не `return &user`. Но это не всегда оказывает
следующей главы.
Прежде чем погрузиться в тему динамического выделения памяти, небольшое
забегание вперёд: в этой книге Вы увидите ещё один пример висячих
забегание вперёд: в этой книге вы увидите ещё один пример висячих
указателей. К тому моменту мы уже будем знать достаточно, чтобы привести
менее ... пример. К этой теме важно будет вернуться потому, что для
разработчиков, которые раньше не имели дела с языками без автоматической
сборки мусора (например, C), ручное управление памятью может поначалу
показаться чем-то трудным, что чревато ошибками в программах и
последующим отчаянием после попыток исправить программу. Но не надо
отчаиваться заранее, Вы получите то, что позволит лучше понять ручное
отчаиваться заранее, вы получите то, что позволит лучше понять ручное
управление память, а пока мы скажем, что ключевой момент это чётко
осознавать, где и когда существуют данные.

View File

@@ -19,9 +19,9 @@
Эта глава посвящена двум темам. Первая это общий обзор нашей третьей
области памяти, кучи. А вот вторая более интересна - в языке Zig имеется
концептуально простой, но тем не менее уникальный подход к управлению
динамической памятью. Даже если Вы так или иначе умеете работать с кучей
динамической памятью. Даже если вы так или иначе умеете работать с кучей
(например, при помощи `malloc` и `free` из стандартной библиотеки языка
C), Вам всё равно следует проштудировать эту главу, поскольку в Zig есть
C), вам всё равно следует проштудировать эту главу, поскольку в Zig есть
свои особенности.
## Динамическая память (heap, "куча")
@@ -85,7 +85,7 @@ fn getRandomCount() !u8 {
Общее правило при динамическом выделении памяти - каждый вызов `alloc`,
как правило, имеет соответствующий вызов `free`. Первый выделяет память,
второй освобождает. Однако, не дайте этому примеру ограничить Ваше
второй освобождает. Однако, не дайте этому примеру ограничить ваше
воображение. Да, этот паттерн, `try alloc` и сразу следом `defer free`
действительно часто используется, и не зря: такое размещение инструкций
является относительно надёжной защитой от ошибок в виде того, что мы
@@ -108,7 +108,7 @@ fn getRandomCount() !u8 {
блоком) до того момента, когда закончится выполняться блок (включая явный
`return`), в котором находится `defer`. Такое отложенное выполнение вовсе
не связано конкретно с аллокаторами и управлением памятью, это более
общий механизм, Вы можете использовать его для выполнения абсолютно
общий механизм, вы можете использовать его для выполнения абсолютно
произвольного кода.
`defer` в Zig подобен таковому в `Go`, с одним важным отличием:
@@ -184,9 +184,9 @@ pub const Game = struct {
Только что мы отметили, что не существует никаких правил относительно
того, кто и когда должен освободить память. Однако, это верно лишь
отчасти, всё таки имеется несколько важных правил, просто ничто, кроме
Вашей педантичности и аккуратности, не заставит Вас их соблюдать.
Вашей педантичности и аккуратности, не заставит вас их соблюдать.
Первое правило состоит, что Вы не можете освободить одну и ту же
Первое правило состоит, что вы не можете освободить одну и ту же
область памяти дважды, смотрим на пример:
```zig
@@ -213,7 +213,7 @@ pub fn main() !void {
проектах со сложными правилами по поводу времён жизни бывает трудно всё
это отследить.
Второе правило гласит, что Вы не можете освобождать память, на которую у
Второе правило гласит, что вы не можете освобождать память, на которую у
Вас нет ссылки. Это вроде как самоочевидно, но бывает не всегда ясно, кто
именно ответственен за освобождение. Вот функция, которая создаёт строку в
нижнем регистре:
@@ -274,7 +274,7 @@ fn isSpecial(allocator: Allocator, name: [] const u8) !bool {
## `create` и `destroy`
Метод `alloc` структуры `std.mem.Allocator` возвращает срез с длиной,
указанной вторым параметром. Если Вам нужно одиночное значение,
указанной вторым параметром. Если вам нужно одиночное значение,
вместо `alloc` и `free` используйте `create` и `destroy`, соответственно.
Пару глав назад, изучая указатели, мы создавали пользователя
и пытались увеличить его силу. Вот работающий пример с динамическим
@@ -336,7 +336,7 @@ pub const User = struct {
В этом случае имеет смысл возвращать `User`, а не указатель на него.
Но иногда нужно, чтобы функция возвращала именно указатель на то,
что она создала. Так нужно делать, если Вы хотите, чтобы время
что она создала. Так нужно делать, если вы хотите, чтобы время
жизни создаваемой сущности не было привязано к механизму стека вызовов.
Чтобы решить проблему висячего указателя, можно использовать `create`:
@@ -370,8 +370,8 @@ fn init(allocator: std.mem.Allocator, id: u64, power: i32) !*User{
## Аллокаторы
Одним из базовых принципов Zig является принцип "никаких скрытых
выделений памяти". В зависимости от Вашего предыдущего опыта
программирования, возможно, для Вас в этом принципе нет ничего
выделений памяти". В зависимости от вашего предыдущего опыта
программирования, возможно, для вас в этом принципе нет ничего
особенного. Тем не менее, это сильно контрастирует с тем, что мы имеем в
стандартной библиотеке языка C, а там для выделения памяти мы имеем
`malloc`. В C для того, чтобы понять, использует ли та или иная функция
@@ -400,7 +400,7 @@ defer allocator.free(say);
Другая форма это когда аллокатор передаётся в "конструктор" (`init`), там
запоминается и далее используется для внутренних нужд объекта. Мы видели
такую форму выше, в реализации структуры `Game`. Такая форма менее явна,
потому что Вы знаете, что объект будет использовать динамическую память,
потому что вы знаете, что объект будет использовать динамическую память,
но не знаете, в каких именно своих методах он будет это делать. Такой
подход более практичен для долго живущих сущностей.
@@ -413,10 +413,10 @@ defer allocator.free(say);
`std.heap.GeneralPurposeAllocator`, но в стандартной библиотеке имеются
реализации и других аллокаторов.
Если Вы разрабатываете библиотеку, лучше всего будет, если она будет
принимать аллокатор, это позволит пользователям Вашей библиотеки сами
Если вы разрабатываете библиотеку, лучше всего будет, если она будет
принимать аллокатор, это позволит пользователям вашей библиотеки сами
выбирать, какой конкретно аллокатор будет использоваться. В противном
случае Вам будет нужно выбирать "правильный" аллокатор и, как мы далее
случае вам будет нужно выбирать "правильный" аллокатор и, как мы далее
увидим, аллокаторы не являются взаимоисключающими - программа может
одновременно использовать несколько видов аллокаторов, и на то могут быть
свои весьма веские причины.
@@ -426,7 +426,7 @@ defer allocator.free(say);
Как и подразумевает само название этого аллокатора
(`std.heap.GeneralPurposeAllocator`), это во всех отношениях аллокатор
общего назначения, в частности, он безопасен для использования в
многопоточных программах и может служить главным аллокатором в Ваших
многопоточных программах и может служить главным аллокатором в ваших
программах. Для большинства программ он будет единственным аллокатором.
Аллокатор создаётся на старте программы и затем передаётся тем функциям,
которым он нужен. Вот небольшой пример:
@@ -460,7 +460,7 @@ pub fn main() !void {
очередь, будет передавать аллокатор дальше по цепочке для своих функций,
объектов и т.п.
Наверное, Вы обратили внимание, что синтаксис создания аллокатора
Наверное, вы обратили внимание, что синтаксис создания аллокатора
какой-то странный. Что вообще такое `GeneralPurposeAllocator(.{}){}`? На
самом деле всё это уже нам знакомо, просто тут оно сразу всё в кучу.
`GeneralPurposeAllocator` это, очевидно, функция и, поскольку она
@@ -477,7 +477,7 @@ var gpa = T{};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
```
Возможно, Вы всё ещё не уверены в смысле `.{}`. Это мы тоже встречали
Возможно, вы всё ещё не уверены в смысле `.{}`. Это мы тоже встречали
раньше, это инициализатор структуры с неявным типом, тип будет выведен
компилятором. А что это за тип и какие у него поля? Это
`std.heap.general_purpose_allocator.Config`, хоть это явно у нас и не
@@ -490,7 +490,7 @@ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
## Аллокатор для тестирования
Автор выражает надежду, что Вы серьезно призадумались, когда мы обсуждали
Автор выражает надежду, что вы серьезно призадумались, когда мы обсуждали
утечки памяти и после заявления про то, что Zig тут может помочь,
захотели узнать, чем именно. Помощь нам тут может оказать
`std.testing.allocator`, который на данный момент реализовать на основе
@@ -499,7 +499,7 @@ Zig, но это детали реализации. Нам будет важно
использовать `std.testing.allocator` а наших тестах, он нам отловит
большинство утечек памяти, если таковые будут.
Наверняка Вы уже знакомы с динамическими массивами, также часто называемых `ArrayList`.
Наверняка вы уже знакомы с динамическими массивами, также часто называемых `ArrayList`.
Во многих языках с динамической типизацией вообще всё массивы динамические.
Динамические массивы могут менять свою длину в процессе работы программы.
В стандартной библиотеке Zig есть реализация обобщённого динамического массива,
@@ -571,7 +571,7 @@ pub fn main() !void {
```
Код работает правильно и печатает то, что от него ожидается. Однако,
несмотря на то, что мы вызвали `deinit`, тут имеет место утечка. Если Вы
несмотря на то, что мы вызвали `deinit`, тут имеет место утечка. Если вы
не заметили, где она происходит, ничего страшного, ведь сейчас мы напишем
тест и будем в нём использовать `std.testing.allocator`:
@@ -645,10 +645,10 @@ self.allocator.free(self.items);
Распределитель памяти общего назначения, то есть `GeneralPurposeAllocator` это
вполне разумный выбор по умолчанию, поскольку он в среднем хорошо работает
для любых возможных сценариев и объёмов выделяемой,высвобождаемой памяти.
Однако, в какой-то конкретной программе Вы можете столкнуться с такой
Однако, в какой-то конкретной программе вы можете столкнуться с такой
схемой выделения/освобождения, для которой будет более выгодно использовать
специализированные аллокаторы. В качестве примера можно привести ситуацию,
когда Вам нужны какие-то относительно короткоживущие данные,
когда вам нужны какие-то относительно короткоживущие данные,
которые хотелось бы удалить, так сказать, одним махом. Часто под такие
требования подпадают парсеры. Вот скелет некой функции разбора каких-то данных:
@@ -871,7 +871,7 @@ pub fn main() !void {
вовсе не в том, чтобы полностью исключить использование кучи,
это просто не сработает, поскольку все такие альтернативы
имеют смысл в каких-то особых случаях. Но, тем не менее,
теперь в Вашем распоряжении есть много возможностей -
теперь в вашем распоряжении есть много возможностей -
начиная от стековых кадров и вплоть до аллокатора общего назначения,
со всеми промежуточными механизмами вроде статических буферов,
потоковой передачи данных и специализированных аллокаторов.

View File

@@ -100,7 +100,7 @@ fn IntArray(comptime length: usize) type {
На первый взгляд, тут ничего "более изящного" нет. Однако, помимо того,
что тут структура вложена в функцию и не имеет имени, она выглядит как
любая другая структура, которые мы уже видели ранее, то есть у неё есть
поля, есть методы. Ну, Вы знаете, как говорят - *если что-то выглядит как
поля, есть методы. Ну, вы знаете, как говорят - *если что-то выглядит как
утка...* тут то же самое, это что-то выглядит, плавает и крякает как
обычная структура, потому что она и есть структура.
@@ -138,7 +138,7 @@ fn List(comptime T: type) type {
используются однобуквенные названия, например `K` и `V` для типа ключа и
типа значения применительно к, скажем, хэш-таблице.
Если Вам ещё не до конца всё понятно, поглядите ещё раз на те места, где
Если вам ещё не до конца всё понятно, поглядите ещё раз на те места, где
фигурирует это наше `T`: `items: []T` и `allocator.alloc(T, 4)`. Когда мы
хотим использовать наш обобщённый тип (для создания конкретизированного),
мы пишем

View File

@@ -1,10 +1,10 @@
# Интерфейсы
Если Вы начали изучать Zig, скорее всего, пройдёт совсем немного времени
до того момента, когда Вы осознаете, что в нём нет никакого специального
Если вы начали изучать Zig, скорее всего, пройдёт совсем немного времени
до того момента, когда вы осознаете, что в нём нет никакого специального
синтаксиса для создания интерфейсов (таких, как в Java или Go). Но,
вероятно, Вы заметите некоторые вещи, которые номинально не являются
вероятно, вы заметите некоторые вещи, которые номинально не являются
таковыми, но очень на них похожи, например, `std.mem.Allocator`. Это
потому, что в Zig действительно нет простого механизма для создания
интерфейсов (например, ключевых слов `interface` и `implements`), но, тем
@@ -104,7 +104,7 @@ fn writeAll(ptr: *anyopaque, data: []const u8) !void {
в примере это `*File`. Мы как бы говорим компилятору: "дружище,
верь мне, я знаю, что делаю - дай мне указатель, который показывает
*туда же*, куда и `ptr`, но только теперь рассматривай содержимое
этой области памяти как `File`". Как Вы, наверное, поняли, `@ptrCast` это
этой области памяти как `File`". Как вы, наверное, поняли, `@ptrCast` это
мощная и полезная штука, поскольку позволяет нам рассматривать какую-то
область памяти как что угодно. Но если мы ошибёмся и преобразуем
"универсальный" указатель к типу, который не соответствует тому, что
@@ -119,7 +119,7 @@ fn writeAll(ptr: *anyopaque, data: []const u8) !void {
точкой (`f32`) располагалось по адресу, кратному размеру этого числа, то
есть четырём. Тип `anyopaque` всегда имеет фактор выравнивания, равный
единице (то есть может располагаться по любому адресу). А вот у структуры
`File` фактор выравнивания равен четырём. Если хотите, Вы можете сами это
`File` фактор выравнивания равен четырём. Если хотите, вы можете сами это
увидеть, напечатав значения `@alignOf(File)` и `@alignOf(anyopaque)`.
Поэтому подобно тому, как нам нужна `@ptrCast` для указания компилятору,
какой именно тип мы имеем ввиду, так и `@alignCast` нужна для того,
@@ -317,7 +317,7 @@ builtin.Type{
`ptr_info.Pointer.child.writeAll(...)` транслируется в
`File.writeAll(...)`, то есть получается именно то, что мы и хотели.
Если Вы посмотрите на другие применения такой витиеватой схемы,
Если вы посмотрите на другие применения такой витиеватой схемы,
Вы там можете обнаружить, что `init` делает ещё кое-что после
получения информации о типе:
@@ -405,7 +405,7 @@ pub fn main() !void {
это ограничение роли не играет. Вы можете построить, к примеру,
объединение `Cache`, которое будет включать в себя все нужные
реализации, например, `InMemory`, `Redis` и `Postgresql`. Если
появляются новые механизмы, Вы просто обновляете объединение.
появляются новые механизмы, вы просто обновляете объединение.
Во многих случаях интерфейс будет вызывать функции с реализациями
непосредственно. Для таких случаев можно использовать специальный
@@ -421,6 +421,6 @@ switch (self) {
Такой оборот `else` автоматически разворачивается, то есть для каждого
варианта получается правильный тип `impl`. Здесь показана ещё одна вещь -
интерфейсы могут привносить свою логику, в данном примере учитывается
пустая реализация (`.null`). Сколько логики Вы добавляете в сам
интерфейс, это Ваше дело, но по большей части интерфейсы должны в
пустая реализация (`.null`). Сколько логики вы добавляете в сам
интерфейс, это ваше дело, но по большей части интерфейсы должны в
основном выполнять диспетчеризацию.

View File

@@ -9,9 +9,9 @@
Начнём с рассмотрения ещё нескольких примеров висячих указателей.
Может показаться странным, что мы опять к этому возвращаемся,
однако, если ранее Вы использовали только языки со сборкой мусора,
однако, если ранее вы использовали только языки со сборкой мусора,
то, скорее всего, висячие указатели будут вызывать наибольшие трудности
из тех, с которыми Вы будете сталкиваться.
из тех, с которыми вы будете сталкиваться.
Сможете догадаться, что напечатает следующий пример?
@@ -57,7 +57,7 @@ Goku's power is: -1431655766
В этом примере мы знакомимся с обобщённой хэш-таблицей
(`std.StringHashMap`), которая является специализилванной версией
`std.AutoHashMap` с типом ключа `[]const u8`. Даже если не уверены на все
сто относительно того, что конкретно происходит при выводе, наверняка Вы
сто относительно того, что конкретно происходит при выводе, наверняка вы
поняли, что это однозначно связано с тем, что второй вызов `print`
делается *после* того, как мы уже удалили элемент из таблицы. Если убрать
вызов `remove`, всё будет нормально.
@@ -144,10 +144,10 @@ const User = struct {
## Владение
Про хэш-таблицы знают все и всё ими пользуются, они имеют массу
применений, со многими из которых Вы, вероятно, имели дело в своей
применений, со многими из которых вы, вероятно, имели дело в своей
практике. Хотя они и могут использоваться как короткоживущие сущности,
но, как правило, они всё таки живут длительное время и поэтому требуют
столь же долгоживущих значений, которые Вы помещаете в таблицы.
столь же долгоживущих значений, которые вы помещаете в таблицы.
В следующем примере хэш-таблица наполняется именами, которые пользователь
вводит с клавиатуры в терминале. Пустое имя завершает цикл ввода.
@@ -198,7 +198,7 @@ const User = struct {
};
```
Программа учитывает регистр, но как бы аккуратно Вы не вводили "Leto",
Программа учитывает регистр, но как бы аккуратно вы не вводили "Leto",
метод `contains` будет всегда возвращать `false`. Давайте попробуем
отладить нашу программу, добавив для начала печать всех ключей
и значений после цикла ввода:
@@ -261,7 +261,7 @@ try lookup.put(owned_name, .{.power = i});
случае не меньше, чем хэш-таблица. Но есть одно но: теперь у нас
появились утечки памяти.
Возможно, Вы подумали, что когда мы вызываем `lookup.deinit`, память под
Возможно, вы подумали, что когда мы вызываем `lookup.deinit`, память под
ключи и значения будет освобождена сама собой. Увы, `StringHashMap` не
может так сделать. Во-первых, ключи могут быть строковыми литералами,
тогда строки хранятся в сегменте данных, а не в куче и их в принципе
@@ -297,9 +297,9 @@ defer {
Всё, достаточно с нас висячих указателей и управления памятью! То, что мы
обсуждали в этом разделе по-прежнему может казаться не совсем ясным или
слишком абстрактным. Но, тем не менее, если Вы планируете писать что-то
слишком абстрактным. Но, тем не менее, если вы планируете писать что-то
нетривиальное, можно совершенно определённо сказать, что управление
памятью это то, чем Вы будете должны овладеть в совершенстве. В этой
памятью это то, чем вы будете должны овладеть в совершенстве. В этой
связи будет весьма полезно проделывать всяческие упражнения с кодом из
этого раздела. Например, сделайте тип `UserLookup`, который содержал бы в
себе весь код, связанный с динамической памятью. Попробуйте держать в
@@ -310,6 +310,15 @@ defer {
## Динамический массив `ArrayList`
Определённо, вы будете рады узнать, что можно забыть про наши упражнения
с `IntList` и обобщённым вариантом `List`, потому что в стандартной
библиотеке Zig есть надлежащая реализация обобщённого динамического массива,
`std.ArrayList(T)`.
Динамические массивы как таковые это вполне стандартная вещь,
но, поскольку эта структура данных весьма часто используется,
стоит поглядеть, каков `ArrayList` в действии:
```zig
const std = @import("std");
const builtin = @import("builtin");
@@ -374,6 +383,55 @@ const User = struct {
};
```
В этом примере делается всё то же самое, как и ранее, но только вместо
`StringHashMap(User)` используется `ArrayList(User)`. Все правила
относительно времён жизни и работы с кучей по прежнему в силе. Обратите
внимание, что мы, как и раньше, делаем копию ключей при помощи `dupe` и,
как раньше, освобождаем память, занимаемую этими копиями, перед тем, как
вызвать `deinit` для `ArrayList`.
И сейчас самое время сказать, что в Zig нет так называемых "свойств"
(properties) или приватных полей. Это можно видеть из кода, который
обращается к `arr.items` для прохода по значениям. Причина, по которой в
Zig нет свойств (это такие поля структуры/класса, которые снаружи
используются как обычные поля, но на самом деле это функции), состоит в
том, чтобы устранить возможный источник сюрпризов. В Zig, если что-то
выглядит как обращение к полю, это действительно обращение к полю и,
соответственно, если что-то не выглядит как вызов функции, то это не
вызов функции. С другой стороны, отсутствие приватных полей, возможно,
ошибка дизайна Zig, но мы можем как-то нивелировать, например, используя
символ `_` в качестве первого символа имен тех полей, которые
предназначены только для внутреннего использования.
Поскольку строки имеют тип `[]8` или `[]const u8`, список из байтов
(то есть `ArrayList(u8)`) это подходящий тип для построения
конструктора строк по типу `StringBuilder` в .NET или `strings.Builder` в Go.
Фактически, вы будете часто использовать в случаях, когда функция
приниммает `Writer` и вам на выходе нужна строка. Ранее мы видели пример,
в котором для вывода документа JSON на стандартный вывод
использовалась `std.json.stringify`. Вот пример использования
`ArrayList(u8)` для вывода в переменную:
```zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try std.json.stringify(.{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2}, out.writer());
std.debug.print("{s}\n", .{out.items});
}
```
## `anytype`
## `@typeOf`

18
src/ex-ch09-05.zig Normal file
View File

@@ -0,0 +1,18 @@
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try std.json.stringify(.{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2}, out.writer());
std.debug.print("{s}\n", .{out.items});
}