Как стать автором
Обновить

Method Dispatch

Время на прочтение4 мин
Количество просмотров16K

Вы когда-нибудь задумывались над тем, как компилятор понимает, какую функцию и откуда вызвать? Постараемся разобраться.

Что такое Method Dispatch?

Method Dispatch - это алгоритм, который решает, какой метод должен вызываться в ответ на сообщение. Его цель заключается в том, чтобы проинформировать процессор о том, где он может найти код для вызова метода в памяти.

Swift имеет три типа method dispatch:

  1. Static Dispatch

  2. Table Dispatch

  3. Message Dispatch

В чем отличие разных типов диспетчеризации?

Как уже говорилось, в Swift их три, но почему нельзя было сделать один или два? Дело в том, что разные типы применимы в разных ситуациях для большей производительности, либо же какие-то типы вообще не будут работать в тех или иных случаях, а в каких мы сейчас разберемся, когда поговорим обо всех трех отдельно.

  • StaticDispatch

    Начнем с самого быстро и простого типа диспетчеризации, а именно со статичного. Самый быстрый он по той причине, что компилятор знает, что вызывать еще на стадии сборки. Поэтому компилятор может оптимизировать эти вызовы достигая минимального количества ассемблерных инструкций. Но такой подход не работает во всех случаях и не применим в работе с подклассами, так как требует единственной реализации метода, который будет лежать в одном месте в памяти.

  • TableDispatch

    Второй способ это табличная диспетчеризация. Такой метод использует массив указателей для всех методов объявленных в классе. У каждого подкласса есть своя копия таблицы с новыми указателями для каждого метода, которые были переопределены классом. Каждый новый метод в подклассах добавляется в конец массива. Отсюда и “Table” в название.

    Рассмотрим небольшой пример того, как это работает:
    Для этого создадим родительский класс и отнаследуем от него ребенка, в котором переопределим один из методов.

    class ParentClass {
    	func method1() {}
    	func method2() {} 
    }
    
    class ChildClass: ParentClass { 
    	override func method2() {}
    	func method3() {}
    }

    Давайте посмотрим как будут выглядеть наши массивы с указателями для обоих классов.

    В массиве подкласса указатель на функцию не изменился и остался таким же как у родителя, в то время как наш переопределенный метод имеет новый указатель. Что же происходит при вызове нашего метода? На самом деле все очень просто, убедимся в этом на примере:

    let childClass = ChildClass() 
    childClass.method2()

    При выполнение такого кода будет происходить следующее:

    1. Берется и читается таблица с нашими указателями у объекта childClass. 

    2. Читается индекс нашей функции в массиве, в данном случае это 1.

    3. Переходим в адрес 0х222.

    Этот метод довольно прост с точки зрения реализации, а характеристики производительности предсказуемы. Однако этот метод отправки по-прежнему медленный по отношению к статическому. Со стороны байт-кода есть два дополнительных чтения и переход, которые которые делают его менее производительным. Однако еще один пункт из-за которого этот метод медленный - неспособность компилятором оптимизировать вызовы.

    Также одним из недостатков является то, что реализация на основе массива не позволяет расширениям расширить нашу таблицу, так как подклассы добавляют новые методы в конец массива, то нет индекса куда расширение могло бы безопасно добавить новый указатель на функцию.

  • MessageDispatch

    Когда есть класс, который является подклассом какого-то класса, который является подклассом какого-то другого класса и так далее, то приложение во время выполнения решает, какой метод какого родителя нужно использовать. Когда сообщение отправляется, среда выполнения просматривает иерархию классов, чтобы определить, какой метод вызывать. Это самый медленный метод.
    Ключевым компонентом этой функциональности является то, что она позволяет разработчикам изменять поведение отправки во время выполнения.
    Классы Swift обычно не используют такой тип диспетчеризации, поскольку это концепция Objective-C, поэтому нам нужно использовать dynamic для принудительного использования такого типа диспетчеризации.

    Так в чем же отличие Table dispatch от Message dispatch? Все очень просто таблица для table dispatch создается во время компиляции, в то время как для message dispatch во время выполнения, это связано с тем, что message dispatch это способ пришедший из “очень run time” языка Objective-C.

Обобщим все типы в одну таблицу

Как управлять диспетчеризацией?

  1. Управлять тем, как будет вызван наш метод, можно поместив его в определенные места нашего класса. Так, например, поместив метод в расширение мы будем уверены, что он вызовется статично.

  2. Так же зависит кому принадлежит наш метод: если это структура, то мы также можем быть уверены, что все вызовы будут статичны, но если принадлежит классу, то будет использована table dispatch. Для протоколов схема такая же как и для классов. Но стоит обратить внимание на то, что у подклассов NSObject в расширениях используется message dispatch.

  3. Как можно было заметить table и message dispatch используется с методами, которые могут быть переопределены, из этого следует, что если мы скажем компилятору, что данный метод не может быть override, то он будет вызываться статически. Как это сделать? Для того, что мы метод нельзя было переиспользовать используется ключевое слово final.

  4. Так же как и с final можно использовать private, если компилятор выведет, что никаких переиспользований метода помеченного private нет, то вызов будет происходить также с помощью static dispatch.

  5. Как уже говорилось ранее, можно включить message dispatch, используя слово dynamic, это также делает метод доступным для Objective-C. Чтобы использовать dynamic, нужно импортировать Foundation, так как он включает NSObject и ядро среды выполнения Objective-C.

  6. @objc и @nonobjc изменяют то, как метод воспринимается Objective-C. Чаще всего @objc используется для пространства имен селектора. @objc не изменяет выбор отправки, он просто делает метод доступным для Objective-C. Однако @nonobjc изменяет выбор отправки и отключает message dispatch, поскольку он не добавляет метод в среду выполнения Objective-C, от которой зависит отправка сообщений. По сути поведение такое же как и с final.

  7. Одним из тонких моментов является использование final вместе с @objc. final позволит использовать static dispatch при непосредственном вызове метода, в то время как @objc позволит среде Objective-C использовать message dispatch, например, в selector’е.

Визуализируем все способы и подведем итоги

Источники

Теги:
Хабы:
Всего голосов 8: ↑6 и ↓2+5
Комментарии0

Публикации