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

Начнем с небольшого теста. Какой вывод у программы ниже?

class A {
func execute(ind: Int = 0) {
print("A: \(ind)")
}

}

class B: A {
override func execute(ind: Int = 1) {
print("B: \(ind)")
}
}

let instance: A = B()
instance.execute()

Выводится «B: 0».

Разберемся, как это получилось.


Диспетчеризация методов

Когда выполняемая программа сталкивается с вызовом метода, она должна направляться по адресу реализации этого метода, которая, как мы порой ожидаем, определяется только во время выполнения.

В Swift имеются механизмы диспетчеризации, различающиеся по скорости.

  1. Диспетчеризация статическая.
  2. С таблицей виртуальных методов.
  3. Диспетчеризация сообщений.

Два последних относятся к «динамической диспетчеризации»: у них одно поведение при определении реализации, используемой во время выполнения, но разная производительность.

Рассмотрим различия между статической и динамической диспетчеризацией.


Динамическая диспетчеризация

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

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

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


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

Во время компиляции в компиляторе уже выбрана реализация, применяемая при вызове метода. Когда вызывается функция, в программе осуществляется переход непосредственно к адресу, сгенерированному при компиляции.

Иногда это называют «прямой диспетчеризацией».


Определение механизма диспетчеризации

В Swift ради повышения производительности приоритет всегда отдается статической диспетчеризации.

Примеры

Типы значений

Поскольку структуры и перечисления не поддерживают наследование, при всех вызовах методов используется статическая диспетчеризация. Компилятор «знает», что во время выполнения для каждого метода будет только одна реализация.

Протокол

Все методы, определенные в самом протоколе, диспетчеризуются динамически. Однако любой метод, определенный внутри расширения протокола, диспетчеризуется статически:

protocol Shape {
func draw() // динамически
}

extension Shape {
func area() {
print("area") // статически
}
}

Как вам этот код? Казалось бы, все просто и понятно, но нет.

Вот мина № 1:

protocol Shape {
func draw()
}

extension Shape {
func draw() {
print("Shape")
}
func area() {
print("Shape")
}
}

class Circle: Shape {
func draw() {
print("Circle")
}
func area() {
print("Circle")
}
}

let circle: Shape = Circle()
circle.draw() // "Circle", диспетчеризуется динамически
circle.area() // "Shape", диспетчеризуется статически

Здесь метод area диспетчеризуется статически, а еще реализован в классе Circle, но в компиляторе все равно для выполнения выбирается реализация по умолчанию Shape.

Этот результат слишком странный. Отладка займет много часов, если не знать о статической/динамической диспетчеризации.

Класс

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

Как разработчику оптимизировать ее производительность в компиляторе?

В компиляторе статическая диспетчеризация метода выполняется в любом из этих случаев.

  1. Класс или метод помечен как final (конечный).
  2. Метод помечен как private (закрытый), он не переопределяется подклассами.
  3. Метод определяется в расширении.

При аннотировании метода с помощью @objc и dynamic механизм диспетчеризации переопределяется на динамическую диспетчеризацию.

class A {
func foo() { } // динамически
private func bar() { } // статически
final func bas() { } // статически
}

extension A {
func doWork() { } // статически
}

final class B {
func doWork() { } // статически
}

Поэтому неплохо бы любой новый класс помечать при создании как final и удалять, когда требуется наследование, а все методы  —  как private и удалять при необходимости.

Мина № 2:

Вернемся к коду в начале статьи:

class A {
func execute(ind: Int = 0) {
print("A: \(ind)")
}

}

class B: A {
override func execute(ind: Int = 1) {
print("B: \(ind)")
}
}

let instance: A = B()
instance.execute()

Откуда взялся вывод «B: 0»? Здесь вызывается метод A, который использует значение по умолчанию (0), а динамическая диспетчеризация выполняется только для реализации. В итоге получается реализация B и определение метода A.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Ahmed Salah: Method dispatching mines in Swift

Предыдущая статьяПочему не стоит писать простой код JavaScript?
Следующая статьяКак создать опрос удовлетворенности сотрудников с Angular и сохранить его результаты в коллекции MongoDB