Всем привет, с вами я, Наиль Габутдинов, iOS разработчик.

В разных языках программирования применяется механизм диспетчеризации методов (Method Dispatch), и все разработчики имеют с ним дело. Однако, не все это осознают и не все понимают, как этот механизм работает. Поэтому данная статья будет полезна всем разработчикам на Swift, которые хотят погрузиться глубже в диспетчеризацию методов и понять принципы ее работы, что в свою очередь позволит писать более эффективный код и избежать появления некоторых неочевидных проблем. Также статья будет полезна тем, кто в поисках работы – данный вопрос часто обсуждается на собеседованиях iOS разработчиков.

Давайте разберемся, что же такое диспетчеризация, рассмотрим ее виды и случаи применения.

По определению, диспетчеризация метода – это процесс выбора имплементации метода при его вызове – то есть процесс, который происходит между вызовом функции и ее выполнением, в ходе которого система определяет какую реализацию метода осуществить (если, к примеру, есть разные переопределения метода в дочерних классах или используются протоколы с их расширениями).

В Swift нам доступно несколько видов диспетчеризации:

  • Direct Dispatch (статическая), 

  • Table Dispatch (динамическая), в свою очередь, делится на:

    • Virtual Table,

    • Witness Table,

  • Message Dispatch (самая динамическая диспетчеризация).

Давайте подробнее рассмотрим эти виды.

Статическая (Direct) диспетчеризация

Статическая/прямая диспетчеризация определяет однозначно реализацию метода уже на этапе компиляции, поэтому это самый быстрый тип диспетчеризации. Он используется в методах value типов (струтур), final классов, extension классов и протоколов. То есть, можно сделать вывод, что статическая диспетчеризация применяется, когда метод не может быть переопределен.

Табличная (Table) диспетчеризация

Как следует из названия, в данном типе диспетчеризации формируется таблица с адресами методов сущности. Она создается на этапе компиляции, а сам адрес метода ищется по таблице уже во время runtime, что, само собой, сказывается на скорости работы в худшую строну.

Рассмотрим два подвида табличной диспетчеризации.

Virtual Table

Данный тип диспетчеризации в основном используется для случаев наследования, то есть для классов. Для каждого класса и для каждого его сабкласса создается виртуальная таблица с адресами методов, при этом для дочернего класса эта таблица копируется вместе с адресами. В случае когда реализация метода полностью переходит из родительского класса, адрес в таблице наследника такой же (в примере ниже это метод method).Если какой-то метод переопределен через override (метод method1), то адрес меняется на другую реализацию. Ниже добавляются другие методы, которые добавились в этом сабклассе (method2).

Ниже приведены достоинства и недостатки данного типа.

Witness Table

Witness Table выделен в Swift для работы с протоколами. В примере ниже для класса реализующего протокол NewProtocol создается отдельная таблица с адресами методов протокола, и при вызове метода у объекта с типом протокола NewProtocol используется Witness Table диспетчеризация. В случае реализации классом нескольких протоколов, для каждого протокола будет создаваться отдельная таблица. Отличие от Virtual Table заключается в отсутствии наследования.

Диспетчеризация на сообщениях (Message Dispatch)

Message Dispatch — это самый динамичный вид диспетчеризации, который в основном используется в связке с Objective-C. Message Dispatch работает полностью в runtime. На примере ниже видим, что тоже присутствуют родительский и дочерний классы, но родительский класс при этом наследуется от NSObject. Для работы Message Dispatch, методу добавляется модификатор @objc dynamic. Для каждого класса тоже есть таблицы, но они, в отличии от Table Dispatch, не копируются при наследовании, в таблице дочернего класса есть ссылка super на родительский класс. При вызове метода у объекта дочернего класса, сначала идет поиск адреса у самого класса. Если не адрес не найден, то поиск продолжается у родительского класса, и так далее до NSObject.

Еще одно отличие от Table Dispatch – все таблицы в этом виде диспетчеризации формируются в runtime, что значительно снижает скорость определения имплементации метода и делает его самым медленным.

Message Dispatch лежит в основе KVO и KVC. Так как Message Dispatch работает в runtime, появляется возможность подменить реализацию методов — это называется Method Swizzling. Method Swizzling позволяет подменить метод другим в runtime, при этом оригинальная имплементация остается доступной.

Ниже приведены плюсы и минусы данного типа диспетчеризации:

Итог

В результате мы понимаем, что при вызове любого метода в Swift до его обработки есть еще процесс диспетчеризации, в ходе которого определяется нужная имплементация. Мы рассмотрели разные виды диспетчеризации и увидели, как отличается их работа и для каких типов объектов применима та или иная диспетчеризация. Ниже привожу таблицу, по которой можно понять, какая диспетчеризация будет в том или ином случае:

А также таблицу, в которой отображены способы управления видом диспетчеризации в зависимости от типа объекта:

Комментарии (4)


  1. ws233
    06.02.2023 09:04

    На самый главный вопрос-то не ответили.

    Насколько различается самый медленный тип диспетчеризация от самого быстрого? В каких случаях имеет смысл учитывать скорость?


    1. gabutdinov Автор
      06.02.2023 10:01

      Разница во времени между static и message dispatch может быть кратной, зависит от конкретных условий. Я посчитал важным показать принципиальную разницу между видами и то, почему это влияет на скорость поиска реализации.


      1. ws233
        06.02.2023 10:38

        Кратной. Но в абсолютных цифрах-то это сколько? наносекунды? десятки наносекунд? сотни? Джуны это читают и потом на собесах категорично заявляют, что динамическую диспетчеризацию нельзя использовать ни в коем случае, потому что она "медленная" ^.^


        1. gabutdinov Автор
          06.02.2023 10:51
          +2

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