Ответ
Да, с помощью ассоциированных типов (associatedtype). Они определяют требования к типу, который будет указан при реализации протокола.
Базовый пример:
protocol Container {
associatedtype Element
var items: [Element] { get set }
mutating func add(_ item: Element)
}
struct IntContainer: Container {
// Явное указание типа (можно опустить, Swift выведет)
typealias Element = Int
var items: [Int] = []
mutating func add(_ item: Int) {
items.append(item)
}
}
// Использование с автоматическим выводом типа
struct StringContainer: Container {
var items: [String] = []
mutating func add(_ item: String) { items.append(item) }
}
Протокол с ограничениями на тип:
protocol ComparableContainer {
associatedtype Item: Comparable // Ограничение
func contains(_ item: Item) -> Bool
}
Ключевые отличия от дженерик-классов:
- Протоколы используют
associatedtype, классы/структуры —<T>в объявлении. - Ассоциированные типы более гибкие: конкретный тип определяется в реализации, а не при использовании протокола.
- Это позволяет создавать коллекции разных типов, реализующих один протокол (например,
[Container]), где каждый элемент может иметь свойElement.
Ответ 18+ 🔞
Ну, вот смотри, как это работает, блядь. Ты же знаешь, что протокол — это как контракт, да? Так вот, иногда в этом контракте нужно оставить дырку для типа, который заполнит тот, кто его подписывает. Это и есть ассоциированный тип, ёпта. Не <T> в угловых скобках, а associatedtype внутри протокола.
Простой пример, чтобы не обосраться:
protocol Container {
associatedtype Element // Вот эта дырка, блядь. Тип — пока хз.
var items: [Element] { get set }
mutating func add(_ item: Element)
}
А теперь какой-нибудь умник приходит и говорит: "А, понял, щас сделаю!" И реализует это всё.
struct IntContainer: Container {
// Можно явно написать, но Swift и так поймёт, хитрая жопа
typealias Element = Int
var items: [Int] = []
mutating func add(_ item: Int) {
items.append(item)
}
}
// Или вот так, вообще без typealias, чтоб мозги не ебать
struct StringContainer: Container {
var items: [String] = [] // Swift видит String и думает: "О, значит Element — это String!"
mutating func add(_ item: String) { items.append(item) }
}
А бывает, что нужно, чтобы этот ассоциированный тип был не абы каким, а с ограничениями. Например, сравниваемым. Тогда пишем так:
protocol ComparableContainer {
associatedtype Item: Comparable // Ограничение, блядь! Только Comparable типы.
func contains(_ item: Item) -> Bool
}
Теперь, если кто-то захочет Item сделать, например, [UIView] — получит пиздюлей от компилятора, ибо UIView не Comparable. Логично, ёпта.
И главное, чем это отличается от обычных дженериков в классе?
- В протоколе —
associatedtype, в классе/структуре —<T>прямо в названии. - Ассоциированные типы — гибче, пиздец как. Конкретный тип определяется в самой реализации, а не когда ты этот протокол используешь. Это позволяет, например, запихнуть в массив кучу разных
Container, и у каждого внутри может быть свойElement—Int,String, да хотьUIViewController. А с дженерик-классом так не выйдет, там тип фиксируется при создании экземпляра.
Вот и вся магия, блядь. Не так страшен чёрт, как его малюют. Главное — не путать, где дыру оставлять, а где её уже заполнять.