В Swift есть интересная фича, о которой мало кто знает, но благодаря дружественному интерфейсу, мы постоянно её используем, даже не замечая этого — экзистенциальные типы (existential types).
Экзистенциальные типы можно понимать как некий абстрактный объект, о котором мы ничего не знаем, но примерно понимаем какие свойства он содержит.
let vehicles: [any Vehicle] = [
Car(),
Car(),
Bus(),
]
Идея экзистенциальных типов в Swift пришла из Haskell, в документации которого они описаны как следующий предикат:
∀x Q(x)⇒ P = ∃x Q(x) ⇒ P
// где предикат P определяется однозначно, если все запрашиваемые им требования к объекту Q удовлетворены.
Это означает, что определение объекта производится по значению (свойствам), а не его типу. Пока не задумывайтесь о том, как это выглядит в коде. Важнее понять главное отличие экзистенциальных типов от шаблонов (generic):
- Generics определяет объект по четким ограничениям типов (по типу объекта);
- Existential определяет объект по его свойствам (в протоколе);
Экзистенциальные типы vs Generics
Из определения видно, что экзистенциальные типы похожи на generic, а именно тем, что заранее конкретный тип объекта не определен. С точки зрения Swift, экзистенциальные типы позволяют строить абстракции над значениями (value-level abstraction) — в этом и кроется отличие от шаблонов, использующих абстракции над типами (type-level abstraction).
На практике мы довольно часто используем экзистенциальные типы. Например, каждый раз, когда ограничиваем параметры ввода и возвращаемое значение функции через протокол:
// Generics-like
func collect<T: AnyProtocol>(value: T, and anotherValue: T) -> [T] {
return [value, anotherValue]
}
// Existential Types
func collect(value: AnyProtocol, and anotherValue: AnyProtocol) -> [AnyProtocol] {
return [value, anotherValue]
}
Для нас, как для разработчиков, различие на первый взгляд незаметно, мы просто используем “протокол как тип данных”. Однако, с точки зрения Swift это не тоже самое, что generic.
Дело в том, что generic являются статически-генерируемой структурой. Хотя в исходном коде они позволяют избегать дублирования, компилятор фактически создает явную перегружаемую функцию на каждый используемый им тип:
// generics in source code
func compare<T: Comparable>(value: T, and anotherValue: T) -> Bool {
return value == anotherValue
}
compare<Int>(value: 5, and: 10)
compare<Double>(value: 10.0, and: 5.0)
// generics after compile (by demand)
func compare(value: Int, and anotherValue: Int) -> Bool {
return value == anotherValue
}
func compare(value: Double, and anotherValue: Double) -> Bool {
return value == anotherValue
}
Такой код создается во время компиляции приложения, в то время как экзистенциальные типы являются динамечиски-генерируемыми, то есть их определение происходит во время исполнения кода.
protocol AnyProtocol {
associatedtype T
}
Они также являются неочевидной конструкцией языка, но имеют высокую важность для будущих фичей Swift. Например, благодаря экзистенциальным типам, параметры могут быть переданы в функцию, основываясь на перечислении списка протоколов (вместо создания класса, реализующего их):
// Existential Complex Types
func isEquals(with value: Collection & Equatable) -> Bool {
return self === value
}
При такой записи компилятор Swift не сможет определить какой конкретно тип поступает на вход функции. Он знает только то, что этот тип соответствует двум протоколам Collection и Equatable. Аналогичные возможности мы получаем при описании возвращаемого значения.
Экзистенциальный тип, созданный с помощью ключевого слова any,не может использовать == оператор для сравнения двух экземпляров экзистенциального типа.
Для компилятора экзистенциальный тип — это просто «коробка», он понятия не имеет, что внутри коробки. Следовательно, компилятор не может провести сравнение, если он не может гарантировать, что содержимое «коробки» имеет один и тот же базовый конкретный тип.
Экзистенциалы относительно дороги в использовании, потому что компилятор и среда выполнения не могут заранее определить, сколько памяти должно быть выделено для конкретного объекта, который будет заполнять экзистенциал. Всякий раз, когда вы вызываете метод для экзистенциала, такого как networking свойство в фрагменте кода, который вы видели ранее, среда выполнения должна будет dynamically dispatch этот вызов конкретному объекту, что медленнее, чем static dispatch, которая идет непосредственно к конкретному типу
5.5.2 Opaque Theme | Back To iOSWiki Contents | 5.5.4 Any vs Some Theme