Начнем с небольшого теста. Какой вывод у программы ниже?
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 имеются механизмы диспетчеризации, различающиеся по скорости.
- Диспетчеризация статическая.
- С таблицей виртуальных методов.
- Диспетчеризация сообщений.
Два последних относятся к «динамической диспетчеризации»: у них одно поведение при определении реализации, используемой во время выполнения, но разная производительность.
Рассмотрим различия между статической и динамической диспетчеризацией.
Динамическая диспетчеризация
Этим механизмом выбирается реализация метода, используемая во время выполнения. Когда дело доходит до вызова функции, в программе начинается поиск корректной реализации, которая должна выполняться, и осуществляется переход к ней. Этот этап поиска становится накладными расходами и замедляет программу.
Почему применяется динамическая диспетчеризация? Она гибкая. Благодаря ей разработчики определяют метод только раз и предоставляют для него несколько реализаций, а корректная реализация выбирается в компиляторе.
Большинство использует динамическую диспетчеризацию, не замечая этого. С ней становится возможным существование полиморфизма и шаблона 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
.
Этот результат слишком странный. Отладка займет много часов, если не знать о статической/динамической диспетчеризации.
Класс
Класс может наследоваться, поэтому диспетчеризация по умолчанию динамическая.
Как разработчику оптимизировать ее производительность в компиляторе?
В компиляторе статическая диспетчеризация метода выполняется в любом из этих случаев.
Класс
или метод помечен какfinal
(конечный).- Метод помечен как
private
(закрытый), он не переопределяется подклассами. - Метод определяется в расширении.
При аннотировании метода с помощью
@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
.
Читайте также:
- Создай и играй: код для игры “Змейка” с кнопками управления в SwiftUI
- Реализация цифрового конверта в iOS
- 3 эффективные новинки Swift с WWDC 2022
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ahmed Salah: Method dispatching mines in Swift