Как создать дженерик-протокол в Swift?

Ответ

Да, с помощью ассоциированных типов (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, и у каждого внутри может быть свой ElementInt, String, да хоть UIViewController. А с дженерик-классом так не выйдет, там тип фиксируется при создании экземпляра.

Вот и вся магия, блядь. Не так страшен чёрт, как его малюют. Главное — не путать, где дыру оставлять, а где её уже заполнять.