Зачем это нужно
Представьте, что вы строите поведение через классы: базовый класс Animal, от него FlyingAnimal, от него SwimmingFlyingAnimal... И тут появляется утка, которая плавает и летает, но не вписывается ни в одну ветку дерева, потому что у Swift нет множественного наследования классов. С протоколами эта проблема не возникает: вместо одного жёсткого дерева вы собираете поведение из независимых кусочков, как из конструктора. Нужна утке способность летать — добавьте протокол Flyable. Нужна способность плавать — добавьте Swimmable. Никакой общей иерархии для этого придумывать не надо.
Более того, в предыдущем уроке вы уже видели, что протокол может не только требовать метод, но и сам предлагать его реализацию через extension. Именно на этом и держится подход, который в документации Apple называется protocol-oriented programming (POP, программирование через протоколы) — способ строить поведение композицией протоколов и их реализациями по умолчанию вместо иерархии классов.
Реализация по умолчанию как основа
Когда несколько типов, соответствующих протоколу, реализуют требование одинаково, вы описываете это один раз в extension протокола — и все конформные типы получают метод бесплатно.
protocol Greeter {
var name: String { get }
func greet() -> String
}
extension Greeter {
func greet() -> String {
return "Привет, я \(name)."
}
}
struct Employee: Greeter {
let name: String
}
let e = Employee(name: "Аня")
print(e.greet()) // Привет, я Аня.
Employee соответствует протоколу Greeter, но нигде не пишет тело greet() — оно взято из extension. Если завтра появится ещё десять типов с таким же name, ни один из них не должен повторять этот код.
Собственная реализация вместо стандартной
Реализация по умолчанию — это именно значение по умолчанию, а не единственный вариант. Тип может объявить требование протокола сам, и тогда используется именно эта версия, а не та, что лежит в extension.
struct Manager: Greeter {
let name: String
func greet() -> String {
return "Здравствуйте, я \(name), и я тут главный."
}
}
let m = Manager(name: "Олег")
print(m.greet()) // Здравствуйте, я Олег, и я тут главный.
Manager — тоже Greeter, но у него своя формулировка. Компилятор выбирает реализацию из самого типа, а к общей заготовке из extension обращается только если тип ничего своего не предложил.
Композиция протоколов: A & B
Часто нужен не «какой-то один протокол», а сразу несколько сразу. Для этого в объявлении типа параметра или переменной протоколы перечисляют через & — это и называют композицией протоколов.
protocol Worker {
var task: String { get }
func work() -> String
}
extension Worker {
func work() -> String {
return "Сейчас я делаю: \(task)."
}
}
struct Intern: Greeter, Worker {
let name: String
let task: String
}
func describe(_ person: Greeter & Worker) -> String {
return "\(person.greet()) \(person.work())"
}
Параметр person: Greeter & Worker принимает любой тип, который соответствует ОБОИМ протоколам сразу — неважно, структура это, класс или перечисление. Не пришлось заводить общий базовый класс и не пришлось придумывать третий протокол-надстройку: два независимых требования просто складываются вместе там, где они нужны вместе.
Ограничение where Self в расширении протокола
Иногда реализация по умолчанию имеет смысл не для всех типов протокола, а только для тех, что дополнительно соответствуют ещё одному протоколу. Для этого у расширения указывают условие where Self: ДругойПротокол.
protocol Reportable {
func report() -> String
}
extension Reportable where Self: Worker {
func report() -> String {
return "Отчёт: \(work())"
}
}
Такая реализация report() появится только у типов, которые соответствуют одновременно Reportable и Worker: только для них внутри extension доступен метод work(). Тип, который подписался под Reportable, но не под Worker, эту заготовку не получит и обязан написать report() сам.
Частые ошибки
- Путать протокол с классом: у протокола нет своего хранимого состояния, только требования к тому, что должно быть у конформного типа. Хранимые свойства по-прежнему объявляются в
struct,classилиenum. - Думать, что если метод есть в
extension, то он есть у любого протокола — реализация по умолчанию появляется только у типов, которые формально объявили соответствие этому протоколу (через: ИмяПротокола). - Путать порядок выбора: сначала компилятор смотрит, объявил ли сам тип требование протокола; если да — используется реализация типа, если нет — реализация из
extension. - Плодить лишние протоколы там, где хватило бы одного с двумя требованиями. Композиция
A & Bнужна, когда поведения действительно независимы и переиспользуются раздельно, а не когда их всегда используют вместе.
Резюме
- Protocol-oriented programming — сборка поведения из протоколов и их реализаций по умолчанию вместо жёсткой иерархии классов.
- Реализация в
extensionпротокола становится реализацией по умолчанию для всех конформных типов. - Тип может переопределить требование протокола собственной реализацией — она выигрывает у заготовки из
extension. - Композиция
A & Bзадаёт тип, который соответствует сразу нескольким протоколам, без общего базового класса. - Условие
where Self: ДругойПротоколограничивает реализацию по умолчанию только теми типами, что соответствуют дополнительному протоколу.
Проверьте себя
4 вопроса
Сводка о сотруднике из двух протоколов
Даны два протокола с реализациями по умолчанию в extension: Greeter (умеет здороваться через greet()) и Worker (умеет рассказывать о своей задаче через work()). Часть типов используют реализацию по умолчанию как есть, часть — переопределяют один из методов собственной версией.
Реализуйте функцию func summarize(_ person: Greeter & Worker) -> String, которая принимает любое значение, соответствующее сразу обоим протоколам, и возвращает строку вида "РЕЗУЛЬТАТ_GREET РЕЗУЛЬТАТ_WORK" — то есть результат вызова greet(), затем один пробел, затем результат вызова work(). Функция не должна знать заранее, какая именно реализация (по умолчанию или собственная) сработает у конкретного типа — просто вызовите оба метода через параметр.