История вопроса:
Абстракция протоколов появилась в Swift в противовес классическому наследованию объектов из ООП. Если в Objective-C и других ООП-языках доминировал подход наследования "от общего к частному", то Swift с самого старта продвигал протоколы как основной способ достижения абстракции, подчеркивая композицию поверх наследования.
Проблема:
Классическое наследование предполагает жесткую иерархию: дерево подклассов с обязательным расширением через override. Это ограничивает гибкость, приводит к "хрупкому" коду, сложному рефакторингу и раздутию базовых классов-дедов. Кроме того, Swift не поддерживает множественное наследование классов — а значит, повторное использование функциональности возможно только через другие механики.
Решение:
Абстракция протоколов позволяет объявлять "набор требований", которые тип должен реализовать. Протоколы можно расширять (extension) для внедрения общей логики, что приближает их к концепции "миксины":
Пример кода:
protocol Drawable { func draw() } extension Drawable { func draw() { print("Default drawing") } } struct Circle: Drawable {} let c = Circle() c.draw() // Выведет "Default drawing"
Ключевые особенности:
В чем разница между extension протокола и обычной реализацией в классе?
Расширение протокола через extension добавляет default-реализацию только для случаев, когда пользователь не реализовал этот метод в своем типе. Если метод реализован явно в типе, вызывается именно он.
Пример:
protocol Demo { func foo() } extension Demo { func foo() { print("default") } } struct X: Demo { func foo() { print("custom") } } X().foo() // "custom"
Что произойдет, если к типу, реализующему протокол и его extension, обратиться как к данным протокола?
Если протокол объявляет метод как обязательный (требование), используется реализация конкретного типа даже при приведении к типу протокола. Однако если в extension добавлено новое (не объявленное ранее в протоколе) свойство, оно будет доступно только через extension, не через тип протокола.
Можно ли хранить в массиве экземпляры с разными struct-ами, реализующими один протокол?
Да — благодаря "экзистенциальным" типам (например, [Drawable]) можно хранить heterogenous коллекции:
struct Tri: Drawable { func draw() { print("Triangle") } } let arr: [Drawable] = [Circle(), Tri()] arr.forEach { $0.draw() }
В компании был базовый суперкласс Shape, от которого наследовались все фигуры (Circle, Square, Polygon). Базовый класс разрастался, потому что каждую новую фигуру приходилось поддерживать через override. Расширять систему становилось все труднее — каждый новый тип ломал ABI и заставлял переписывать существующий код.
Плюсы:
Минусы:
Стали использовать несколько протоколов: Drawable, Colorable, Animatable. Теперь каждую фигуру легко делать одновременно "анимируемой и цветной", не изменяя остальные структуры. Новый функционал добавляется через extension.
Плюсы:
Минусы: