Протоколы и расширения

Протокол — это контракт: набор требований, которые тип обязуется выполнить. Расширение — способ добавить возможности уже существующему типу.
Суть урока: вместо наследования от классов Swift поощряет протоколы. Тип может соответствовать многим протоколам, а расширения позволяют дополнять даже стандартные типы вроде Int и String.

Протокол описывает, ЧТО тип должен уметь, не говоря КАК. Это контракт, который реализуют структуры, классы и перечисления:

protocol Describable {
    var summary: String { get }
    func describe() -> String
}

struct Book: Describable {
    let title: String
    var summary: String { "Книга: \(title)" }
    func describe() -> String { summary }
}

Любой тип, соответствующий Describable, гарантированно имеет summary и describe(). Это позволяет писать код, работающий с любым подходящим типом, не зная его конкретики. Многие возможности SwiftUI — это соответствие протоколам: View, Identifiable, Codable.

Расширение добавляет функциональность существующему типу — даже тому, исходники которого вам недоступны:

extension Int {
    var isEven: Bool { self % 2 == 0 }
    func times(_ action: () -> Void) {
        for _ in 0..<self { action() }
    }
}

print(4.isEven)      // true
3.times { print("тук") }

Расширения часто используют, чтобы дать протоколу реализацию по умолчанию. Тогда соответствующим типам не нужно писать общий код вручную — это и есть протокольно-ориентированное программирование:

extension Describable {
    func describe() -> String { summary }   // по умолчанию
}
// Теперь типам достаточно задать только summary
            protocol View
           /     |      \
     struct    struct    struct
     Button    Text      MyCard
   (каждый соответствует контракту View по-своему)

   extension View { ... }  ->  общие возможности всем сразу

Попробуй сам ▶ — запусти код прямо в браузере (Pyodide). Здесь нет Swift, но логика та же, что под капотом мобильного кода:

# Имитируем протокол: "утиная типизация" — лишь бы был нужный метод.
class Book:
    def __init__(self, title):
        self.title = title
    def describe(self):                 # выполняем контракт
        return f'Книга: {self.title}'

class Movie:
    def __init__(self, name):
        self.name = name
    def describe(self):
        return f'Фильм: {self.name}'

# Функция работает с любым, кто умеет describe() — как протокол
for item in [Book('Swift'), Movie('Tron')]:
    print(item.describe())

Как работает под капотом

Когда тип объявляет соответствие протоколу, компилятор проверяет, что все требования выполнены, иначе — ошибка. При вызове метода через протокольный тип Swift использует таблицу свидетелей (witness table), чтобы найти нужную реализацию. Реализации по умолчанию в extension позволяют делиться кодом без наследования, а ограничения where делают расширения условными — например, добавить метод только для коллекций, где элементы сравнимы.

Частые ошибки

  • Тянуть наследование классов отовсюду. В Swift часто чище композиция через протоколы.
  • Хранить состояние в расширении. Расширения не могут добавлять хранимые свойства, только вычисляемые.
  • Забыть реализовать требование протокола. Компилятор не скомпилирует несоответствующий тип.

Best practices

  • Описывайте поведение протоколами, а не базовыми классами.
  • Используйте extension для группировки методов и реализаций по умолчанию.
  • Соответствуйте стандартным протоколам (Identifiable, Codable, Equatable) — это открывает множество готовых возможностей.

Итоги. Протоколы задают контракты, а расширения дополняют типы и дают реализации по умолчанию. Этот дуэт — основа гибкой архитектуры Swift и причина, по которой SwiftUI так элегантно собирается из небольших соответствующих типов.

Шире контекста

Протокольно-ориентированное программирование — это фирменный стиль Swift, который Apple даже выносила в отдельную сессию на конференции WWDC. Идея проста и мощна: вместо жёсткой иерархии наследования классов вы собираете поведение из небольших протоколов-контрактов, а общий код выносите в расширения с реализациями по умолчанию. Тип может соответствовать множеству протоколов одновременно, оставаясь при этом лёгкой структурой. Весь SwiftUI построен на этом: View, Identifiable, Hashable, Codable — это протоколы, и стоит вашему типу соответствовать им, как открывается лавина бесплатных возможностей. Например, соответствие Codable даёт автоматическую сериализацию в JSON и обратно, а Identifiable позволяет использовать тип в списках. Расширения же помогают держать код опрятным: можно группировать методы по смыслу и даже добавлять удобства стандартным типам вроде String и Int, не имея доступа к их исходникам.

Проверьте себя
1. Что описывает протокол?
AКонкретную реализацию метода
BКонтракт: набор требований, которые тип обязан выполнить
CСпособ хранения данных в памяти
DЗначение по умолчанию переменной
2. Какое ограничение есть у расширений (extension)?
AНельзя добавлять методы
BНельзя добавлять хранимые свойства, только вычисляемые
CРаботают только с классами
DНельзя реализовывать протоколы