Написать пост

Диспетчеризация методов в Swift: что это и как работает

Аватарка пользователя Max Nechaev

Сегодня мы поговорим про механизм диспетчеризации методов. Он существует в разных языках, но мы сделаем акцент именно на Swift.

Обложка поста Диспетчеризация методов в Swift: что это и как работает

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

Всем привет, меня зовут Макс Нечаев, я тех-лид и iOS разработчик в крупном фудтех стартапе на арабском рынке (Snoonu).

Что такое диспетчеризация простым языком? Диспетчеризация — это процесс, результатом которого является выбор имплементации метода. Хорошо, давайте попробуем проще. Диспетчеризация ловит вызов метода и выбирает его реализацию.

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

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

Диспетчеризация методов в Swift: что это и как работает 1

Всего у нас три типа диспетчеризации

Первая: Static dispatch — статическая.

Вторая: Table dispatch — динамическая, которая делится на две:

– Virtual Table

– Witness Table

Третья: Message dispatch

Далее предлагаю рассмотреть все три более подробно.

Static dispatch

Статическая диспетчеризация самая быстрая. Зачастую разработчики стараются привести именно к этому типу вызова методов. Почему так? Потому что уже на этапе комплиляции известно, какую имплементацию метода нужно использовать.

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

Примеры методов со Static dispatch:

			final class SampleFinalClass {
    func sayHello() {
        print("Hello!")
    }
}

struct SampleStruct {
    func sayHello() {
        print("Hello!")
    }
}

extension SampleStruct {
    func someFunc() {
        print("Hello!")
    }
}
		

Преимущества: самая быстра диспетчеризация.

Недостатки: отсутствие полиморфизма и наследия.

Table dispatch

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

Так как это происходит именно в рантайме, думаю, вы понимаете, что здесь скорость диспетчеризации может повлиять на производительность. Но несмотря на это, есть и явные плюсы Table dispatch. Далее мы рассмотрим два подвида этой диспетчеризации.

Virtual Table

Если говорить про Virtual Table Dispatch, то скорее всего мы имеем ввиду работу с классами, с наследованием и переопределением методов.

Сейчас я попробую на простом примере рассказать как работает этот тип диспетчеризации. Сначала я сделаю это текстом, затем возьму в пример небольшую реализацию и пройдем по ней для закрепления.

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

Давайте создадим два класса, отец (Father) и ребенок (Child). Причем ребенок должен быть наследником класса отца. У отца будут методы: есть, гулять, спать, работать.

			class Father {
    func eat() {}
    func walk() {}
    func sleep() {}
    func work() {}
}

class Child: Father {
}

let child = Child()
child.eat()
		

Смотрите, это очень интересный момент. У каждого класса своя виртуальная таблица. Но при это когда у child мы вызываем метод eat(), вызывается имплементация из класса Father. Почему так? Потому что адреса методов в виртуальной таблице ребенка ведут на те же адреса, что и в виртуальной таблице класса отца.

Вопрос на подумать. Как же сделать так, чтобы у виртуальной таблицы класса Child были свои уникальные адреса на имплементацию этих методов?

А вот как:

			class Child: Father {
    override func eat() {}
    override func walk() {}
    override func sleep() {}
    override func work() {}
}
		

Мы переопределяем методы, теперь Virtual Table Dispatch у класса Child будет вызывать именно его имплементацию методов, а не класса Father.

Witness Table

Когда мы говорим про Witness Table Dispatch, то здесь скорее всего мы имеем ввиду работу с протоколами. На уровне теории, для класса, который реализует протокол, а именно для методов этого протокола, создается отдельная таблица. Если класс реализует несколько протоколов, то создается несколько Witness таблиц. Основное отличие от Virtual table, что здесь нет наследования.

Давайте попробуем разобраться на примере. Создадим два класса: брат и сестра (Brother / Sister). Также создадим протокол GameProtocol, который обязует класс реализовать метод func buyGame().

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

			protocol GameProtocol {
    func buyGame()
}

class Brother: GameProtocol {
    func buyGame() { print("AssassinsCreed") }
}

class Sister: GameProtocol {
    func buyGame() { print("Barbie") }
}
		

Каждый класс имеет свою Witness Table и сам решает, как ему реализовать протокол.

Подведем небольшой итог Table dispatch.

Преимущества: наличие полиморфизма (гибкости), возможность наследования

Недостатки: медленнее, чем статическая диспетчеризация

Message dispatch

Динамичный вид диспетчеризации, самый медленный и связан с Objective-C. Он полностью работает в runtime. Для работы с Message Dispatch методам добавляется модификатор @objc dynamic. Как он работает под капотом? В отличии от табличной диспетчеризации, Message Dispatch ищет реализацию метода в дочернем классе, затем по ссылке переходит в таблицу родительского класса и ищет там, если и там нет реализации, переходит на уровень ниже. Таким образом создается длинная цепочка поиска реализации метода от самого верхнего класса, до самого нижнего класса, именно это и делает данный тип диспатча самым медленным.

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

Преимущества: KVO и KVC, Method Swizzling

Недостатки: самая медленная диспетчеризация

Итог

В конце концов хочется сказать, что данная тема не самая ключевая в работе iOS разработчика, но при этом рекомендуется к пониманию. Более того, на собеседованиях вас часто будут спрашивать и гонять по диспетчеризации. Данных знаний вам должно хватить, чтобы ответить на 95% всех вопросов по теме. Более того, прилагаю вам такую схемку, которую можно держать перед глазами, она помогает очень просто и быстро понять, какая и где диспетчеризация.

Диспетчеризация методов в Swift: что это и как работает 2
Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий2К показов