Паттерн Посетитель и двойная диспетчеризация
Рассмотрим пример, в котором у нас есть небольшая иерархия классов геометрических фигур (осторожно, псевдокод):
Нам нужно добавить внешнюю операцию над всеми этими компонентами, например, экспорт. В нашем языке (Java, C#, . ) есть перегрузка методов, поэтому мы создаём такой класс:
Кажется, что всё хорошо. Но давайте испробуем такой класс в деле:
Побывать в шкуре компилятора
Примечание: всё что здесь описано – правда для большинства современных объектных языков программирования (Java, C#, PHP и другие).
Позднее/динамическое связывание
Давайте представим себя компилятором. Вам нужно понять как скомпилировать такой код:
Итак, вызов метода draw в классе Shape . Но нам известно ещё и о четырёх классах переопределяющих этот метод. Можно ли уже сейчас понять какую реализацию нужно выбрать? Похоже, что нет, ведь для этого придётся запустить программу и узнать какой же объект будет подан в параметр. Но одно вы знаете точно – какой бы объект ни был передан, он точно будет иметь реализацию draw .
В результате машинный код, который вы создадите, будет каждый раз при проходе через этот участок проверять что за объект этот shape , и выбирать реализацию метода draw из соответствующего класса.
Такая динамическая проверка типа называется поздним или динамическим связыванием:
- Поздним, потому что мы связываем объект и реализацию уже после компиляции.
- Динамическим, потому что мы делаем это при каждом прохождении через этот участок.
Раннее/статическое связывание
Теперь давайте «скомпилируем» такой код:
С созданием объекта всё ясно. Как насчёт вызова метода export ? В классе Exporter у нас есть пять версий метода с таким именем, которые отличаются только типом параметра. Похоже, здесь тоже придётся динамически отслеживать тип передаваемого параметра и по нему определять, какой из методов выбрать.
Но здесь нас ждёт засада. Что если кто-то подаст в метод exportShape такой объект, для которого не существует метода export в классе Exporter ? Например, объект Ellipse , для которого у нас нет экспорта. Действительно, у нас нет гарантии что необходимый метод будет существовать, как это было с переопределенными методами. А значит, возникнет неоднозначная ситуация.
Именно поэтому все разработчики компиляторов выбирают безопасную тропинку и применяют раннее или статическое связывание для перегруженных методов:
- Раннее, потому что оно происходит ещё на этапе компиляции программы.
- Статическое, потому что его уже не изменить во время выполнения.
Вернемся к нашему примеру. Мы уверены в том, что имеем параметр с типом Shape . Мы знаем что в Exporter существует подходящая реализация: export(s: Shape) . Значит, этот участок кода мы жёстко связываем с известной реализацией метода.
И поэтому даже если мы подадим в параметрах один из подклассов Shape , всё равно будет вызвана реализация export(s: Shape) .
Двойная диспетчеризация (double dispatch)
Двойная диспетчеризация или double dispatch – это трюк, позволяющий обойти ограниченность раннего связывания в перегруженных методах. Вот как это делается:
Послесловие
Хотя паттерн Посетитель и построен на механизме двойной диспетчеризации, это не основная его идея. Посетитель позволяет добавлять операции к целой иерархии классов, без надобности менять код этих классов.
Диспетчеризация процессов
Стратегии планирования. Дисциплины диспетчеризации. Вытесняющие и невытесняющие алгоритмы. Алгоритмы планирования без переключений. Циклическое и приоритетное планирование. Динамические приоритеты. Планирование в системах реального времени. Планирование потоков. Гарантии обслуживания процесса
Существует задача такой организации работы параллельных процессов, при которой они как можно реже конфликтуют из-за имеющихся в системе ресурсов. Эта задача называется планированием . Сейчас наиболее актуальны задачи динамического (краткосрочного) планирования. Эти задачи называются диспетчеризацией . Планирование осуществляется значительно реже, чем задачи текущего распределения ресурсов между уже выполняющимися процессами. При долгосрочном планировании планировщик решает, какой из процессов, находящихся во входной очереди, должен быть переведен в очередь готовых процессов в случае освобождения ресурсов памяти. При этом он пытается спланировать активные процессы таким образом, чтобы в списке готовых процессов находились как процессы, занятые преимущественно вводом-выводом, так и процессы, занятые преимущественно вычислениями. Краткосрочный планировщик решает, какая из задач в очереди готовых должна быть передана на исполнение. В современных ОС долгосрочный планировщик зачастую отсутствует. Для обычных персональных компьютеров планирование не играет важной роли, поскольку большую часть времени активен только один процесс, а процессорное время перестало быть дефицитным ресурсом. Для рабочих станций и серверов планирование играет важную роль, поскольку доступ к процессору пытаются получить одновременно несколько процессов. Помимо выбора задачи для выполнения планировщик должен решать и задачу эффективного использования процессора, поскольку переключение контекста требует затрат: переключение из режима пользователя в режим ядра, сохранение состояния текущего процесса, карту памяти (признаки обращения к страницам памяти), запуск следующего процесса, а также в большинстве случаев перезагрузка кэша.
Все процессы можно разделить на 2 большие группы: процессы, занятые в основном вычислениями, и изредка требующими операций ввода-вывода (ограниченные возможностями процессора), и процессы, большую часть времени ожидающими вво- да-вывода (ограниченные возможностями устройств в/в). С увеличением быстродействия процессоров, процессы все более смещаются в сторону задач, ограниченных возможностями устройств вв.
Планирование необходимо в следующих случаях:
1. Создание нового процесса. Требуется принятие решения, какой процесс – родительский или дочерний запускать.
2. Завершение процесса. Необходимо выбрать из очереди готовых процессов.
3. При блокировке процесса. Требуется выбор, какой процесс следующий. Иногда причина блокировки может влиять на выбор. Если А – высокоприоритетный процесс, и он блокируется в ожидании выхода процесса В из КС, то можно запустить процесс В, чтобы быстрее продолжил работу А.
4. При прерывании в/в. Если прерывание пришло от устройства, завершившего операцию, можно запустить процесс, ожидающий именно этого устройства.
Стратегия планирования определяет каким образом следует выбирать процессы в очереди, чтобы достичь оптимальной эффективности при выполнении параллельных процессов. Три наиболее известных стратегии следующие:
— по возможности процессы завершаются в том же самом порядке, в каком они были начаты.
— отдавать предпочтение коротким процессам
— всем процесса предоставлять одинаковое время ожидания.
Дисциплина диспетчеризации – совокупность правил, в соответствии с которыми формируется очередь готовых к выполнению задач. Дисциплины диспетчеризации могут быть разбиты на 2 класса: вытесняющие и невытесняющие . Если после выбора процесса он работает вплоть до блокировки или пока сам не отдаст управление другому процессу, это невытеснящая многозадачность. Другими словами, используются алгоритмы планирования без переключений. В случае, когда принудительно средствами ОС выполняется переключение между задачами, речь идет о вытесняющей многозадачности. При этом используются алгоритмы с переключениями. В современных ОС реализуется как правило вытесняющая многозадачность. Процесс выбирается и ему предоставляется какое-то количество времени. Если за этот отрезок процесс не завершил работы, он приостанавливается, и запускается другой процесс. Как правило, для организации переключений используется прерывание от таймера. В случае вытесняющей многозадачности механизм диспетчеризации целиком сосредоточен в ОС, что снимает с программиста обязанность о передаче управления другим процессам системы, т.е. он может программировать задачу так, как если бы задача запускалась в однопроцессной системе. При невытесняющей многозадачности управление системой может теряться на некоторый (в общем случае на достаточно большой) период времени, величина которого в значительной мере зависит от степени эффективной передачи управления иным процессам. Примером такой системы может служить
Для сравнения алгоритмов диспетчеризации используют следующие критерии :
— Загрузка центрального процессора. В большинстве персональных систем средняя загрузка процессора не превышает 2-3%, на серверах – до 20-40%
— Пропускная способность. Измеряется количеством выполненных процессов за час
— Среднее время оборота. Время от момента появления процесса во входной очереди до его завершения. Включает в себя время ожидания во входной очереди, время ожидания в очереди готовых процессов, время ожидания устройств вв, время выполнения.
— Время ожидания. Суммарное время нахождения процесса в очереди готовых процессов
— Время отклика. Для интерактивных программ является важным показателем. Время, прошедшее от момента поступления команды до получения результата.
Выбор конкретного алгоритма определяется классом решаемых задач и целями , которых стараются достичь. К таким целям относятся:
— справедливость. Гарантия того, что каждому процессу будет предоставлена определенная часть времени процессора, причем без возникновения ситуации голодовки.
— Эффективность. Процессор должен быть максимально нагружен. В идеальном варианте на 100%.
— Сокращение полного времени выполнения
— Сокращение времени ожидания
— Сокращение времени отклика.
Кроме того, желательно, чтобы алгоритмы обладали следующими свойствами :
— предсказуемость. Одно и то же задание должно выполняться примерно за одинаковое время
— минимизация накладных расходов, т.е. время на выбор очередного процесса, переключение контекста и т.д.
— равномерная загрузка ресурсов системы
— масштабируемость, т.е. при увеличении нагрузки не должна теряться работоспособность алгоритма.
Классификация дисциплин диспетчеризации:
Дисциплины диспетчеризации удобно классифицировать по типу оптимизации под конкретный класс задач: для систем пакетной обработки данных, для интерактивных систем, для систем реального времени.
Диспетчеризация FCFS (First Come First Served) – в порядке очереди. Самый простой вариант, использует невытесняющие алгоритмы (без переключений). Задачи обслуживаются в том порядке, в котором они возникли. Как правило, формируется общая очередь задач. Задачи, которые были заблокированы в процессе выполнения, после окончания ожидания (блокировки) ставятся в очередь перед теми процессами, которые еще не выполнялись либо в конец очереди. В итоге реализуется стратегия завершения процессов в том порядке, в котором они были начаты. Дисциплина не требует внешнего вмешательства в процесс вычислений, не происходит перераспределение процессорного времени. Эта дисциплина достаточно просто реализуются, требует малых затрат на реализацию очереди задач. Однако при увеличении нагрузки на систему, среднее время ожидания обслуживания возрастает. При этом короткие задачи ожидают столько же, сколько и длинные. Например, запускаются три задачи, время выполнения которых составляет 13, 4, 1 квант машинного времени. Тогда время ожидания процессов составит: 0, 13, 13+4=17 квантов, а полное время выполнения 0+13=13, 13+4=17, 17+1=18 квантов, соответственно среднее время ожидания 0+13+17=30/3=10 квантов, среднее время выполнения 13+17+18=48/3=16 квантов. Т.о. среднее время ожидания и среднее полное время выполнения зависят от порядка расположения процессов, поэтому алгоритм практически неприменим в системах разделения времени – т.к. среднее время отклика велико.
Дисциплина SJN (shortest job next) – следующим выполняется кратчайшее задание. Для задач должна быть известна оценка потребностей в машинном времени. Необходимость сообщать ОС о потребности задач в машинном времени привела и к появлению соответствующих языков, например, JCL (job control language). Пользователи указывали предполагаемое время, а чтобы не было явно заниженных оценок, использовался подсчет реальных потребностей. Диспетчер сравнивал заявленное время и расчетное, и если оценка явно занижалась, задача попадала не в начало, а в конец очереди. Заблокированные в процессе выполнения задачи попадают в конец очереди готовых к выполнению задач. Невытесняющая. Для того же примера: время ожидания составит 0, 1, 1+4=5, полное время выполнения 0+1=1, 1+4=5, 5+13=18, среднее время ожидания
0+1+5=6/3=2, среднее время выполнения 1+5+18=24/3=8.
Дисциплина SRT (shortest remaining time) – следующее задание требует наименьшего времени для завершения. В отличие от предыдущего случая, после блокировки задача может попасть в начало очереди, если для завершения требует минимум времени. При поступлении новой задачи ее время выполнения сравнивается с оставшимся временем до завершения текущей задачи, и если пришедшая задача короче, текущий процесс останавливается, а управление переходит новой задаче. Невытесняющая.
Трехуровневое планирование . По мере поступления новые задачи сначала помещаются в очередь на диске. Планировщик выбирает задание и передает его системе, остальные остаются в очереди. При этом может быть использован любой алгоритм
выбора задачи из очереди. Попав в систему, для задачи запускается соответствующий процесс, и он конкурирует за доступ к процессору. При большом количестве процессов часть из них сбрасывается на диск. Это второй уровень планирования – какие из процессов сбросить, а какие оставить. Этим занят планировщик памяти. Третий уровень планирования отвечает собственно за доступ процессов к процессору.
Дисциплина RR (Round Robin). Циклическая. Предполагает, что каждый процесс получает время порциями (квантами). После окончания выделенного кванта времени (или при блокировке процесса), задача снимается с выполнения и запускается следующая. Снятая задача ставится в конец очереди готовых задач. Рассмотрим тот же пример, при условии, что квант времени постоянен и равен 4.
Время ожидания 5, 4, 8. Полное время исполнения – 5+13=18, 4+4=8, 8+1=9. Среднее время ожидания 5+4+8=17/3=5.67. Среднее полное время выполнения 18+8+9=35/3=11.67. Величина кванта влияет на производительность. Пусть квант равен
Время ожидания 5, 5, 2. Полное время исполнения – 5+13=18, 5+4=9, 2+1=3. Среднее время ожидания 5+5+2=12/3=4. Среднее полное время выполнения 18+9+3=30/3=10. Для оптимальной работы системы требуется правильно выбрать закон, по которому распределяются кванты времени. Величина кванта выбирается как компромисс между приемлемой реакцией системы на действия пользователя и накладными расходами на смену контекста задачи. В случае, если квант достаточно мал, реакция системы будет высокой, однако частая смена контекста снизит производительность системы. Если же квант велик, то при высокой производительности значительно снижается реакция системы. В некоторых ОС величина кванта указывается явно. Например, OS/2 имела переменную в системном файле конфигурации TIMESLICE, которая позволяла указывать минимальную и максимальную величину кванта. Если процесс прервался из-за окончания кванта времени, то новый выделяемый ему квант будет увеличен на время одно периода таймера, и так до тех пор, пока она не сравняется с максимальной. Это позволяет эффективнее распределять время для длительных задач. Циклическая дисциплина одна из самых распространенных.
Приоритетное планирование. В случае бесприоритетного обслуживания выбор задачи производится в некотором заранее установленном порядке без учета их относительной важности и времени обслуживания. В случае с приоритетом у каждой задачи есть приоритет, в зависимости от которого она с большей или меньшей частотой (вероятностью) попадает в выполнения. Если приоритет задач не изменяется со временем, то это диспетчеризация с фиксированными приоритетами, если же он изменяется в процессе выполнения задачи, то это диспетчеризация с динамическим приоритетом. Этот вариант требует дополнительных временных затрат на расчет приоритетов, зато позволяет обеспечить гарантированное обслуживание процесса. Фиксированные приоритеты часто используются в ОС реального времени.
Использование динамических приоритетов.
Часто используется две составляющих приоритета – первая, заданная жестко при создании процесса, и вторая, формируемая диспетчером задач, и изменяемая в зависимости от текущей ситуации. Чтобы высокоприоритеные процессы не работали постоянно, не предоставляя время низкоприоритетным, с течением времени, по прошествии каждого кванта, приоритет процесса снижается. В то же время, периодически ядро пересчитывает текущие приоритеты процессов, готовых к запуску, увеличивая их. В итоге приоритет текущего процесса оказывается меньше, чем у одного из очереди, и происходит переключение. При динамическом назначении приоритетов для процессов, активно использующих устройства вв, т.е. требующих мало процессорного времени, приоритет может выбираться как 1/f, где f – часть использованного времени кванта, т.е. если процесс использовал 1/10 часть кванта, то приоритет назначается 10, если 1/25, то 25. Часто удобно группировать приоритеты по классам, используя приоритетное планирование между классами, но циклическое внутри класса. Пока в классе с высшими приоритетами есть задачи, они запускаются согласно циклическому планированию. Если их нет, обрабатывается очередь процессов более низкого приоритета. Во всех ОС имеются средства для изменения приоритета процесса.
Многоуровневые очереди с обратной связью . Развитие систем с динамическим приоритетом, в которых задачи разбиваются на классы приоритетов. В данном методе задачи могут перемещаться между классами. Например, класс с наивысшим приоритетом имеет квант, равный 8. Следующий по приоритетности класс – 16, следующий – 32, и последний класс обслуживается не циклически, а в порядке очереди. Задача изначально поступает в класс с максимальным приоритетом. Если процесс отработал эти кванты и требует еще времени, он переводится в менее приоритетный класс, если и там ему не хватило времени, то в еще менее приоритетный класс. В итоге слишком длинные задачи попадут в последний класс. В результате, чем длиннее процесс, тем в более низкий по приоритету класс он попадет, в результате время ожидания увеличивается, но зато количество предоставляемых квантов увеличивается. При завершении ожидания (например, от устройств в/в) процессы могут помещаться в более приоритетный класс задач.
Гарантированное планирование . При этом варианте гарантируется предоставление процессу определенной доли процессорного времени, тогда как в системе с приоритетами нет гарантии обслуживания.
Лотерейное. Процессам предоставляются “лотерейные билеты” на доступ к различным ресурсам, в т.ч. и к процессору. Когда планировщику необходимо принять решение, случайным образом выбирается лотерейный билет, и его обладатель получает доступ. Для важных процессов раздается больше лотерейных билетов, повышая шанс на выигрыш. В итоге каждый про-
цесс в среднем получает такую долю ресурса, какая доля билетов у него находится. Взаимодействующие процессы могут обмениваться лотерейными билетами. Например, если клиент посылает запрос серверу и блокируется в ожидании, он может предварительно передать все билеты серверу, чтобы ускорить получение ответа. Сервер, выполнив запрос, возвращает лотерейные билеты.
Справедливое. Все предыдущие стратегии не учитывают, что многие процессы могут быть созданы одним и тем же пользователем. Т.о., если один пользователь создал несколько процессов для решения одной задачи, а другой –только один, то первый получит и большую часть процессорного времени, что не справедливо. Поэтому перед планированием можно обращать внимание на хозяина процесса.
В системах реального времени время является самым важным критерием. Задача разделяется на несколько процессов, каждый из которых предсказуем, как правило это очень короткие процессы. При этом могут возникать внешние сигналы, как периодические (с определенной частотой), так и непериодические. Может случиться так, что система не в состоянии обработать все возникшие события за необходимое время. Если в систему поступает m периодических событий, событие i поступает с периодом Pi, и обрабатывается за Ci секунд, то все потоки могут быть обработаны только при следующем условии:
Системы, удовлетворяющие этому условию, называются планируемыми . В случае статических алгоритмов планирования
все решения планирования приняты заранее, до запуска системы. Во втором случае решения принима-
ются по мере функционирования системы. Статическое возможно только при наличии достоверной
информации о работе и временных ограничениях. Динамическое планирование не требует этого.
Диспетчеризация в Swift: погружение в теорию и практику
Все iOS-разработчики так или иначе сталкиваются с диспетчеризацией (Method Dispatch), но далеко не каждый понимает, как это работает. Зная, как проходит процесс диспетчеризации под капотом программы, вы можете повысить производительность своего кода.
В этой статье мы разберем типы диспетчеризации, их плюсы и минусы, а также затронем один из распространённых багов.
Материал будет полезен для всех iOS-разработчиков, которые хотят улучшить производительность своего кода. Кроме того, этот материал поможет при подготовке к собеседованию, где вопросы о Method Dispatch встречаются достаточно часто.
Что такое диспетчеризация в Swift?
Это процесс, при котором программа выбирает, какие инструкции выполнить при вызове метода. Диспетчеризация происходит каждый раз, когда вызывается метод. Многим кажется, что это происходит так:
Рис 1. Неправильное понимание отработки метода.
Это заблуждение, потому что есть промежуточный пункт — это диспетчеризация.
Рис 2. Правильное понимание отработки метода.
Цель диспетчеризации состоит в том, чтобы программа сообщила центральному процессору, где в памяти он может найти исполняемый код для вызова конкретного метода.
Типы диспетчеризации
Перейдем к типам диспетчеризации на языке Swift. Существует три вида: Direct Dispatch (статическая), Table Dispatch (динамическая, в свою очередь, делится на Virtual Table и Witness Table) и Message Dispatch (самая динамическая диспетчеризация).
Рис 3. Типы диспетчеризации.
1. Direct Dispatch
Direct Dispatch — это самый быстрый способ отправки метода, часто называют статической диспетчеризацией. Однако прямая отправка наиболее ограничивающая с точки зрения программирования и недостаточно динамична для ООП. У всех value-объектов (например, структуры) используется Direct Dispatch.
Рис 4. Плюсы и минусы Direct Dispatch.
Рассмотрим практические примеры Direct Dispatch:
Final Class
При добавлении final становится недоступным наследование этого класса, соответственно, метод тоже нельзя оверрайднуть. Возможно, вы часто видели такое и не понимали, для чего это делается. А это один из плюсов — изменение диспетчеризации на Direct Dispatch и, соответственно, улучшение производительности кода. Ну и защита от наследования, когда оно не нужно, например, при создании класса ViewController, единственного в приложении.
Protocol Extension
При реализации дефолтного метода протокола с помощью его расширения, диспетчеризация c Witness Table (об этом больше информации представлено ниже) меняется на Direct Dispatch.
Class Extension
При написании метода класса в его расширении данный метод нельзя будет оверрайднуть, и он меняет свою диспетчеризацию на Direct Dispatch.
Access Control
К приватному методу нет доступа, чтобы его как-то переписать у наследника (сабкласса), и соответственно он меняет свою диспетчеризацию на Direct Dispatch.
Данные практические примеры Direct Dispatch используются при разработке приложений для улучшения производительности кода, а также для защиты участков кода от переписывания сабклассами. Они помогут повысить скорость вашего кода и сделать его более профессиональным, старайтесь не забывать об этом.
2. Table Dispatch
Virtual Table
Virtual Table используется в наследовании. Для каждого класса и его наследника (сабкласса) создается виртуальная таблица (пример приведён ниже), по которой центральный процессор понимает, где искать нужную ссылку на метод для его выполнения. Главный минус динамической диспетчеризации в том, что ее скорость существенно ниже, чем у статической.
Рис 5. Плюсы и минусы Virtual Table.
Рассмотрим Virtual Table на практическом примере:
Создается класс и его сабкласс, для каждого из этих классов создается отдельная виртуальная таблица.
Рис 6. Virtual Table для данного кода.
Witness Table
Witness Table используется для реализации протоколов и создается для каждого класса, реализовавшего протокол. По этой таблице центральный процессор понимает, где искать нужную ссылку на метод для его выполнения. Главный минус Witness Table такой же, как и у Virtual Table — скорость существенно ниже, чем у Direct Dispatch.
Рис 7. Плюсы и минусы Witness Table.
Рассмотрим Witness Table на практическом примере:
Создается протокол и два класса, которые реализуют этот протокол, для каждого из этих классов создается Witness Table.
Рис 8. Witness Table для данного кода.
3. Message Dispatch
Message Dispatch — это самый динамичный вызов метода с помощью Objective-C. Message Dispatch работает в рантайме и показывает, какой метод вызывать, то есть проверяет это в реальном времени. Message Dispatch лежит в основе KVO (и соответственно в реактивном программировании), UIAppearance, CoreData. Так как Message Dispatch работает в рантайме, соответственно, можно подменять реализацию методов — это называется Method Swizzling. Method Swizzling позволяет подменить метод вашим в рантайме, оставляя оригинальную имплементацию доступной. А также в рантайме можно менять экземпляры класса.
Message Dispatch часто используется для тестирования кода. Редко его можно встретить в проде, так как это не очень безопасный вызов и относится к самой медленной диспетчеризации. Но иногда он встречается и в проде, когда существующая библиотека не может обеспечить нам нужный результат и разработчик делает замену метода на свой в рантайме с помощью Method Swizzling.
Рис 9. Плюсы и минусы Message Dispatch.
Для реализации Message Dispatch требуется префикс «@objc dynamic», или можно добавить @objcMembers перед классом, тогда все его методы станут с префиксом @objc по дефолту.
Рассмотрим пример из того, что разобрали выше:
Тут присутствуют все типы диспетчеризации. Так как остальные типы мы уже разобрали сверху, разберем только Message Dispatch. В рантайме при вызове метода doSomething сначала будет выполняться поиск в данном сабклассе, если имплементация метода не найдется, то будет поиск по родительскому классу. Если и там его не окажется, то поиск переходит в класс NSObject. Если метод не найдется, а такое может произойти при указании метода через функцию performSelector, то случится краш с ошибкой: «NSInvalidArgumentException: unrecognized selector sent to instance».
Баг с диспетчеризацией
Вместе с тем в Swift существуют различные баги с диспетчеризацией от Apple. Многие их них были пофикшены с выходом новых версий языка. Ниже разберем баг с протоколом, который до сих пор присутствует при разработке, и приведём пример, как его пофиксить.
По логике, два раза в консоль должна выводиться строчка «Required Implementation», но реальный результат таков:
Это происходит, потому что во втором случае система выбирает дефолтную реализацию протокола и это Direct Dispatch, а мы осуществляем в нашем классе свою реализацию, и под капотом этот метод переходит в Witness Table.
Для того, чтобы программа поняла, что в нашем случае мы используем Witness Table, нам нужно добавить этот метод в протокол.
После добавления результат становится правильным:
Итоги
Итак, мы разобрали, как работает диспетчеризация. Это обширная тема, изучив которую, вы поймете, как методы реализуются под капотом на самом деле. Например, сможете смело заменять многие методы на Direct Dispatch, где ранее использовалась другая диспетчеризация по умолчанию. От этого ваш код станет лучше и быстрее.
Спасибо за внимание! Хорошего кодинга 🙂
Полезные материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.
Диспетчеризация методов в Swift
Начнем с небольшого теста. Какой вывод у программы ниже?
Разберемся, как это получилось.
Диспетчеризация методов
Когда выполняемая программа сталкивается с вызовом метода, она должна направляться по адресу реализации этого метода, которая, как мы порой ожидаем, определяется только во время выполнения.
В Swift имеются механизмы диспетчеризации, различающиеся по скорости.
- Диспетчеризация статическая.
- С таблицей виртуальных методов.
- Диспетчеризация сообщений.
Два последних относятся к «динамической диспетчеризации»: у них одно поведение при определении реализации, используемой во время выполнения, но разная производительность.
Рассмотрим различия между статической и динамической диспетчеризацией.
Динамическая диспетчеризация
Этим механизмом выбирается реализация метода, используемая во время выполнения. Когда дело доходит до вызова функции, в программе начинается поиск корректной реализации, которая должна выполняться, и осуществляется переход к ней. Этот этап поиска становится накладными расходами и замедляет программу.
Почему применяется динамическая диспетчеризация? Она гибкая. Благодаря ей разработчики определяют метод только раз и предоставляют для него несколько реализаций, а корректная реализация выбирается в компиляторе.
Большинство использует динамическую диспетчеризацию, не замечая этого. С ней становится возможным существование полиморфизма и шаблона Protocol. В этих случаях нужно лишь определить метод в одном месте и реализовать его в нескольких классах — остальная работа за динамической диспетчеризацией.
Статическая диспетчеризация
Во время компиляции в компиляторе уже выбрана реализация, применяемая при вызове метода. Когда вызывается функция, в программе осуществляется переход непосредственно к адресу, сгенерированному при компиляции.
Иногда это называют «прямой диспетчеризацией».
Определение механизма диспетчеризации
В Swift ради повышения производительности приоритет всегда отдается статической диспетчеризации.
Примеры
Типы значений
Поскольку структуры и перечисления не поддерживают наследование, при всех вызовах методов используется статическая диспетчеризация. Компилятор «знает», что во время выполнения для каждого метода будет только одна реализация.
Протокол
Все методы, определенные в самом протоколе, диспетчеризуются динамически. Однако любой метод, определенный внутри расширения протокола, диспетчеризуется статически:
Как вам этот код? Казалось бы, все просто и понятно, но нет.
Вот мина № 1:
Здесь метод area диспетчеризуется статически, а еще реализован в классе Circle , но в компиляторе все равно для выполнения выбирается реализация по умолчанию Shape .
Этот результат слишком странный. Отладка займет много часов, если не знать о статической/динамической диспетчеризации.
Класс
Класс может наследоваться, поэтому диспетчеризация по умолчанию динамическая.
Как разработчику оптимизировать ее производительность в компиляторе?
В компиляторе статическая диспетчеризация метода выполняется в любом из этих случаев.
- Класс или метод помечен как final (конечный).
- Метод помечен как private (закрытый), он не переопределяется подклассами.
- Метод определяется в расширении.
При аннотировании метода с помощью @objc и dynamic механизм диспетчеризации переопределяется на динамическую диспетчеризацию.
Поэтому неплохо бы любой новый класс помечать при создании как final и удалять, когда требуется наследование, а все методы — как private и удалять при необходимости.
Мина № 2:
Вернемся к коду в начале статьи:
Откуда взялся вывод «B: 0»? Здесь вызывается метод A , который использует значение по умолчанию (0), а динамическая диспетчеризация выполняется только для реализации. В итоге получается реализация B и определение метода A .