SwiftMiddleCoding

Что такое generics в Swift и как использовать ограничения типов (type constraints)?

Generics в Swift — параметрический полиморфизм со статической специализацией. Ограничения типов задаются через T: Protocol и where-клаузы; some Protocol скрывает конкретный тип (opaque), any Protocol — existential с рантайм-диспетчеризацией.

Generics в Swift

Обобщения (generics) позволяют писать один алгоритм или тип, работающий с любым типом данных, при этом сохраняя статическую типобезопасность. Компилятор специализирует код под конкретные типы во время компиляции — в отличие от existentials, которые работают через таблицы методов в рантайме.

Базовый синтаксис

// Обобщённая функция
func swap<T>(_ a: inout T, _ b: inout T) {
    let tmp = a; a = b; b = tmp
}

// Обобщённая структура
struct Stack<Element> {
    private var storage: [Element] = []
    mutating func push(_ item: Element) { storage.append(item) }
    mutating func pop() -> Element? { storage.popLast() }
    var top: Element? { storage.last }
}

var stack = Stack<Int>()
stack.push(1)
stack.push(2)
print(stack.pop()!) // 2

Type constraints — ограничения типов

Ограничения задаются через двоеточие: T: Protocol или T: Class. Можно накладывать несколько через запятую или через where.

// Одно ограничение: T должен соответствовать Comparable
func max<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// Несколько ограничений через where
func printSorted<T>(_ items: [T]) where T: Comparable & CustomStringConvertible {
    items.sorted().forEach { print($0.description) }
}

// Ограничение на ассоциированный тип
func sum<C: Collection>(_ c: C) -> C.Element
    where C.Element: AdditiveArithmetic {
    c.reduce(.zero, +)
}

print(sum([1, 2, 3]))       // 6
print(sum([1.5, 2.5]))      // 4.0

where-clause в extension

Расширения можно добавлять только для определённых специализаций через where.

extension Stack where Element: Equatable {
    func contains(_ item: Element) -> Bool {
        storage.contains(item)
    }
}
// Stack<String> получает метод contains, Stack<URLRequest> — нет

Opaque types (some) vs generics

some Protocol — обратные обобщения (reverse generics). Тип знает реализацию, но скрывает её от вызывающего кода. Использутся в SwiftUI (var body: some View) и для стабильных API.

// some — компилятор знает конкретный тип, вызывающий — нет
func makeShape() -> some Shape {
    Circle(radius: 10)
}

// any — existential, тип неизвестен на этапе компиляции
func drawAll(_ shapes: [any Shape]) {
    shapes.forEach { $0.draw() }
}

Primary associated types (Swift 5.7)

Протоколы могут указывать primary associated type, что позволяет использовать синтаксис some Collection.

protocol Repository<Entity> {
    associatedtype Entity
    func findById(_ id: UUID) async throws -> Entity
}

struct UserRepository: Repository<User> {
    func findById(_ id: UUID) async throws -> User { ... }
}

func loadUser(from repo: some Repository<User>) async throws -> User {
    try await repo.findById(UUID())
}

Parameter packs (Swift 5.9)

Для функций с произвольным числом разнотипных аргументов используются parameter packs:

func zip<each T, each U>(
    _ firsts: repeat each T,
    _ seconds: repeat each U
) -> (repeat (each T, each U)) {
    return (repeat (each firsts, each seconds))
}

Подводные камни

  • Specialization и размер бинарника: компилятор генерирует отдельный код для каждой специализации generic-функции; чрезмерное использование в библиотеках раздувает бинарник.
  • Protocol with associated type (PAT): до Swift 5.7 PAT нельзя было использовать как тип напрямую — только через some/any; старый код с AnyProtocol-обёртками теперь устарел, но ещё встречается.
  • where-клаузы не транзитивны: ограничение where T: Sendable в методе не означает, что контейнер Stack автоматически Sendable.
  • Рекурсивные обобщения: Swift имеет ограниченную поддержку рекурсивных generic-типов; сложные деревья типов могут не компилироваться или компилироваться экспоненциально долго.
  • Existential и Comparable: any Comparable не поддерживает оператор > напрямую — требуется обобщённая функция или some Comparable.
  • Conditional conformance и override: нельзя переопределить условное соответствие протоколу в подклассе — это ограничение системы типов Swift.
  • some в stored property: до Swift 5.7 some Protocol нельзя было использовать как тип хранимого свойства; теперь можно, но тип фиксируется при инициализации.

Common mistakes

  • Сводить «generics в Swift и как использовать ограничения типов (type constraints)» к синтаксису и не объяснять система типов.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-10.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «generics в Swift и как использовать ограничения типов (type constraints)» и подтверждает ее корректным примером.
  • Умеет связать ответ с ARC, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swift-10, включая производительность, память и сопровождение.

Sources

Related topics