Для чего нужны протоколы в swift
Протокольно-ориентированное программирование в Swift 5.1
Протоколы — фундаментальное свойство Swift. Они играют важную роль в стандартных библиотеках Swift и являются обычным способом абстракции кода. Во многом они похожи на интерфейсы в других языках программирования.
В этом руководстве мы представим вам подход к разработке приложений, называемый «протокольно-ориентированным программированием», который стал практически основным в Swift. Это действительно то, что вам необходимо уяснить при изучении Swift!
В этом руководстве вы:
Начинаем
Представьте, что вы разрабатываете игру — гонки. Ваши игроки могут гонять на машинах, мотоциклах и на самолётах. И даже летать на птицах, это же ведь игра, верно? Основное здесь то, что есть дофига всяких «штук», на которых можно гонять, летать и т.п.
Обычный подход при разработке подобных приложений состоит в объектно-ориентированном программировании. В этом случае мы заключаем всю логику в неких базовых классах, от которых в дальнейшем наследуемся. Базовые классы, таким образом, должны содержать внутри логику «вести» и «пилотировать».
Мы начинаем разработку с создания классов для каждого средства передвижения. «Птиц» отложим пока на потом, к ним мы вернёмся чуть позже.
Мы видим, что Car и Motorcycle весьма похожи по функционалу, так что мы создаём базовый класс MotorVehicle. Car и Motorcycle будут наследоваться из MotorVehicle. Таким же образом мы создаем базовый класс Aircraft, от которого создадим класс Plane.
Вы думаете, что всё прекрасно, но — бац! — действие вашей игры происходит в XXX столетии, и некоторые машины уже могут летать.
Итак, у нас случился затык. У Swift нет множественного наследования. Каким образом ваши летающие автомобили смогут наследоваться и от MotorVehicle и от Aircraft? Создавать еще один базовый класс, в котором соединятся обе функциональности? Скорее всего, нет, так как нет ясного и простого способа добиться этого.
И что же спасёт нашу игру в этой ужасной ситуации? На помощь спешит протокольно-ориентированное программирование!
Что такого в протокольно-ориентированном программировании?
Протоколы позволяют группировать похожие методы, функции и свойства применительно к классам, структурам и перечислениям. При этом только классы позволяют использовать наследование от базового класса.
Преимущество протоколов в Swift состоит в том, что объект может соответствовать нескольким протоколам.
Ваш код при при использовании такого метода становиться более модульным. Думайте о протоколах как о строительных блоках функционала. Когда вы добавляете новую функциональность объекту, делая его соответствующим некоему протоколу, вы не делаете совершенно новый объект «с нуля», это было бы слишком долго. Вместо этого, вы добавляете разные строительные блоки до тех пор, пока объект не будет готов.
Переход от базового класса к протоколам решит нашу проблему. С протоколами мы можем создать класс FlyingCar, который соответствует и MotorVehicle и Aircraft. Миленько, да?
Займёмся кодом
Запускаем Xcode, создаём playground, сохраняем как SwiftProtocols.playground, добавляем этот код:
Скомпилируем при помощи Command-Shift-Return, чтобы быть уверенным, что все в порядке.
Здесь мы определяем простой протокол Bird, со свойствами name и canFly. Затем определяем протокол Flyable со свойством airspeedVelocity.
В «допротокольную эпоху» разработчик начал бы с класса Flyable в качестве базового, а затем, используя наследование, определил бы Bird и всё прочее, что может летать.
Но в протокольно-ориентированном программировании всё начинается с протокола. Эта техника позволяет нам инкапсулировать набросок функционала без базового класса.
Как вы сейчас увидите, это делает процесс проектирования типов гораздо гибче.
Определяем тип, соответствующий протоколу
Добавьте этот код внизу playground:
Этот код определяет новую структуру FlappyBird, которая соответствует и протоколу Bird и протоколу Flyable. Её свойство airspeedVelocity — произведение flappyFrequency and flappyAmplitude. Свойство canFly возвращает true.
Теперь добавьте определения еще двух структур:
Penguin это птица, но не может летать. Хорошо, что мы не пользуемся наследованием и не сделали всех птиц Flyable!
При использовании протоколов вы определяете компоненты функционала и делаете все подходящие объекты соответствующими протоколу
Затем мы определяем SwiftBird, но в нашей игре есть несколько разных её версий. Чем больше номер версии, тем больше её airspeedVelocity, которая определена как вычисляемое свойство.
Однако, здесь есть некоторая избыточность. Каждый тип Bird должен определить явно определить свойство canFly, хотя у нас есть определение протокола Flyable. Похоже, что нам нужен способ определить реализацию методов протокола по умолчанию. Что ж, для этого существуют расширения протоколов (protocol extensions).
Расширяем протокол поведением по умолчанию
Расширения протокола позволяют задать поведение протокола по умолчанию. Напишите этот код сразу за определением протокола Bird:
Этот код определяет расширение протокола Bird. В этом расширении определяется, что свойство canFly вернёт true в случае, когда тип соответствует протоколу Flyable. Другими словам, всякой Flyable-птице больше не нужно явно задавать canFly.
Займёмся перечислениями
Перечисления в Swift могут соответствовать протоколам. Добавьте следующее определение перечисления:
Определяя соответствующие свойства, UnladenSwallow соответствует двум протоколам — Bird и Flyable. Такими образом, реализуется определение по умолчанию для canFly.
Переопределяем поведение по умолчанию
Наш тип UnladenSwallow, соответствуя протоколу Bird, автоматически получил реализацию для canFly. Нам, однако, нужно, чтобы UnladenSwallow.unknown возвращала false для canFly.
Добавьте внизу следующий код:
Скомпилируйте playground и проверьте полученные значения с указанными в комментариях выше.
Таким образом мы переопределяем свойства и методы почти так же, как используя виртуальные методы в объектно-ориентированном программировании.
Расширяем протокол
Вы также можете сделать свой собственный протокол соответствующим другому протоколу из стандартной библиотеки Swift и определить поведение по умолчанию. Замените объявление протокола Bird следующим кодом:
Соответствие протоколу CustomStringConvertible означает, что у вашего типа должно быть свойство description. Вместо того, чтобы добавлять это свойство в типе Bird и во всех производных от него, мы определяем расширение протокола CustomStringConvertible, которое будет ассоциировано только с типом Bird.
Наберите UnladenSwallow.african внизу playground. Скомпилируйте и вы увидите “I can fly”.
Протоколы в стандартных библиотеках Swift
Как видите, протоколы — эффективный способ расширять и настраивать типы. В стандартной библиотеке Swift это их свойство также широко применяется.
Добавьте этот код в playground:
Вы наверняка знаете, что выведет этот код, но, возможно, удивитесь использованным здесь типам.
Например, slice — не Array, а ArraySlice. Это специальная «обёртка», которая обеспечивает эффективный способ работы с частями массива. Соответственно, reversedSlice — это ReversedCollection.
К счастью, функция map определена как расширение к протоколу Sequence, которому соответствуют все типы-коллекции. Это позволяет нам применять функцию map как к Array, так и к ReversedCollection и не замечать разницы. Скоро вы воспользуетесь этим полезным приёмом.
На старт
Пока что мы определили несколько типов, соответствующих протоколу Bird. Сейчас же мы добавим нечто совсем другое:
У этого типа нет ничего общего с птицами и полётами. Мы хотим устроить гонку мотоциклистов с пингвинами. Пора выводить эту странную компашку на старт.
Соединяем всё вместе
Чтобы как-то объединить столь разных гонщиков, нам нужен общий протокол для гонок. Мы сможем все это сделать, даже не трогая все созданные нами до этого типы, при помощи замечательной вещи, которая называется ретроактивное моделирование. Просто добавьте это в playground:
Вот что мы тут делаем: сначала определяем протокол Racer. Это всё то, что может участвовать в гонках. Затем мы приводим все наши созданные до этого типы к протоколу Racer. И, наконец, мы создаём Array, который содержит в себе экземпляры каждого нашего типа.
Скомпилируйте playground, чтобы все было в порядке.
Максимальная скорость
Напишем функцию для определения максимальной скорости гонщиков. Добавьте этот код в конце playground:
Здесь мы используем функцию max чтобы найти гонщика с максимальной скоростью и возвращаем её. Если массив пуст, то возвращается 0.0.
Делаем функцию более обобщенной
Предположим, что массив Racers достаточно велик, а нам нужно найти максимальную скорость не во всем массиве, а в какой-то его части. Решение состоит в том, чтобы изменить topSpeed(of:) таким образом, чтобы она принимала в качестве аргумента не конкретно массив, а всё, что соответствует протоколу Sequence.
Заменим нашу реализацию topSpeed(of:) следующим образом:
Теперь наша функция работает с любым типом, отвечающим протоколу Sequence, в том числе и с ArraySlice.
Делаем функцию более «свифтовой»
По секрету: можно сделать ещё лучше. Добавим это в самом низу:
А вот теперь мы расширили сам протокол Sequence функцией topSpeed(). Она применима только в случае, когда Sequence содержит тип Racer.
Компараторы протоколов
Другая особенность протоколов Swift — это то, как вы определяете операторы равенства объектов или их сравнения. Напишем следующее:
Имея протокол Score можно писать код, который обращается со всеми элементами этого типа одним образом. Но если завести вполне определенный тип, такой как RacingScore, то вы не спутаете его с другими производными от протокола Score.
Мы хотим, чтобы очки (scores) можно было сравнивать, чтобы понять, у кого максимальный результат. До Swift 3 разработчикам было нужно писать глобальные функции для определения оператора к протоколу. Теперь же мы можем определить эти статические методы в самой модели. Сделаем это, заменив определения Score и RacingScore следующим образом:
Мы заключили всю логику для RacingScore в одном месте. Протокол Comparable требует опеределить реализацию только для функции «меньше, чем». Все остальные функции сравнения будут реализованы автоматически, на основании созданной нами реализации функции «меньше, чем».
Вносим изменения в объект
До сих пор каждый пример демонстрировал, как добавить функционал. Но что, если мы хотим сделать протокол, который что-то изменяет в объекте? Это можно сделать при помощи mutating методов в нашем протоколе.
Добавьте новый протокол:
Здесь мы определяем протокол, который дает нам возможность жульничать (cheat). Каким образом? Произвольно изменяя содержимое boost.
Теперь создадим расширение SwiftBird, которое соответствует протоколу Cheat:
Здесь мы реализуем функцию boost(_:), увеличивая speedFactor на передаваемую величину. Ключевое слово mutating даёт структуре понять, что одно из её значений будет изменено этой функцией.
Заключение
Здесь вы можете загрузить полный исходный код playground.
Вы узнали о возможностях протокольно-ориентированного программирования, создавая простые протоколы и увеличивая их возможности их при помощи расширений. Используя реализацию по умолчанию, вы даете протоколам соответсвующее «поведение». Почти как с базовыми классами, но только лучше, так как все это применимо также к структурам и перечислениям.
Вы также увидели, что расширение протоколов применимо и к базовым протоколам Swift.
Вы можете также посмотреть прекрасную лекцию на WWDC, посвященную протокольно-ориентированному программированию.
Как и с любой парадигмой программирования, есть опасность увлечься и начать использовать протоколы налево и направо. Здесь интересная заметка о том, что стоит опасаться решений в стиле «серебряная пуля».
Мобильная разработка. Swift: таинство протоколов
Сегодня мы продолжаем цикл публикаций на тему мобильной разработки под iOS. И если в прошлый раз речь шла о том, что нужно и не нужно спрашивать на собеседованиях, в этом материале мы коснемся тематики протоколов, которая имеет в Swift важное значение. Речь пойдет о том, как устроены протоколы, чем они отличаются друг от друга, и как сочетаются с интерфейсами Objective-C.
Как мы уже говорили ранее, новый язык Apple продолжает активно развиваться, и большинство его параметров и особенностей явно указаны в документации. Но кто читает документацию, когда код нужно написать здесь и сейчас? Поэтому давайте пройдемся по основным особенностям протоколов Swift прямо в нашем посте.
Для начала стоит оговориться, что протоколы Apple – это альтернативный термин для понятия «Интерфейс», которое применяется в других языках программирования. В Swift протоколы служат для того, чтобы обозначить шаблоны определенных структур (т.н. blueprint), с которыми можно будет работать на абстрактном уровне. Говоря простыми словами, протокол определяет ряд методов и переменных, которые в обязательном порядке должен наследовать определенный тип.
Далее в статье будут постепенно раскрываться моменты следующим образом: от простых и часто используемых к более сложным. В принципе, на собеседованиях можно давать вопросы в таком порядке, так как они определяют уровень компетенции соискателя — от уровня джунов до уровня сеньоров.
Для чего нужны протоколы в Swift?
Мобильные разработчики часто обходятся вообще без использования протоколов, но при этом теряется возможность работать с некоторыми сущностями абстрактно. Если выделить основные особенности протоколов в Swift, у нас получится следующие 7 пунктов:
При этом типы коллекций (сollection), а именно – array, set, dictionary – также можно упаковывать в протокол, потому что они тоже являются структурами. Например, словарь (Dictionary) определяется следующим образом
Обычно в объектно-ориентированном программировании применяется понятие классов, и всем хорошо знаком механизм наследования методов и переменных родительского класса классом-потомком. При этом никто не запрещает ему содержать дополнительные методы и переменные.
В случае с протоколами можно создать куда более интересную иерархию взаимосвязей. Для описания очередного класса можно использовать несколько протоколов одновременно, что позволяет создавать достаточно сложные конструкции, которые будут удовлетворять множеству условий одновременно. С другой стороны, ограничения разных протоколов позволяют сформировать некоторый объект, предназначенный только для узкого применения и содержащий определенный ряд функций.
Реализация протокола в Swift достаточно проста. Синтаксис подразумевает название, ряд методов и параметры (переменные), которые он будет содержать.
Кроме этого в Swift протоколы могут содержать не только наименования методов, но и их реализацию. Код метода в протоколе добавляются через расширения (extension). В документации можно найти много упоминаний расширений, но применительно к протоколам в расширениях можно размещать имя функции и тело функции.
То же самое можно делать с переменными.
Если мы где-то будем применять объект, связанный с протоколом, можно задать в нем переменную с фиксированным или передаваемым значением. Фактически переменная представляет собой небольшую функцию без входных параметров…или с возможностью прямого назначения параметра.
Расширение протокола в Swift позволяет реализовать тело переменной, и тогда она по факту будет представлять собой computed value – вычисляемый параметр с функциями get и set. То есть такая переменная не будет хранить никаких значений, а будет или играть роль функции или функций, или будет играть роль прокси для какой-то другой переменной.
Или если мы берем какой-то класс или структуру и реализуем протокол, то в нем можно использовать обычную переменную:
Стоит отметить, что переменные в определении протокола не могут быть weak. (weak – это вариант реализации переменой).
Есть и более интересные примеры: можно реализовать расширение массива и добавить туда функцию, связанную с типом данных массива. Например, если массив содержит целочисленные значения или имеет формат equitable (пригодные для сравнения), функция может, например, сравнивать все значения ячеек массива.
Небольшое замечание. Переменные и функции в протоколах могут быть статическими (static).
Использование @ objc
Главное, что нужно знать в данном вопросе — это то, что @ objc протоколы Swift видны в Objective-C коде. Строго говоря, для этого «волшебное слово» @ objc и существует. Но всё остальное остаётся без изменений
Протоколы такого типа могут быть наследоваными только классами. Для перечислений и структур это сделать нельзя.
То есть только так.
Стоит отметить, что в этом случае появляется возможность определять опциональные функции (@obj optional func), которые при желании можно не реализовывать, как для функции test() в предыдущем примере. Но условно опциональные функции можно реализовать и при помощи расширения протокола с пустой иплементацией.
Наследование типами
Создавая класс, структуру или перечисление, мы можем обозначить наследование определенного протокола – в этом случае унаследованные параметры будут работать и для нашего класса, и для всех остальных классов, которые наследуют этот класс, даже если мы не имеем к ним доступа.
Кстати, в этом контексте появляется одна очень интересная задачка. Допустим у нас есть протокол. Есть некоторый класс. И класс реализует протокол, а в нем есть функция work(). Что будет, если у нас есть extension протокола, в котором также есть метод work(). Какой из них будет вызван при обращении к методу?
Запущен будет метод класса – таковы уж особенности диспетчеризации методов в Swift. И этот ответ дают многие соискатели. Но вот на вопрос, как сделать так, чтобы в коде был выполнен не метод класса, а метод протокола, знают ответ лишь немногие. Однако и для этой задачи есть решение – оно подразумевает удалени функции из определения протокола и вызов метода следующим образом:
Generic-протоколы
В Swift также есть дженерик-протоколы (generic protocol) с абстрактными ассоциативными типами (associated types), которые позволяют определять переменные типов. Такому протоколу можно присваивать дополнительные условия, которые накладываются на ассоциативные типы. Несколько подобных протоколов позволяют выстраивать сложные конструкции, необходимые для формирования архитектуры приложения.
Однако реализовать переменную в виде дженерик-протокола нельзя. Его можно только наследовать. Эти конструкции используются для того, чтобы создавать зависимости в классах. То есть мы можем описать некоторый абстрактный generic-класс, чтобы определить используемые в нем типы.
Следует помнить, что дженерик-протоколы отличаются высоким уровнем абстракции. Поэтому в самих приложениях они могут быть излишними. Но при этом дженерик-протоколы используются при программировании библиотек.
Классовые протоколы
В Swift существуют также class-bound (ограниченные классом) протоколы. Для их описания применяется два вида синтаксиса
По словам разработчиков языка, использование этих синтаксисов равнозначно, но ключевое слово class используется только в этом месте, в отличие от AnyObject, который является протоколом.
Тем временем, как мы видим в ходе собеседований, люди часто не могут объяснить, что такое классовый протокол, и зачем он нужен. Его суть заключается в том, что мы получаем возможность использовать некоторый объект, которой был бы протоколом, и одновременно с этим функционировал бы как ссылочный тип. Пример:
В iOS используются управление памятью по методу automatic reference count, что подразумевает наличие сильных и слабых ссылок. И в некоторых случаях следует учитывать, какие именно — сильные (strong) или слабые (weak) – переменные используются в классах.
Проблема заключается в том, что при использовании некоторого протокола в качестве типа, при описании переменной (являющейся сильной ссылкой), может возникнуть циклические связи (retain cycle), приводящие к утечкам памяти, потому что объекты будут держаться везде сильными ссылками. Также неприятности могут возникнуть, если вы всё-таки решили писать код в соответствии с принципами SOLID.
Чтобы не возникали такие ситуации, в Swift используют классовые протоколы, которые позволяют изначально задавать «слабые» переменные. Классовый протокол разрешает держать объект слабой ссылкой. Пример, где это часто стоит учитывать, называется делегат.
Другой пример, где стоит использовать классовые протоколы, – это явное указание, что объект передается по ссылке.
Множественное наследование и диспетчеризация методов
Как говорилось в начале статьи, протоколы можно множественно наследовать. То есть,
Это полезно, но какие подводные камни здесь скрыты? Всё дело в том, что сложности, по крайней мере на первый взгляд, возникают из-за диспетчеризации методов (method dispatch). Говоря простыми словами, может быть непонятно, какой метод будет вызываться – родительский или из текущего типа.
Чуть выше, мы уже раскрыли тему того, как работает код, он вызывает метод класса. То есть, как ожидается.
Но если попробовать убрать сигнатуру метода из определения протокола, то происходит «магия». Собственно говоря, это вопрос из собеседования: «как сделать, чтобы вызвалась функция из протокола?»
Но если использовать переменную не как протокол, а как класс, то всё будет нормально.
Все дело в статической диспетчеризации методов при расширении протокола. И это надо учитывать. Причем тут множественное наследование? А вот при чем: если взять два протокола с реализованными функциями, такой код не сработает. Чтобы функция была выполнена, потребуется явно делать каст к нужному протоколу. Такой вот отголосок множественного наследования из C++.
Схожая история будет, если наследовать один протокол другим, где есть функции, которые реализуются в расширениях. Компилятор не даст его собрать.
Два последних примера показывают, что полностью заменять протоколы классами не стоит. Можно запутаться в статической диспетчеризации.
Дженерики и протоколы
Можно сказать, что это — вопрос со звёздочкой, который вовсе и не надо спрашивать. Но кодеры любят сверхабстрактные конструкции, и конечно, парочка ненужных generic class обязательно должны быть в проекте (куда ж без этого). Но программист не был бы программистом, если бы не захотел всё это обернуть это в ещё одну абстракцию. И Swift, являясь хоть и молодым, но динамично развивающимся языком, даёт такую возможность, но в ограниченном варианте. (да, это уже не про мобилки).
Во-первых, полноценная проверка на возможное наследование есть только в Swift 4.2, то есть только с осени будет возможность это нормально использовать в проектах. На Swift 4.1 выдаётся сообщение, что возможность ещё не реализована.
Для Swift 4.1 выводится следующее:
Тогда как в Swift 4.2 всё работает, как ожидается:
Также стоит заметить, что можно наследовать протокол только при одном типе связей. Если есть два типа связей, то наследование будет запрещено на уровне компилятора. Подробное объяснение того, что можно, а что нельзя, показано тут.
Но, не смотря на эти сложности, работа со связями в дженериках достаточно удобная.
Swift. Протоколы
Материал является примером, который демонстрирует возможности языка и не претендует на звание «хороший код».
Материал предназначен начинающим разработчикам для общего ознакомления.
Весь свой информационный мусор, я коллекционирую на своей стене в ВК, так что добро пожаловать.
Немного от себя
Вот уж не знаю почему, но многих протоколы пугают, их боятся использовать, понимать и в целом избегают как огня, что является в корне неверным выбором. Иными словами, если Вам не нравятся протоколы, Вы просто не умеете их готовить.
Если разобраться поглубже, язык Swift, является крайне продуманным и собрал в себе все лучшее из многообразия языков. Он быстрый, продуманный и очень оптимизированный. Каждый его инструмент, всегда преследует определенную цель и не всегда понятно с первого раза, какую же такую цель преследует изучаемый инструмент, но поверьте, цель есть всегда. Так и у протоколов есть масса возможностей и перекочевали они с Objective-C не просто так. Начнем мы с простого и плавно перейдем к более сложному (хотя протоколы весьма просты в усвоении и очень даже функциональны).
Вопреки критике, я буду все так же, подробно, разбирать каждый момент т.к. напоминаю, что материал предназначен именно для начинающих разработчиков. Ну что же, давайте начинать.
Что такое протокол
Давайте начнем с постановки простой, фейковой задачи. Нам необходимо разработать функцию, которая получает некий класс/структуру, берет свойство count и возвращает нам квадрат этого значения, пускай в Int. Эту функцию мы будем передавать другим разработчикам, чтобы они могли её использовать.
Казалось бы, в чем проблема? Но давайте подумаем. Какой тип данных указать во входных параметрах функции? Any? Не очень удачное решение т.к. никто не знает какой тип данных будет использован в ней и приведение типов выполнить не получится.
Можно конечно решить ситуацию посредством дженериков, но это тема для другой публикации.
Подумайте немного, над этой задачей. Она не так проста как кажется на первый взгляд и все было бы печально, если бы не протоколы.
Посмотрите на код ниже, и начинайте постигать убийственную мощь протоколов.
Внезапно и удивительно, но оказывается, что протокол является типом данных. Мы можем его создать, описать и использовать. В момент создания протокола, Вы просто описываете содержимое потенциального типа, который будет его использовать (будет следовать протоколу, будет подписан на протокол и т.д.), а потом в момент создания типа, указываете протокол, чтобы сказать компилятору «привет, я класс и я следую протоколу», если тип не соответствует протоколу, Xcode выдаст подсказку и в большинстве случаев, даже предложит исправить несоответствие. В коде выше, как Вы уже вероятно догадались, мы создали простой протокол, который декларирует простое свойство count и его тип данных, не более (конструкция , говорит о том, что свойство должно быть доступно для чтения) А потом, с его помощью(протокола), создали функцию из первоначальной задачи. Давайте теперь создадим класс, который соответствует протоколу.
синтаксис такой же как при наследовании классов
Так как у нас были указания исключительно о необходимости наличия свойства count для чтения, нам никто не запрещает использовать константу и класс выше соответствует декларации. Теперь создадим структуру под наш протокол.
У нее есть, еще свойство, помимо count, но это не является нарушением протокола т.к. протокол говорить только о том, что должно быть, а не то, чего быть не должно. Давайте попробуем это с нашей функцией.
Все работает. Давайте добавим классу MegaClass, свойство property для наглядности. Теперь обратите внимание на важный момент. Если мы будем обращаться к типу, который соответствует протоколу внутри функции, даже при наличии иных свойств/методов, нам будет доступно только описанное в протоколе. Грубо говоря, протокол определяет, что конкретно есть у типа и дает возможность с этим взаимодействовать.
Точно таким же образом, мы можем поместить разные типы в массив если они соответствуют определенному протоколу.
Наследование протоколов
Конечно же протоколы умеют в наследование, причем множественное и типы данных могут соответствовать нескольким протоколам
Если класс имеет суперкласс в виде класса, который соответствует какому-либо протоколу, то естественно он так же будет ему соответствовать.
Опять же ожидаемо и использование подобных типов в работе без каких либо проблем. Давайте для более простого понимая, представим следующее:
протоколы Папа, Мама и общий протокол Ребенок.
Некий класс Класс, который соответствует протоколу Ребенок у которого есть подкласс Подкласс.
Некая функция/массив/что-либо еще, умеющие работать с типами, которые соответствуют протоколу Мама
Мы можем передать в функцию/массив и т.д. которые требуют соответствие протоколу Мама как Класс так и Подкласс т.к. они оба соответствую ему (протоколу Мама) через наследование. Эта логичная концепция конечно бывает сложна для понимания, но на самом деле очень проста. Необходимо просто переварить. А еще лучше просто побаловаться в коде. Ничего не заменит практику.
Если классу необходимо наследовать какой-либо класс и соответствовать протоколу/протоколам, то первым указывается суперкласс, а далее идут протоколы.
Протоколы могут декларировать так же методы и инициализаторы, но про инициализаторы в протоколах лучше говорить в отдельной статье т.к. это обширный материал, пока могу сказать, что инициализатор следует помечать как обязательный для потенциальных подклассов, через required
как видно из листинга выше, нам необязательно использовать аргумент инициализатора в подобной реализации т.к. протокол определяет инициализатор, а не его тело. С методами та же история, почти.
обратите внимание — для удобства восприятия, я помечаю типы цифрами, никогда не делайте так в коде
мы определяем тип метода, а что в нем происходит, протокол не интересует
Если метод должен изменять значения, то его следует пометить как mutating, это не обязательное требование, но его игнорирование заблокирует нормальное использование такого протокола со структурами.
Хотя протоколы можно использовать с различными типами, мы можем ограничить протокол только для использования с классами
ранее использовалось для декларирования слабых ссылок (в Swift 5 вызовет ошибку)
обратите внимание констукция говорит о том, что свойство должно уметь менять значение т.е. константой его объявлять нельзя, только переменной. И как обсуждалось ранее, get разрешает использовать константы. Использовать set без get нельзя.
сейчас практическое применение может понадобится, например для использования опциональных методов. Это невозможно в Swift реализации, но было возможно в Objective-c, а так как Swift обратно совместим с ним, мы все же можем использовать эту возможность.
не забудте импортировать Foundation для доступа к @objc
но в таком случае у нас нет жесткой необходимости использовать AnyObject т.к. подобный протокол и так не будет функционировать со структурами.
Соответственно опциональные методы как следует из названия не являются обязательными и класс без них, вполне себе будет соответствовать протоколу
Расширения протоколов
Ну вот мы и добрались до истинной мощи протоколов. Казалось бы, ну декларация, ну удобная, но мне и так нормально, сами используйте свои протоколы, мне и так хорошо. Работаю я один, свой код знаю, все пучком. Нет! Это ошибочное суждение!Уверен сейчас, ваше мировоззрение изменится раз и навсегда, а первая мысль будет «черт, почему я не использовал это раньше. », начнем.
Сейчас вам должно уже быть все понятно. Есть два протокола, который декларируют по свойству. Протокол который наследуется от них и два типа, класс со структурой. А теперь добавим вот такой код
Однако здравствуйте, я думаю вы уже поняли, все эти возможности появляются у наших типов, которые соответствуют протоколу в доступе.
В расширениях протокола можно не только объявлять методы, но и задавать им логику. Дополнять протокол вычисляемыми значениями и даже определять инициализатор, но с нюансами. Давайте изменим наш код
добавим инициализатор в протокол, это заставит нас изменить наш класс (структуры не касается т.к. структуры не наследуются)
и расширим наш SuperProtocol дополнительным инициализатором
Нюансы и особенности работы с протоколами
Диспетчеризация методов, посмотрите на код
как думаете, какой метод будет вызван?
method dispatch работает таким образом, что отдает приоритет типу, а константа myClass относится к типу ClassExample т.к. неявное присваивание типа, определяет её как класс, а не протокол. Но если мы явно укажем тип ProtocolExample, то будет вызван метод протокола.
Соответственно во всех функциях/методах/массивах и т.д., во всем, что работает с типом протокола, приоритет будет за методом протокола.
2. Можно перечислять несколько протоколов для взаимодействия
3. В протоколах нельзя определять доступы (private и т.д.), но можно сделать вот так
Итоги по протоколам:
Используются с class, struct, enum
Не хранят состояние
Могут быть унаследованы другими протоколами