On branch main

modified:   src/ch01.md
new file:   src/ex-ch01-04.zig
new file:   src/ex-ch01-05.zig
This commit is contained in:
zed
2023-11-11 00:27:29 +03:00
parent 9462460af6
commit 6bca0507ff
3 changed files with 197 additions and 1 deletions

View File

@@ -53,7 +53,7 @@ zig run ex-ch01-01.zig
и базируется на 2-х вещах
* `@import` - это встроенная в компилятор функция
* `pub` - ключевое слово для экспортирования чего-то
* `pub` - ключевое слово для экспортирования различных определений
При импортировании модуля нужно указать его имя. Стандартная библиотека Zig доступна
по имени `std`. Чтобы импортировать какой-то конкретный файл, нужно использовать
@@ -492,3 +492,183 @@ pub fn main() void {
"бинарные" данные или строка в кодировке UTF-8. Как бы то ни было,
для всех случаев используется один и тот же тип, как для собственно строк,
так и для всего прочего.
Из того, что мы уже знаем про массивы и срезы, мы могли бы заключить,
что `[]const u8` это срез неизменяемого массива байтов, беззнаковых
8-ми битных целых чисел. Но при инициализации строк мы нигде никакие
массивы не использовали, мы просто писали `.name = "Пётр".` Как это работает?
Длины строковых констант (strings literals, буквальные значения)
известны во время компиляции. Компилятор знает, что строка `"Пётр"` имеет
длину 8 байт (в UTF-8 на каждую букву кириллицы идёт 2 байта),
так что мы можем предположить, что тип этой константы будет `[8]const u8`.
Однако, строковые константы обладают парочкой особых свойств, а именно,
для их хранения в исполнимом файле отведено специальное место,
при этом дубликаты исключаются. Поэтому переменная, через которую
доступна та или иная строковая константа, по идее, должна быть указателем
в это специальное место. Это означает, что тип строки `"Пётр"` это скорее
указатель вида `*const [8]u8`, то есть указатель на неизменяемый массив
длиной 8 байт.
Но это ещё не всё. Строковые константы заканчиваются бинарным нулём (как в C).
Собственно, так сделано как раз ради "взаимодействия" с кодом, уже
когда-то написанным на языке C. Строка `"Пётр"` в памяти будет выглядеть
как `{0xD0, 0x9F, 0xD1, 0x91, 0xD1, 0x82, 0xD1, 0x80, 0x00}` и тогда,
возможно, вы подумаете, что её тип это `*const [9]u8`, Но это в лучшем
случае будет неоднозначным, а в худшем - даже опасным. Поэтому для представления
строк с признаком конца в виде `\0` (null-terminated strings) в Zig есть
специальный синтаксис - вообщем, строка `"Пётр"` имеет тип `*const [8:0]u8`,
то есть указатель на массив из 8-ми байт с дополнительным нулём на конце.
Тут мы невольно сделали акцент именно на C-подобные строки, однако,
этот синтаксис более общий - [LENGTH:SENTINEL], где `SENTINEL` это специальное значение,
служащее для обозначения конца массива. Вот странный пример с непонятным
потенциальным применением, но тем не менее, он вполне корректен с точки
зрения Zig:
```zig
const std = @import("std");
pub fn main() void {
// массив из 3 булевских значений с false на конце
const a = [3:false]bool{false, true, false};
// эта строка более сложная, объясняться не будет
std.debug.print("{any}\n", .{std.mem.asBytes(&a).*});
}
```
Работает, как и задумано (то есть признак конца тоже печатается):
```
$ /opt/zig-0.11/zig run src/ex-ch01-04.zig
{ 0, 1, 0, 0 }
```
Весьма вероятно, что у Вас остался ещё один вопрос. Если `"Пётр"` это
`*const [8:0]u8`, а поле `name` это `[]const u8`, то как так получается.
что мы свободно присваиваем первое второму? Ответ простой - в этом случае
Zig делает приведение типов (coercion) автоматически. Это означает,
что если функция имеет параметр с типом `[]const u8` или поле структуры
имеет типа `[]const u8`, то при передаче параметра или приваивании значения
полю можно использовать строковые конатанты. Приведение типов в этом
случае не требует больших временных затрат, поскольку строки с нулём
на конце это массивы, длина их известна на этапе компиляции, поэтому
не нужно проходить по всей строке в поисках признака конца.
Итак, когда мы говорим о строках, то обычно мы имеем ввиду `[]const u8`.
Когда необходимо, мы явно указываем, что строка в стиле C, то есть
с нулём на конце и помним, что она автоматически приводится к `[]const u8`.
Но опять же, мы помним, что `[]const u8` используется также для
произвольных бинарных данных и поэтому в Zig нет специального типа для строк.
Конечно, в "настоящих" программах большинство строк/массивов неизвестны
во время компиляции - данные могут поступать от пользователя, из сети и т.п.
К этому вопросу мы ещё вернёмся, когда будем рассматривать работу
с оперативной памятью. А сейчас мы пока скажем следующее - для
такого рода данных, которые возникают лишь в процессе работы программы,
память выделяется динамически. При этом наши переменные всё равно будут
иметь тип `[]u8` и указатель в этих срезах будет показывать на динамически
(то есть в процессе выполнения программы) выделенную память.
## comptime и anytype
В нашем примере осталась ещё одна совершенно не исследованная строчка:
```zig
std.debug.print("{s} обладает силой {d}\n", .{user.name, user.power});
```
Тут происходит намного больше, чем может показаться на первый взгляд.
За этой с виду простой строчкой скрываются 2 из более мощных
возможностей Zig, о которых нужно иметь представление, даже
если Вы пока ещё не овладели ими в совершенстве.
Первая из них это выполнение кода во время компиляции, которое
обозначается ключевым словом `comptime`. Это основа для метапрограммирования
в Zig и, как подразумевает само название, речь идёт о выполнении кода
во время компиляции, а не во время исполнения уже откомпилированной прораммы.
На протяжении этой книги мы лишь слегка коснёмся этой темы,
но надо понимать, что эта особенность всегда незримо присутствует.
Вам уже, наверное, не терпится спросить, а какое отношение к этому
имеет та самая строчка? Давайте посмотрим на сигнатуру функции `print`:
```zig
// обратите внимание на 'comptime' перед первым аргументом
pub fn print(comptime fmt: []const u8, args: anytype) void {
```
Ключевое слово `comptime` перед `fmt` означает, что строка формата
должна быть известна во время компиляции. Причиной для этого является
то, что `print` во время компиляции делает различные проверки.
Что это за проверки? Ну, скажем, Вы поменяли формат на `"it's over {d}\n"`,
но при этом оставили 2 аргумента. Тогда Вы получите ошибку компиляции.
Также делается проверка на соответствие типов: измените формат на
`"{s}'s power is {s}\n"` и Вы тоже получите ошибку про несоответствие
`u64` спецификатору `{s}`. Подобного рода проверки были бы невозможны
во время компиляции, если бы строка формата была бы неизвестна во время
этой самой компиляции, отсюда такое требование для параметра `fmt`.
Одна из ситуаций, где Вы сразу же столкнётесь с `comtime` это
типы по умолчанию для целочисленных констант и констант с плавающей точкой -
`comptime_int` и `comptime_float`. Например, строчка `var i = 0;`
не является корректной, компилятор в это месте скажет, что
"переменная типа `comptime_int` должна быть `const` или `comptime`".
Любой код, который помечен как `comptime`, может работать с данными,
известными во время компиляции и, что касается целых и вещественных чисел,
такие данные имеют специальные типы, `comptime_int` и `comptime_float`,
соответственно. Однако, вряд ли Вы будете большую часть времени писать
код, предназначенный для выполнения во время компиляции, так что
это не особо полезное умолчание. То, что Вам нужно, это просто явно задать типы,
например
```zig
var i: usize = 0;
var j: f64 = 0;
```
Отметим также, что если бы мы написали `const i = 0` (а не `var i = 0`),
то ошибки бы не было, поскольку вся её (ошибки) суть в том, что
"переменные" типа `comptime_int` _должны_ быть константой.
В одной из следующих глав мы познакомимся с `comptime` поближе,
когда будем изучать обобщённые структуры данных ("generics").
Ещё одна особенная вещь, которая имеется в нашей строчке, это
вот это странное `{user.name, user.power}`, которое, как следует
из сигнатуры `print`, передаётся как `anytype`. Этот тип не следует
путать с чем-то вроде `Object` из Java или `any` из Go. Здесь
дело обстоит так - во время компиляции Zig сгенерирует специализированные
варинаты `print` для всех типов, которые Вы передавали `print` в Вашей
программе.
Тут возникает вопрос - а _что_, собственно, мы передаём в функцию `print`?
Мы уже видели подобную запись, когда немного говорили об автоматическом
выведении типов. Здесь ситуация похожая, но тут такая запись создаёт
анонимную структуру. Запустите вот такой код:
```zig
const std = @import("std");
pub fn main() void {
std.debug.print("{any}\n", .{@TypeOf(.{.year = 2023, .month = 8})});
}
```
Он напечатает
```
struct{comptime year: comptime_int = 2023, comptime month: comptime_int = 8}
```
Тут мы дали полям анонимной структуры имена (`year` и `month`).
В изначальном варианте мы этого не делали, просто указывали значения.
В таких случаях имена полей генерируются автоматически, они будут
такие - `"0"`, `"1"`, `"2"` и т.д. Функция `print` ожидает структуру
с такими полями и использует позиции спецификаторов в строке формата
для получения соответствующего аргумента.
В Zig нет перегрузки функций ("function overloading"), а также нет
функций с переменным числом аргументов ("variadic functions").
Вместо этого компилятор Zig умеет создавать специализированные варианты
функций, основываясь на передаваемых в "универсальный" вариант типах,
как выведенных, так и созданных самим компилятором.

10
src/ex-ch01-04.zig Normal file
View File

@@ -0,0 +1,10 @@
const std = @import("std");
pub fn main() void {
// an array of 3 booleans with false as the sentinel value
const a = [3:false]bool{false, true, false};
// This line is more advanced, and is not going to get explained!
std.debug.print("{any}\n", .{std.mem.asBytes(&a).*});
}

6
src/ex-ch01-05.zig Normal file
View File

@@ -0,0 +1,6 @@
const std = @import("std");
pub fn main() void {
std.debug.print("{any}\n", .{@TypeOf(.{.year = 2023, .month = 8})});
}