Протоколы и расширения
Протокол — это контракт: набор требований, которые тип обязуется выполнить. Расширение — способ добавить возможности уже существующему типу.
Суть урока: вместо наследования от классов 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, не имея доступа к их исходникам.