Урок по треку Swift и iOS 7 мин

Протоколы, POP и дженерики в Swift

Протоколы и расширения, диспетчеризация, associatedtype, some и any, ограничения дженериков.

Swift называют протокольно-ориентированным языком. На собесе спросят, что это значит, как работает диспетчеризация и чем some отличается от any.

Протоколы и расширения

Протокол описывает контракт, а расширение протокола даёт реализацию по умолчанию:

protocol Describable {
    var title: String { get }
    func describe() -> String
}

extension Describable {
    func describe() -> String { "Объект: \(title)" }
}

struct Product: Describable { let title: String }
Product(title: "Чайник").describe()   // реализация по умолчанию

Протокол может реализовать структура, enum, класс и даже чужой тип через ретроактивное расширение. В отличие от наследования, протоколов у типа сколько угодно. Это и есть POP: поведение собирается из протоколов, а не из иерархии классов.

Для делегатов протокол ограничивают классами, чтобы ссылку можно было сделать слабой:

protocol ListDelegate: AnyObject { func didSelect(_ id: Int) }
weak var delegate: ListDelegate?

Диспетчеризация: главная ловушка

  • Статическая: методы структур и enum, final-методы, методы, объявленные только в расширениях. Самая быстрая, позволяет инлайнинг.
  • Таблица виртуальных методов (vtable): методы классов, которые можно переопределить.
  • Witness table: требования протоколов при вызове через any P или дженерик.
  • Message dispatch: @objc dynamic, через Objective-C runtime (нужно для KVO).

Ловушка: метод, объявленный только в расширении протокола (не в самом протоколе), не требование, и через any P всегда вызывается версия расширения:

protocol Greeter { func hello() -> String }
extension Greeter {
    func hello() -> String { "протокол" }
    func bye() -> String { "пока из протокола" }
}
struct Robot: Greeter {
    func hello() -> String { "робот" }
    func bye() -> String { "пока от робота" }
}
let g: any Greeter = Robot()
g.hello()   // "робот"
g.bye()     // "пока из протокола"

Ещё один вариант той же ловушки: базовый класс принял протокол и пользуется реализацией по умолчанию, а подкласс определил свой метод. Через протокол вызовется версия по умолчанию: в witness table базового класса записана она.

associatedtype

Протокол может содержать тип-заглушку:

protocol Store {
    associatedtype Item
    func all() -> [Item]
}

Такие протоколы нельзя использовать как обычный тип без any или дженерика. С Swift 5.7 есть первичные associated types: protocol Store<Item> и запись some Store<User>.

Дженерики

func maxOf<T: Comparable>(_ a: T, _ b: T) -> T { a > b ? a : b }

func save<S: Sequence>(_ items: S) where S.Element: Codable { ... }

Компилятор специализирует дженерики под конкретные типы, если видит тело функции. Между модулями без @inlinable вызов идёт через неспециализированную версию, это медленнее.

Условное соответствие: extension Array: Summable where Element: Numeric делает массив Summable, только когда элементы числовые. Так устроено Array: Equatable.

some и any

  • some Shape (opaque type): конкретный тип, который не называют, но он всегда один и известен компилятору. Быстро, можно специализировать. SwiftUI построен на some View. С Swift 5.7 some в параметре это сахар для дженерика.
  • any Shape (existential): коробка, в которой может лежать любой тип. Значение до 3 машинных слов лежит прямо в коробке, больше уезжает в кучу; методы вызываются через witness table.

Правило: по умолчанию some или дженерик, any там, где нужна гетерогенность, например [any Shape].

Протоколы с Self в позиции параметра (как Equatable) существуют как any Equatable, но сравнить два таких значения через == нельзя: типы в коробках могут различаться.

Type erasure

До Swift 5.7 протоколы с associatedtype нельзя было хранить в свойствах, поэтому появились обёртки AnyPublisher, AnyHashable, AnyView. Часть задач теперь решает any P<T>, но в SwiftUI AnyView по-прежнему вредит диффингу.

Частые ошибки

  • Ожидать динамическую диспетчеризацию для метода из расширения.
  • Протокол делегата без AnyObject и попытка сделать ссылку weak.
  • any повсюду там, где хватает дженерика.
  • Протокол ради протокола: одна реализация и ни одного теста.

Что спросят на собесе

  • Что такое POP и чем протокол с расширением лучше базового класса?
  • Какие виды диспетчеризации есть в Swift?
  • Что выведет код с методом в расширении протокола?
  • Чем some отличается от any?
  • Что такое associatedtype, условное соответствие, type erasure?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (23)