В чем разница между статической и динамической диспетчеризацией в Swift?

Ответ

Диспетчеризация — это механизм определения того, какая реализация метода должна быть вызвана. В Swift есть два основных типа.

Определения

  • Статическая диспетчеризация (Static Dispatch, раннее связывание): Решение о вызываемом методе принимается во время компиляции. Компилятор знает точный адрес функции и может выполнить прямой вызов или даже встраивание (inline).
  • Динамическая диспетчеризация (Dynamic Dispatch, позднее связывание): Решение принимается во время выполнения программы. Используется таблица (например, Virtual Table для классов или Witness Table для протоколов) для поиска нужной реализации.

Сравнение

Характеристика Статическая диспетчеризация Динамическая диспетчеризация
Время разрешения Время компиляции. Время выполнения (Runtime).
Скорость Быстрее. Нет накладных расходов на поиск в таблице, возможна оптимизация. Медленнее. Требуется косвенный вызов через таблицу.
Полиморфизм Не поддерживает полиморфное поведение. Поддерживает полиморфизм (вызов метода зависит от фактического типа объекта).
Гибкость Менее гибкая. Более гибкая, основа ООП.
Типы в Swift Структуры (struct), перечисления (enum), final классы и методы, глобальные функции. Не-final классы и их методы, вызовы через протоколы.

Примеры кода

// 1. СТАТИЧЕСКАЯ ДИСПЕТЧЕРИЗАЦИЯ (на примере структуры)
struct Calculator {
    // Для структур компилятор использует статическую диспетчеризацию.
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
}
let calc = Calculator()
let result = calc.add(5, 3) // Компилятор ЗНАЕТ, что вызовется Calculator.add

// 2. ДИНАМИЧЕСКАЯ ДИСПЕТЧЕРИЗАЦИЯ (на примере классов)
class Animal {
    func speak() { print("...") } // Метод попадает в Virtual Table класса Animal
}
class Dog: Animal {
    override func speak() { print("Woof!") } // Метод попадает в Virtual Table класса Dog
}

let myPet: Animal = Dog() // Статический тип Animal, динамический (реальный) - Dog
myPet.speak() // В RUNTIME система смотрит в vtable объекта Dog и вызывает Dog.speak() -> "Woof!"

// 3. ДИНАМИЧЕСКАЯ через протокол (Witness Table)
protocol Drawable {
    func draw()
}
struct Circle: Drawable {
    func draw() { print("Drawing a circle") }
}
let shape: Drawable = Circle() // Тип протокола
shape.draw() // В RUNTIME вызов через Witness Table для Circle -> "Drawing a circle"

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

Ответ 18+ 🔞

Давай разберём эту дичь, которая в учебниках выглядит как заумная хуйня, а на деле проще пареной репы.

Представь, что ты звонишь другу. Есть два сценария, блядь.

Сценарий первый, статический (раннее связывание). Ты заранее знаешь, что у Васи дома только стационарный телефон, и ты звонишь прямо на него. Ты набираешь номер — тебя соединяют сразу. Быстро, чётко, никаких сюрпризов. Компилятор, как ты, уже всё знает и сразу лепит вызов куда надо. Это и есть статическая диспетчеризация.

Сценарий второй, динамический (позднее связывание). А вот у Пети, пидораса, три номера: домашний, рабочий и мобильный. Ты звонишь на «Петю». А оператор (это runtime, программа во время работы) смотрит в свою табличку: «А, Петя сейчас на рыбалке, на мобильном». И соединяет тебя с мобильным. Дольше? Ну да, потому что надо таблицу посмотреть. Зато гибко! Петя может быть где угодно. Это и есть динанамическая диспетчеризация. Полиморфизм, ёпта!

А теперь на пальцах и с кодом, чтобы не ебал мозг.

Кто как диспетчеризуется?

  • Быстро и статически (как Вася с домашним телефоном):

    • Все структуры (struct) и перечисления (enum). Они по умолчанию не поддерживают наследование, поэтому компилятор про них всё знает.
    • final классы и методы. Сказал final — всё, приехали, переопределять нельзя, значит можно оптимизировать нахуй.
    • Обычные функции, не привязанные к классам.
  • Медленно, но гибко (как Петя-бродяга):

    • Обычные не-final классы. Вот тут начинается магия полиморфизма и таблицы виртуальных методов (vtable).
    • Протоколы. Тут своя отдельная таблица — witness table. Та же хуйня, только в профиль.
// ПРИМЕР 1: Статика, всё ясно как божий день.
struct Танк {
    func выстрелить() { print("Ба-бах!") }
}
let мойТанк = Танк()
мойТанк.выстрелить() // Компилятор тут уже ржёт: "Да я знаю, что это Танк.выстрелить(), вызываю сразу!"

// ПРИМЕР 2: Динамика, ёперный театр с классами.
class Транспорт {
    func сигналить() { print("Бип!") } // Метод в таблицу (vtable) родителя
}
class Грузовик: Транспорт {
    override func сигналить() { print("БА-БААААХ!") } // Метод в таблицу (vtable) наследника
}

let мойТранспорт: Транспорт = Грузовик() // Тип в коде "Транспорт", а по факту-то "Грузовик"!
мойТранспорт.сигналить() // Runtime: "Так-с, объект типа Грузовик... Лезу в его таблицу... Нашёл! Грузовик.сигналить()!" -> "БА-БААААХ!"

// ПРИМЕР 3: Динамика через протоколы (та же песня, другой оркестр).
protocol Рисуемый {
    func нарисовать()
}
struct Квадрат: Рисуемый {
    func нарисовать() { print("Рисую квадратик") }
}
let фигура: Рисуемый = Квадрат()
фигура.нарисовать() // Runtime лезет в witness table для Квадрат и находит реализацию.

Вывод, блядь, практический: Если ты делаешь класс и точно знаешь, что его не будут наследовать и методы не будут переопределять — ставь final! Компилятор скажет тебе спасибо, вырежет лишние телодвижения и сделает всё быстрее. Не надо плодить лишнюю динамику там, где можно обойтись статикой. Всё гениальное — просто, как три копейки.