On branch main
modified: src/ch08.md
This commit is contained in:
355
src/ch08.md
355
src/ch08.md
@@ -1,4 +1,359 @@
|
||||
|
||||
# Интерфейсы
|
||||
|
||||
Если Вы начали изучать Zig, скорее всего, пройдёт немного времени до того
|
||||
момента, когда Вы осознаете, что в нём специального синтаксиса для
|
||||
создания интерфейсов. Но, вероятно, Вы заметите некоторые вещи, которые
|
||||
номинально не являются таковыми, но очень на них похожи, например,
|
||||
`std.mem.Allocator`. Это потому, что в Zig действительно нет простого
|
||||
механизма для создания интерфейсов (например, ключевых слов `interface` и
|
||||
`implements`), но, тем не менее, сам по себе язык вполне пригоден для
|
||||
достижения аналогичных целей.
|
||||
|
||||
|
||||
## Простая реализация интерфейса
|
||||
|
||||
Мы сейчас реализуем простой интерфейс, который будет называться `Writer`.
|
||||
Он будет достаточно прост для понимания сути и будет содержать
|
||||
всего одну функцию. Если надо больше, они легко добавляются.
|
||||
Прежде всего, вот сам интерфейс:
|
||||
|
||||
```zig
|
||||
const Writer = struct {
|
||||
ptr: *anyopaque,
|
||||
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
|
||||
|
||||
fn writeAll(self: Writer, data: []const u8) !void {
|
||||
return self.writeAllFn(self.ptr, data);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Это вполне себе законченный интерфейс. Это структура, в которой два поля,
|
||||
`ptr` (это указатель на *нечто*, что этот интерфейс реализует,
|
||||
а про `*anyopaque` мы вскоре поговорим) и `writeAllFn`, это
|
||||
указатель на функцию, которая реализует данный метод.
|
||||
|
||||
Заметим, что функция `writeAll`, входящая в состав нашей стркутуры-интерфейса,
|
||||
просто вызывает реализацию по указателю `writeAllFn`, передавая ей,
|
||||
наряду с другими параметрами, указатель `ptr`.
|
||||
|
||||
Теперь скелет того самого нечто, что реализует данный интерфейс:
|
||||
|
||||
```zig
|
||||
const File = struct {
|
||||
fd: os.fd_t,
|
||||
|
||||
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
|
||||
// What to do with ptr?!
|
||||
}
|
||||
|
||||
fn writer(self: *File) Writer {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.writeAllFn = writeAll,
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Сразу отметим, что функция `writer` это то, как мы из нашей сущности
|
||||
"достаём" `Writer`, то есть собственно, интерфейс. Это примерно так же,
|
||||
как мы получаем `std.mem.Allocator` при помощи вызова `gpa.allocator()`.
|
||||
В целом, тут нет ничего странного и необычного, за исключением, пожалуй,
|
||||
одного факта, а именно, присваивания `.ptr = self`, слева у нас
|
||||
`*anyopaque`, справв `*File`. В общем, мы тут просто создаём некую
|
||||
структуру, а что касается этого присваивания, то в нём тоже нет чего-то
|
||||
сверх-особенного. Автоматическое приведение типов в Zig должно
|
||||
гарантировать безопасность и отсутствие неоднозначностей, а эти два
|
||||
требования при присваивании какого угодно указателя указателю с типом
|
||||
`*anyopaque` выполняются всегда.
|
||||
|
||||
В примере мы опустили часть, которая должна связать всё воедино, а
|
||||
именно: что делать с `ptr: *anyopaque`, который передаётся из интерфейса
|
||||
обратно в нашу его реализацию? Указатель с типом `*anyopaque` это
|
||||
указатель на что-то с неизвестным типом и размером, то есть это своего
|
||||
рода обобщённый указатель, который может хранить адрес чего угодно (тут
|
||||
уместно вспомнить `void*` из языка C, это по сути абсолютно то же самое).
|
||||
Вполне ясно, почему `Writer.ptr` должен иметь именно такой тип - ведь
|
||||
рассматриваемый интерфейс может быть реализован не только в структуре
|
||||
`File`, но в других тоже, поэтому типом `ptr` не может быть `*File`.
|
||||
Природа интерфейсов такова, что в процессе компиляции совершенно
|
||||
неизвестно, какие нам попадутся реализации, поэтому использование
|
||||
"универсального" указателя остаётся единственной возможностью.
|
||||
|
||||
Тут важно понимать один (возможно, не вполне очевидный) момент: когда мы
|
||||
в функции `writer` делаем `.ptr = self`, происходит так называемое
|
||||
"стирание типа". Указатель `self` как показывал на то место в памяти, где
|
||||
находится экземпляр структуры `File`, так и продолжает показывать, но,
|
||||
поскольку слева от знака присваивания обобщённый указатель, компилятор
|
||||
после этого уже не знает, на что именно он показывает. И как нам вернуть
|
||||
это знание обратно? Это можно сделать при помощи встроенных функций
|
||||
`@ptrCast` и `@alignCast`:
|
||||
|
||||
```zig
|
||||
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
|
||||
const self: *File = @ptrCast(@alignCast(ptr));
|
||||
_ = try std.os.write(self.fd, data);
|
||||
}
|
||||
```
|
||||
|
||||
Функция `@ptrCast` как бы преобразует указатель одного типа в указатель
|
||||
другого типа, при этом тип, к которому нужно привести, выводится
|
||||
из типа того, чему присваивается значение обобщённого указателя,
|
||||
в примере это `*File`. Мы как бы говорим компилятору: "дружище,
|
||||
верь мне, я знаю, что делаю - дай мне указатель, который показывает
|
||||
*туда же*, куда и `ptr`, но только теперь рассматривай содержимое
|
||||
этой области памяти как `File`". Как Вы, наверное, поняли, `@ptrCast` это
|
||||
мощная и полезная штука, поскольку позволяет нам рассматривать какую-то
|
||||
область памяти как что угодно. Но если мы ошибёмся и преобразуем
|
||||
"универсальный" указатель к типу, который не соответствует тому, что
|
||||
реально содержится в памяти по данному адресу, то, кроме себя, винить
|
||||
будет некого, компилятор тут нам не поможет. А последствия такой ошибки
|
||||
могут быть самые печальные, причём аварийное завершение программы это
|
||||
далеко не самый худший исход.
|
||||
|
||||
Функция `@alignCast` это более сложная штука. Существуют зависящие от
|
||||
вида процессора правила по поводу выравнивания данных в памяти. Например,
|
||||
для некоторых моделей CPU необходимо, чтобы, скажем, число с плавающей
|
||||
точкой (`f32`) располагалось по адресу, кратному размеру этого числа, то
|
||||
есть четырём. Тип `anyopaque` всегда имеет фактор выравнивания, равный
|
||||
единице (то есть может располагаться по любому адресу). А вот у структуры
|
||||
`File` фактор выравнивания равен четырём. Если хотите, Вы можете сами это
|
||||
увидеть, напечатав значения `@alignOf(File)` и `@alignOf(anyopaque)`.
|
||||
Поэтому подобно тому, как нам нужна `@ptrCast` для указания компилятору,
|
||||
какой именно тип мы имеем ввиду, так и `@alignCast` нужна для того,
|
||||
чтобы сообщить информацию о выравнивании. При этом, как и `@ptrCast`,
|
||||
`@alignCast`выводит требуемый фактор выравнивания исходя из типа левой
|
||||
части оператора присваивания.
|
||||
|
||||
Итак, вот полная реализация:
|
||||
|
||||
```zig
|
||||
const Writer = struct {
|
||||
ptr: *anyopaque,
|
||||
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
|
||||
|
||||
fn writeAll(self: Writer, data: []const u8) !void {
|
||||
return self.writeAllFn(self.ptr, data);
|
||||
}
|
||||
};
|
||||
|
||||
const File = struct {
|
||||
fd: os.fd_t,
|
||||
|
||||
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
|
||||
const self: *File = @ptrCast(@alignCast(ptr));
|
||||
// os.write might not write all of `data`, we should really look at the
|
||||
// returned value, the number of bytes written, and handle cases where data
|
||||
// wasn't all written.
|
||||
_ = try std.os.write(self.fd, data);
|
||||
}
|
||||
|
||||
fn writer(self: *File) Writer {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.writeAllFn = writeAll,
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Вся суть сводится к двум вещам: использованию `*anyopaque` для хранения
|
||||
указателя на реализацию в любой структуре и `@ptrCast(@alignCast(ptr))`
|
||||
для восстановления корректной информации о типе.
|
||||
|
||||
И ещё одно (второстепенное) замечание. Поле `ptr` в интерфейсе не может
|
||||
иметь тип `anyopaque`, оно должно быть именно указателем, то есть иметь
|
||||
тип `*anyopaque`. Это связано с тем, что `anyopaque` это тип с
|
||||
неизвестным размером, а в Zig все типы должны иметь известный размер.
|
||||
Интерфейс `Writer` имеет вполне определённый размер, поскольку это два
|
||||
указателя, размеры которых известны всегда. А если бы мы использовали тип
|
||||
`anyopaque`, то размер бы этой структуры был бы неизвестен.
|
||||
|
||||
## Делаем лучше
|
||||
|
||||
Вышеприведённая реализация интерфейса `Writer` вполне хороша и, к тому
|
||||
же, она требует знания лишь небольшого "кусочка магии". Некоторые
|
||||
интерфейсы из стандартной библиотеки (например, `std.mem.Allocator`)
|
||||
именно так примерно и сделаны. Что касается конкретно интерфейса
|
||||
`std.mem.Allocator`, в него входит несколько функций (а не одна, как а
|
||||
нашем примере с `Writer`), поэтому там указатели на все эти функции
|
||||
объединены в структуру, которая называется `VTable`.
|
||||
|
||||
Главный недостаток такой схемы состоит в том, что использовать методы
|
||||
структур, реализующих какой-то интерфейс, можно только через этот
|
||||
интерфейс, а "напрямую" нельзя. Мы не можем использовать `file.writeAll`
|
||||
непосредственно, поскольку у реализаций `writeAll` нет параметра с типом
|
||||
`*File`, там везде `*anyopaque`. Вообще говоря, использование чего-то
|
||||
только через интерфейс это само по себе очень даже неплохо, но если нам
|
||||
по каким-то причинам нужно, чтобы реализации можно было бы использовать
|
||||
как сами по себе, так и через интерфейс, то описанная в предыдущем
|
||||
разделе схема не сработает.
|
||||
|
||||
Иными словами, мы хотим, чтобы `File.writeAll` был нормальным методом,
|
||||
без необходимости манипуляций с обобщёнными указателями, как то вот так:
|
||||
|
||||
```zig
|
||||
fn writeAll(self: *File, data: []const u8) !void {
|
||||
_ = try std.os.write(self.fd, data);
|
||||
}
|
||||
```
|
||||
|
||||
Это можно сделать, но для этого придётся изменить сам интерфейс:
|
||||
|
||||
```zig
|
||||
const Writer = struct {
|
||||
// These two fields are the same as before
|
||||
ptr: *anyopaque,
|
||||
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
|
||||
|
||||
// This is new
|
||||
fn init(ptr: anytype) Writer {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
pub fn writeAll(pointer: *anyopaque, data: []const u8) anyerror!void {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.Pointer.child.writeAll(self, data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.writeAllFn = gen.writeAll,
|
||||
};
|
||||
}
|
||||
|
||||
// This is the same as before
|
||||
pub fn writeAll(self: Writer, data: []const u8) !void {
|
||||
return self.writeAllFn(self.ptr, data);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Тут у нас появилась функция `init`. Она довольно заковыристая и, чтобы
|
||||
понять, что там делается, лучше сначала думать в рамках нашей изначальной
|
||||
реализации. Суть всего кода `init` в том, чтобы превратить `*anyopaque` в
|
||||
конкретный тип, такой как `*File`. Это легко было сделать внутри самого
|
||||
`File`, поскольку там мы по самому определению знаем, к чему надо
|
||||
привести универсальный указатель. Но тут, чтобы это понять, нам нужна
|
||||
некоторая дополнительная информация.
|
||||
|
||||
Чтобы лучше понять `init`, давайте посмотрим, как эта функция
|
||||
используется. Функция `File.writer`, которая ранее создавала интерфейс
|
||||
непосредственно, теперь далает это не так:
|
||||
|
||||
```zig
|
||||
fn writer(self: *File) Writer {
|
||||
return Writer.init(self);
|
||||
}
|
||||
```
|
||||
|
||||
Отсюда мы видим, что в `init` передаётся ссылка на структуру с
|
||||
реализацией интерфейса, в данном случае это `*File` (а в сигнатуре `init`
|
||||
мы видим `anytype`!). Далее вступают в дело встроенные функции `@TypeOf`
|
||||
и `@typeInfo`, которые в Zig являются основными для работы во время
|
||||
компиляции. Первая из них возвращает тип указателя, в нашем случае это
|
||||
будет `*File`, а вторая возвращает маркированную структуру (tagged
|
||||
union), которая полностью описывает тип. Далее мы видим, что создаётся
|
||||
некоторая вложенная структура (`gen`), которая также содержит реализацию
|
||||
`writeAll`. Это как раз и есть то место, где `*anyopaque` преобразуется к
|
||||
нужному типу и далее вызывается функция с конкретной реализацией. Эта
|
||||
структура необходима, потому что в Zig нет анонимных функций. Поэтому нам
|
||||
нужно, чтобы `Writer.writeAllFn` была оформлена в виде этой небольшой
|
||||
обёртки и использование вложенной структуры это единственный способ
|
||||
сделать это.
|
||||
|
||||
Очевидно, что `file.writer()` это что-то, что будет работать во время
|
||||
выполнения программы, что подбивает нас думать, что всё внутри
|
||||
`Writer.init`, вызываемой из `file.writer()`, создаётся во время
|
||||
исполнения. Тут впору спросить про время жизни внутренней структуры
|
||||
`gen`, особенно с учётом того, что интерфейс могут реализовывать многие
|
||||
структуры. Но в действительности всё, кроме инструкции возврата, `init`
|
||||
это генерация кода во время компиляции. То есть для каждого типа `ptr`,
|
||||
используемого в программе, компилятор Zig создаст свою версию `init`. В
|
||||
этом смысле `init` это некий шаблон для компилятора, и всё это потому,
|
||||
что параметр `ptr` имеет тип `anytype`. Когда функция `file.writer()`
|
||||
вызывается во время работы программы, для каждого типа, реализующего
|
||||
интерфейс `Writer`, будет своя функция `Writer.init`.
|
||||
|
||||
В первоначальной версии каждая реализация интерфейса была ответственна за
|
||||
преобразование `*anyopaque` к тому, что нужно для данной реализации, что
|
||||
и делалось с помощью `@ptrCast(@alignCast(ptr))`. В новой, более
|
||||
изощрённой версии, каждая реализация всё так же должна это делать, мы
|
||||
просто изловичились и встроили это в сам интерфейс, задействовав
|
||||
возможности Zig по генерации исходного кода во время компиляции.
|
||||
|
||||
Последняя часть этого кода это вызов функции посредством
|
||||
`ptr_info.Pointer.child.writeAll(self, data)`. Встроенная функция
|
||||
`@@typeInfo(T)` возвращает `std.builtin.Type`, а это, как уже было
|
||||
отмечено, маркированная объединение, которое описывает тот или иной тип.
|
||||
Она может описывать 24 разновидности типов, как то целые числа,
|
||||
необязательные значения, структуры, указателя и т.д. Каждый тип имеет
|
||||
свои особенные свойства. Так, целые числа обладают "знаковостью"
|
||||
(`signedness`), в то время как другие типы такого свойства не имеют. Вот
|
||||
так выглядит `@typeInfo(*File)`:
|
||||
|
||||
```zig
|
||||
builtin.Type{
|
||||
.Pointer = builtin.Type.Pointer{
|
||||
.address_space = builtin.AddressSpace.generic,
|
||||
.alignment = 4,
|
||||
.child = demo.File,
|
||||
.is_allowzero = false,
|
||||
.is_const = false,
|
||||
.is_volatile = false,
|
||||
.sentinel = null,
|
||||
.size = builtin.Type.Pointer.Size.One
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поле `child` это фактический тип сущности, на которую показывает
|
||||
указатель. Когда мы вызываем `init`, передавая ей `*File`,
|
||||
`ptr_info.Pointer.child.writeAll(...)` транслируется в
|
||||
`File.writeAll(...)`, то есть получается именно то, что мы и хотели.
|
||||
|
||||
Если Вы посмотрите на другие применения такой витиеватой схемы,
|
||||
Вы там можете обнаружить, что `init` делает ещё кое-что после
|
||||
получения информации о типе:
|
||||
|
||||
```zig
|
||||
if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
|
||||
if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single item pointer");
|
||||
```
|
||||
|
||||
Таким образом делаются дополнительные проверки во время компиляции,
|
||||
а именно, проверяется, что `ptr` это указатель и не просто указаьтель,
|
||||
а указатель на одиночную сущность (а не на массив, в Zig это разные вещи).
|
||||
|
||||
Также вместо вызова функции посредством
|
||||
|
||||
```zig
|
||||
ptr_info.Pointer.child.writeAll(self, data);
|
||||
```
|
||||
|
||||
можно встретить такое
|
||||
|
||||
```zig
|
||||
@call(.always_inline, ptr_info.Pointer.child.writeAll, .{self, data});
|
||||
```
|
||||
|
||||
Вызов функции напрямую (как мы это и делали) по сути ничем не отличается
|
||||
от вызова функции про помощи встроенной функции `@call`, за исключением того,
|
||||
что использование `@call` предоставляет некоторые дополнительные
|
||||
возможности посредством её первого параметра (`CallModifier`). Как можно
|
||||
видеть из приведённого примера, мы можем попросить компилятор не делать
|
||||
вызов функции в буквальном смысле, а встроить тело функции в точку вызова.
|
||||
|
||||
Будем надеяться, что эта глава прояснила детали конструирования
|
||||
интерфейсов в Zig, а также, возможно, обрисовала такие возможности
|
||||
языка, которые не были никак затронуты в предыдущих главах.
|
||||
Однако, для простых случаев, когда все реализации заведомо известны,
|
||||
можно применять другой подход, о чём мы поговорим в следующем разделе.
|
||||
|
||||
## Использование маркированных объединений
|
||||
|
||||
|
||||
Reference in New Issue
Block a user