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.7someв параметре это сахар для дженерика.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?