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



Книга Диспетчеризация методов в 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.



66   0  
    Ничего не найдено.

Добавить ответ:
Отменить.