Продолжая тему, давайте рассмотрим типы протоколов и общий код. Попутно будут рассмотрены следующие вопросы:
- реализация полиморфизма без наследования и ссылочных типов
- как хранятся и используются объекты типа протокола
- как с ними работает диспетчеризация методов
Типы протоколов
Реализация полиморфизма без наследования и ссылочных типов:protocol Drawable { func draw() } struct Point: Drawable { var x, y: Int func draw() { .
} } struct Line: Drawable { var x1, x2, y1, y2: Int func draw() { .
} } var drawbles = [Drawable]() for d in drawbles { d.draw() }
- Обозначим протокол Drawable, у которого есть метод отрисовки
- Давайте реализуем этот протокол для Point и Line — теперь вы можете обращаться с ними как с Drawable (вызвать метод draw)
Основной принцип (ad-hoc) полиморфизма: «Общий интерфейс – множество реализаций»Динамическая отправка без виртуальной таблицы Напомним, что определение корректной реализации метода при работе с классами (ссылочными типами) достигается за счет динамической диспетчеризации и виртуальной таблицы.
Каждый тип класса имеет виртуальную таблицу, в которой хранятся реализации его методов.
Динамическая диспетчеризация определяет реализацию метода для типа, просматривая его виртуальную таблицу.
Все это необходимо из-за возможности наследования и переопределения методов.
В случае структур наследование, а также переопределение методов невозможно.
Тогда, на первый взгляд, в виртуал-столе нет необходимости, но как тогда будет работать динамическая отправка? Как программа может понять, какой метод будет вызван в d.draw()?
Стоит отметить, что количество реализаций этого метода равно количеству типов, соответствующих протоколу Drawable.
Таблица свидетелей протокола
есть ответ на этот вопрос.Эта таблица есть у каждого типа, реализовавшего какой-либо протокол.
Подобно виртуальной таблице классов, она хранит реализации методов, требуемых протоколом.
в будущем таблица протоколов-свидетелей будет называться «таблицей протокола-метода».Отлично, теперь мы знаем, где искать реализации методов.
Осталось только два вопроса:
- Как найти соответствующую таблицу протокол-метод для конкретного объекта, реализующего этот протокол? В нашем случае, как мы можем найти эту таблицу для элемента d массива drawables?
- Элементы массива должны быть одинакового размера (в этом суть массива).
Тогда как же рисуемый массив может удовлетворить этому требованию, если он может хранить как линию, так и точку, и они имеют разные размеры?
MemoryLayout.size(ofValue: Line(.
)) // 32 bits
MemoryLayout.size(ofValue: Point(.
)) // 16 bits
Ээкзистенциальный контейнер
Чтобы решить эти две проблемы, Swift использует специальную схему хранения экземпляров типов протоколов, называемую экзистенциальным контейнером.
Она выглядит так:
Принимает 5 машинных слов (в 64-битной системе 5 * 64 = 320 бит).
Разделен на три части: буфер значений — пространство для самого экземпляра vwt — указатель на таблицу-свидетеля значений pwt - указатель на таблицу свидетелей протокола Давайте подробнее рассмотрим все три части: Буфер контента Всего три машинных слова для хранения экземпляра.
Если экземпляр может поместиться в буфере содержимого, он сохраняется там.
Если экземпляр больше 3-х машинных слов, то он не поместится в буфер и программа вынуждена выделить память в куче, поместить туда экземпляр и поместить указатель на эту память в буфер содержимого.
Давайте посмотрим на пример: let point: Drawable = Point(.
)
Point() принимает 2 машинных слова и идеально помещается в буфер значений — программа добавит их туда:
let line: Drawable = Line(.
)
Line() занимает 4 машинных слова и не может поместиться в буфер значений — программа выделит для нее память в куче и добавит указатель на эту память в буфер значений:
ptr указывает на экземпляр Line(), расположенный в куче:
Таблица жизненного цикла Как и таблица протокол-метод, эта таблица есть у каждого типа, соответствующего протоколу.
Содержит реализацию четырех методов: выделить, скопировать, уничтожить, освободить.
Эти методы контролируют весь жизненный цикл объекта.
Давайте посмотрим на пример:
- При создании объекта ( Point(.
) as Drawable) вызывается метод allocate из Т.
Ж.
Ц.
этот объект. Метод allocate решит, где следует разместить содержимое объекта (в буфере значений или в куче), и если оно должно быть выделено в куче, он выделит необходимый объем памяти.
- Метод копирования поместит содержимое объекта в соответствующее место.
- После окончания работы с объектом будет вызван метод destruct, который уменьшит все счетчики ссылок, если они есть.
- После деструкции будет вызван метод освобождения, который освободит память, выделенную в куче, если таковая имеется.
Ээкзистенциальный контейнер - Ответы Таким образом, мы ответили на два поставленных вопроса:
- Таблица протокол-метод хранится в экзистенциальном контейнере этого объекта и может быть легко получена из него.
- Если тип элемента массива — протокол, то любой элемент этого массива занимает фиксированное значение в 5 машинных слов — именно столько и нужно для экзистенциального контейнера.
Если содержимое элемента невозможно поместить в буфер значений, оно будет размещено в куче.
Если это возможно, то все содержимое будет помещено в буфер значений.
В любом случае мы получаем, что размер объекта с типом протокола составляет 5 машинных слов (40 бит), а из этого следует, что все элементы массива будут иметь одинаковый размер.
let line: Drawable = Line(.
)
MemoryLayout.size(ofValue: line) // 40 bits
let drawables: [Drawable] = [Line(.
), Point(.
), Line(.
)]
MemoryLayout.size(ofValue: drawables._content) // 120 bits
Ээкзистенциальный контейнер — пример Рассмотрим поведение экзистенциального контейнера в этом коде:
func drawACopy(local: Drawable) {
local.draw()
}
let val: Drawable = Line(.
)
drawACopy(val)
Экзистенциальный контейнер можно представить следующим образом:
struct ExistContDrawable {
var valueBuffer: (Int, Int, Int)
var vwt: ValueWitnessTable
var pwt: ProtocolWitnessTable
}
Псевдокод За кулисами функция drawACopy принимает ExistContDrawable:
func drawACopy(val: ExistContDrawable) {
.
}
Параметр функции создается вручную: создаем контейнер, заполняем его поля из полученного аргумента:
func drawACopy(val: ExistContDrawable) {
var local = ExistContDrawable()
let vwt = val.vwt
let pwt = val.pwt
local.type = type
local.pwt = pwt
.
}
Мы решаем, где будет храниться контент (в буфере или куче).
Вызовите vwt.allocate и vwt.copy, чтобы заполнить local содержимым val: func drawACopy(val: ExistContDrawable) {
.
vwt.allocateBufferAndCopy(&local, val)
}
Вызываем метод draw и передаем ему указатель на себя (метод projectBuffer сам решит, где находится self — в буфере или в куче — и вернет правильный указатель): func drawACopy(val: ExistContDrawable) {
.
pwt.draw(vwt.projectBuffer(&local))
}
Заканчиваем работу с local. Очищаем все ссылки на хип от локальных.
Функция возвращает значение — очищаем всю память, выделенную для drawACopy (фрейм стека): func drawACopy(val: ExistContDrawable) {
.
vwt.destructAndDeallocateBuffer(&local)
}
Ээкзистенциальный контейнер – Цель Использование экзистенциального контейнера требует большой работы — пример выше это подтвердил — но зачем он вообще нужен, какова цель? Цель состоит в том, чтобы реализовать полиморфизм с использованием протоколов и типов, которые их реализуют. В ООП мы используем абстрактные классы и наследуем их, переопределяя методы.
В POP мы используем протоколы и реализуем их требования.
Опять же, даже при использовании протоколов реализация полиморфизма — это большая работа и энергозатратность.
Поэтому, чтобы избежать «лишней» работы, нужно понимать, когда полиморфизм нужен, а когда нет. Полиморфизм в реализации POP выигрывает от того, что при использовании структур нам не нужен постоянный подсчет ссылок и нет наследования классов.
Да, все очень похоже, классы используют виртуальную таблицу для определения реализации метода, протоколы используют таблицу протокол-метод. Классы размещаются в куче, там же иногда могут размещаться и структуры.
Но проблема в том, что на класс, помещенный в кучу, можно направить любое количество указателей, и необходим подсчет ссылок, а для структур, размещенных в куче, указатель только один и хранится он в экзистенциальном контейнере.
Фактически важно отметить, что структура, хранящаяся в экзистенциальном контейнере, сохранит семантику своего типа значения независимо от того, помещена ли она в стек или в кучу.
Таблица жизненного цикла отвечает за сохранение семантики, поскольку она описывает методы, определяющие семантику.
Ээкзистенциальный контейнер — сохраненные свойства Мы рассмотрели, как переменная типа протокола передается и используется функцией.
Давайте посмотрим, как хранятся такие переменные: struct Pair {
init(_ f: Drawable, _ s: Drawable) {
first = f
second = s
}
var first: Drawable
var second: Drawable
}
var pair = Pair(Line(), Point())
Как эти две структуры Drawable хранятся внутри структуры Pair? Каково содержание пары? Он состоит из двух экзистенциальных контейнеров — один для первого, другой для второго.
Строка не помещается в буфер и размещается в куче.
Точка помещается в буфер.
Это также позволяет структуре Pair хранить объекты разных размеров: pair.second = Line()
Теперь содержимое второго тоже помещается в кучу, так как не поместилось в буфер.
Давайте посмотрим, к чему это может привести: let aLine = Line(.
)
let pair = Pair(aLine, aLine)
let copy = pair
После выполнения этого кода программа получит следующее состояние памяти:
У нас в куче 4 выделения памяти, что нехорошо.
Попробуем это исправить:
- Создадим класс аналог Line
class LineStorage: Drawable {
var x1, y1, x2, y2: Double
func draw() {}
}
- Давайте использовать это в паре
let lineStorage = LineStorage(.
)
let pair = Pair(lineStorage, lineStorage)
let copy = pair
Получаем одно размещение в куче и 4 указателя на него:
Но мы имеем дело с референтным поведением.
Изменение copy.first повлияет на пару.
first (то же самое и на .
секунду), а это не всегда то, что нам нужно.
Косвенное хранение и копирование при записи (копирование при записи) Ранее упоминалось, что String представляет собой структуру копирования при записи (хранит свое содержимое в куче и копирует его при изменении).
Давайте посмотрим, как можно реализовать собственную структуру, которая копируется при изменении: struct BetterLine: Drawable {
private var storage: LineStorage
init() {
storage = LineStorage((0, 0), (10, 10))
}
func draw() -> Double { .
}
mutating func move() {
if !isKnownUniquelyReferenced(&storage) {
storage = LineStorage(self.storage)
}
// storage editing
}
}
- BetterLine хранит все свойства в хранилище, а хранилище является классом и хранится в куче.
- Память можно изменить только с помощью метода перемещения.
В нем мы проверяем, что на память указывает только один указатель.
Если указателей больше, то этот BetterLine с кем-то делит хранилище, и чтобы BetterLine полностью вел себя как структура, хранилище должно быть индивидуальным — делаем копию и дальше работаем с ней.
let aLine = BetterLine()
let pair = Pair(aLine, aLine)
let copy = pair
copy.second.x1 = 3.0
В результате выполнения этого кода получим:
Другими словами, у нас есть два экземпляра Pair, которые используют одно хранилище: LineStorage. При изменении хранилища у одного из своих пользователей (первого/второго) для этого пользователя будет создана отдельная копия хранилища, чтобы ее изменение не повлияло на других.
Это решает проблему нарушения семантики типов значений из предыдущего примера.
Типы протоколов — сводка
- Маленькие значения .
Если мы работаем с объектами, которые занимают мало памяти и могут быть помещены в буфер экзистенциального контейнера, то:
- не будет никакого распределения кучи
- нет подсчета ссылок
- полиморфизм (динамическая отправка) с использованием таблицы протоколов
- Большие значения.
- размещение кучи
- подсчет ссылок, если объекты содержат ссылки.
Продемонстрированы механизмы использования перезаписи изменений и косвенного хранения, которые позволяют существенно улучшить ситуацию с подсчетом ссылок в случае большого количества ссылок.Мы обнаружили, что типы протоколов, как и классы, способны реализовывать полиморфизм.
Это происходит за счет хранения в экзистенциальном контейнере и использования таблиц протоколов — таблицы жизненного цикла и таблицы протокол-метод. Теги: #Swift #copy #протокольно-ориентированное программирование #buffer #draw #свидетельская таблица протокола #существующий контейнер #drawable #allocate #destruct #deallocate #value buffer
-
Powershell: Выстрелил И Забыл
19 Oct, 24