Протоколы не позволяют использовать примитивные бинарные операции (н/р, сравнение), важной фичей в своё время было добавление типа Self, позволяющего обращаться к объекту, реализующему данный протокол. Однако, такая гибкость порождает неочевидную проблему.
let vehicles: [some Vehicle] = [
Car(),
Car(),
Car(),
]
Так как Swift, используя экзистенциальные типы, не имеет представления о конкретном возвращаемом типе функции (а только о свойствах этого типа), компилятор выдает ошибку, известную как Protocol
AnyProtocol can only be used as a generic constraint because it has Self or associated type requirements
.
Решением проблемы выступило внедрение новой фичи — Opaque Types, позволяющей указать компилятору конкретный возвращаемый тип для каждой функции в коде. Аналогичные решения используют и в других языках. Реализация для Swift была перенесена из Rust, в котором разработчики языка придумали классный и лаконичный подход.
some используется вместе с протоколами чтобы создать an opaque type that represents something that is conformed to a specific protocol
.
Например, следующий код выдаст ошибку при компиляции:
protocol AnyProtocol: Equatable { }
struct MainClass: AnyProtocol { }
// код вызывает ошибку
// из-за экзистенциального контейнер
func generate() -> AnyProtocol {
return MainClass()
}
let main = generate()
В своей реализации протокол Equatable определяет свойство Self, позволяющее обращаться в объекту, реализующему этот протокол. Свойство используется для определения оператора ==.
При запуске этого примера кода, мы получим ошибку вида Protocol
AnyProtocol can only be used as a generic constraint because it has Self or associated type requirements
, которая говорит нам о том, что протоколы имеют некоторые ограничения и могут использоваться только в качестве Type Constraints (задавать правила для неабстрактных типов данных).
Дело в том, что компилятор Swift не знает какой конкретный тип может вернуться от этой функции в рантайме. Он знает только то, что тип реализует протокол AnyProtocol. В данном случае, функция могла бы вернуть значения Int: Equatable или Array: Equatable. Хотя их абстрактный тип данных схожий, конкретные типы невозможно сравнить оператором ==, о чем нам и сообщает компилятор.
Хотя под капотом все устроено немного сложнее. Проблему решили достаточно изящно — с помощью добавления синтаксической конструкции к возвращаемому значению функции:
protocol AnyProtocol: Equatable { }
struct MainClass: AnyProtocol { }
// код не вызывает ошибку
func generate() -> some AnyProtocol {
return MainClass()
}
let main = generate()
Но каким образом это решило проблему? Для пользователя такая конструкция выглядит, как и раньше, — “возвращением абстрактного типа, реализующего протокол AnyProtocol”.
На самом деле, в случае использования Opaque Result Types компилятор Swift под капотом заранее определяет все конкретные возвращаемые типы для каждой функции программы. То есть после компиляции функция generate() возвращает тип MainClass, а не AnyProtocol. Это позволяет устранить ошибку, приведенную выше, потому что компилятор теперь знает о конкретном возвращаемом типе данных функции. Неужели это действительно необходимо?
Проблему и правда можно решить с помощью обычных Generics:
// код не вызывает ошибку
func generate<T: AnyProtocol>() -> T {
return MainClass() as! T
}
// еще пример без force unwrapped
func collect<T: AnyProtocol>(value: T, and anotherValue: T) -> [T] {
return [value, anotherValue]
}
// применение функции
// auto infer to collect<MainClass.Type>
// returns [MainClass(), MainClass()]
collect(value: MainClass(), and: MainClass())
И это вполне рабочий вариант. Тогда зачем нужны Opaque Types, если можно использовать шаблоны?
Во время использования generic информация о типе данных поступает от пользователя (то есть мы руками задаём нужный тип данных в параметрах функции). А Opaque Result Types позволяют делегировать выбор возвращаемого типа самой функции (а точнее — компилятору).
5.5.1 Generics Theme | Back To iOSWiki Contents | 5.5.3 Existential Types Theme