Что такое protocol в swift

Обновлено: 02.07.2024

Протоколы — фундаментальное свойство Swift. Они играют важную роль в стандартных библиотеках Swift и являются обычным способом абстракции кода. Во многом они похожи на интерфейсы в других языках программирования.

В этом руководстве мы представим вам подход к разработке приложений, называемый «протокольно-ориентированным программированием», который стал практически основным в Swift. Это действительно то, что вам необходимо уяснить при изучении Swift!

В этом руководстве вы:

  • поймёте разницу между объектно-ориентированным и протокольно-ориентированным программированием;
  • разберётесь со стандартной реализаций протоколов;
  • научитесь расширять функционал стандартной библиотеки Swift;
  • узнаете, как расширять протоколы при помощи дженериков.

Для чего нужны протоколы в Swift?

Мобильные разработчики часто обходятся вообще без использования протоколов, но при этом теряется возможность работать с некоторыми сущностями абстрактно. Если выделить основные особенности протоколов в Swift, у нас получится следующие 7 пунктов:

  • Протоколы обеспечивают множественное наследование
  • Протоколы не могут хранить состояние
  • Протоколы могут быть унаследованы другими протоколами
  • Протоколы могут применяться к структурам (struct), классам(class) и перечислениям (enum), определяя функционал типов
  • Дженерик протоколы (Generic-protocol) позволяют задавать сложные зависимости между типами и протоколами во время их наследования
  • Протоколы не определяют «сильные» и «слабые» ссылки на переменные
  • В расширениях к протоколам можно описывать конкретные реализации методов, и вычисляемых переменных (computed values)
  • Классовые протоколы разрешают себя наследовать только классам


При этом типы коллекций (сollection), а именно – array, set, dictionary – также можно упаковывать в протокол, потому что они тоже являются структурами. Например, словарь (Dictionary) определяется следующим образом


Обычно в объектно-ориентированном программировании применяется понятие классов, и всем хорошо знаком механизм наследования методов и переменных родительского класса классом-потомком. При этом никто не запрещает ему содержать дополнительные методы и переменные.

В случае с протоколами можно создать куда более интересную иерархию взаимосвязей. Для описания очередного класса можно использовать несколько протоколов одновременно, что позволяет создавать достаточно сложные конструкции, которые будут удовлетворять множеству условий одновременно. С другой стороны, ограничения разных протоколов позволяют сформировать некоторый объект, предназначенный только для узкого применения и содержащий определенный ряд функций.

Реализация протокола в Swift достаточно проста. Синтаксис подразумевает название, ряд методов и параметры (переменные), которые он будет содержать.


Кроме этого в Swift протоколы могут содержать не только наименования методов, но и их реализацию. Код метода в протоколе добавляются через расширения (extension). В документации можно найти много упоминаний расширений, но применительно к протоколам в расширениях можно размещать имя функции и тело функции.


То же самое можно делать с переменными.


Если мы где-то будем применять объект, связанный с протоколом, можно задать в нем переменную с фиксированным или передаваемым значением. Фактически переменная представляет собой небольшую функцию без входных параметров…или с возможностью прямого назначения параметра.

Расширение протокола в Swift позволяет реализовать тело переменной, и тогда она по факту будет представлять собой computed value – вычисляемый параметр с функциями get и set. То есть такая переменная не будет хранить никаких значений, а будет или играть роль функции или функций, или будет играть роль прокси для какой-то другой переменной.

Или если мы берем какой-то класс или структуру и реализуем протокол, то в нем можно использовать обычную переменную:


Стоит отметить, что переменные в определении протокола не могут быть weak. (weak – это вариант реализации переменой).

Есть и более интересные примеры: можно реализовать расширение массива и добавить туда функцию, связанную с типом данных массива. Например, если массив содержит целочисленные значения или имеет формат equitable (пригодные для сравнения), функция может, например, сравнивать все значения ячеек массива.


Небольшое замечание. Переменные и функции в протоколах могут быть статическими (static).

Protocol used in structure Syntax


Syntax


Мобильная разработка. Swift: таинство протоколов

Сегодня мы продолжаем цикл публикаций на тему мобильной разработки под iOS. И если в прошлый раз речь шла о том, что нужно и не нужно спрашивать на собеседованиях, в этом материале мы коснемся тематики протоколов, которая имеет в Swift важное значение. Речь пойдет о том, как устроены протоколы, чем они отличаются друг от друга, и как сочетаются с интерфейсами Objective-C.




Как мы уже говорили ранее, новый язык Apple продолжает активно развиваться, и большинство его параметров и особенностей явно указаны в документации. Но кто читает документацию, когда код нужно написать здесь и сейчас? Поэтому давайте пройдемся по основным особенностям протоколов Swift прямо в нашем посте.

Для начала стоит оговориться, что протоколы Apple – это альтернативный термин для понятия «Интерфейс», которое применяется в других языках программирования. В Swift протоколы служат для того, чтобы обозначить шаблоны определенных структур (т.н. blueprint), с которыми можно будет работать на абстрактном уровне. Говоря простыми словами, протокол определяет ряд методов и переменных, которые в обязательном порядке должен наследовать определенный тип.

Далее в статье будут постепенно раскрываться моменты следующим образом: от простых и часто используемых к более сложным. В принципе, на собеседованиях можно давать вопросы в таком порядке, так как они определяют уровень компетенции соискателя — от уровня джунов до уровня сеньоров.

Протоколы в стандартных библиотеках Swift

Как видите, протоколы — эффективный способ расширять и настраивать типы. В стандартной библиотеке Swift это их свойство также широко применяется.

Добавьте этот код в playground:


Вы наверняка знаете, что выведет этот код, но, возможно, удивитесь использованным здесь типам.

Например, slice — не Array, а ArraySlice. Это специальная «обёртка», которая обеспечивает эффективный способ работы с частями массива. Соответственно, reversedSlice — это ReversedCollection<ArraySlice>.

К счастью, функция map определена как расширение к протоколу Sequence, которому соответствуют все типы-коллекции. Это позволяет нам применять функцию map как к Array, так и к ReversedCollection и не замечать разницы. Скоро вы воспользуетесь этим полезным приёмом.

Generic-протоколы

В Swift также есть дженерик-протоколы (generic protocol) с абстрактными ассоциативными типами (associated types), которые позволяют определять переменные типов. Такому протоколу можно присваивать дополнительные условия, которые накладываются на ассоциативные типы. Несколько подобных протоколов позволяют выстраивать сложные конструкции, необходимые для формирования архитектуры приложения.

Однако реализовать переменную в виде дженерик-протокола нельзя. Его можно только наследовать. Эти конструкции используются для того, чтобы создавать зависимости в классах. То есть мы можем описать некоторый абстрактный generic-класс, чтобы определить используемые в нем типы.


Следует помнить, что дженерик-протоколы отличаются высоким уровнем абстракции. Поэтому в самих приложениях они могут быть излишними. Но при этом дженерик-протоколы используются при программировании библиотек.

Мобильная разработка. Swift: таинство протоколов

Сегодня мы продолжаем цикл публикаций на тему мобильной разработки под iOS. И если в прошлый раз речь шла о том, что нужно и не нужно спрашивать на собеседованиях, в этом материале мы коснемся тематики протоколов, которая имеет в Swift важное значение. Речь пойдет о том, как устроены протоколы, чем они отличаются друг от друга, и как сочетаются с интерфейсами Objective-C.




Как мы уже говорили ранее, новый язык Apple продолжает активно развиваться, и большинство его параметров и особенностей явно указаны в документации. Но кто читает документацию, когда код нужно написать здесь и сейчас? Поэтому давайте пройдемся по основным особенностям протоколов Swift прямо в нашем посте.

Для начала стоит оговориться, что протоколы Apple – это альтернативный термин для понятия «Интерфейс», которое применяется в других языках программирования. В Swift протоколы служат для того, чтобы обозначить шаблоны определенных структур (т.н. blueprint), с которыми можно будет работать на абстрактном уровне. Говоря простыми словами, протокол определяет ряд методов и переменных, которые в обязательном порядке должен наследовать определенный тип.

Далее в статье будут постепенно раскрываться моменты следующим образом: от простых и часто используемых к более сложным. В принципе, на собеседованиях можно давать вопросы в таком порядке, так как они определяют уровень компетенции соискателя — от уровня джунов до уровня сеньоров.

Займёмся кодом

Запускаем Xcode, создаём playground, сохраняем как SwiftProtocols.playground, добавляем этот код:

Скомпилируем при помощи Command-Shift-Return, чтобы быть уверенным, что все в порядке.

Здесь мы определяем простой протокол Bird, со свойствами name и canFly. Затем определяем протокол Flyable со свойством airspeedVelocity.

В «допротокольную эпоху» разработчик начал бы с класса Flyable в качестве базового, а затем, используя наследование, определил бы Bird и всё прочее, что может летать.

Но в протокольно-ориентированном программировании всё начинается с протокола. Эта техника позволяет нам инкапсулировать набросок функционала без базового класса.

Как вы сейчас увидите, это делает процесс проектирования типов гораздо гибче.

Example

In the above program, the StudentName protocol has a single requirement for a gettable and settable String property. The StudentAge protocol has a single requirement for a gettable and settable String property.

Both protocols are adopted by a structure called StudentDetails . In structure-function defines printingDetails(details:) . The type of the details parameter is StudentName & StudentAge . which means “any type that conforms to both the Named and Aged protocols”.

First, create a new StudentDetails an instance for student and passes this new instance to the printingDetails function. Because StudentDetals conforms to both protocols, this call is valid, so printingDetails() function can print name and age .

Protocol in swift


A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

Protocol syntax


placing the protocol’s name after the type’s name, separated by a colon, as part of their definition.

Multiple protocols can be listed, and are separated by commas

Начинаем

Представьте, что вы разрабатываете игру — гонки. Ваши игроки могут гонять на машинах, мотоциклах и на самолётах. И даже летать на птицах, это же ведь игра, верно? Основное здесь то, что есть дофига всяких «штук», на которых можно гонять, летать и т.п.

Обычный подход при разработке подобных приложений состоит в объектно-ориентированном программировании. В этом случае мы заключаем всю логику в неких базовых классах, от которых в дальнейшем наследуемся. Базовые классы, таким образом, должны содержать внутри логику «вести» и «пилотировать».

Мы начинаем разработку с создания классов для каждого средства передвижения. «Птиц» отложим пока на потом, к ним мы вернёмся чуть позже.

Мы видим, что Car и Motorcycle весьма похожи по функционалу, так что мы создаём базовый класс MotorVehicle. Car и Motorcycle будут наследоваться из MotorVehicle. Таким же образом мы создаем базовый класс Aircraft, от которого создадим класс Plane.

Вы думаете, что всё прекрасно, но — бац! — действие вашей игры происходит в XXX столетии, и некоторые машины уже могут летать.

Итак, у нас случился затык. У Swift нет множественного наследования. Каким образом ваши летающие автомобили смогут наследоваться и от MotorVehicle и от Aircraft? Создавать еще один базовый класс, в котором соединятся обе функциональности? Скорее всего, нет, так как нет ясного и простого способа добиться этого.

И что же спасёт нашу игру в этой ужасной ситуации? На помощь спешит протокольно-ориентированное программирование!

Protocol Requirments

Protocol is used to specify particular class type property or instance property. It just specifies the type or instance property alone rather than specifying whether it is a stored or computed property. Also, it is used to specify whether the property is ‘gettable’ or ‘gettable and settable’.

Example

Syntax


Example

In the above program, the StudentDetails protocol can be extended to be provide age() method , which uses to return a string value. By creating an extension on the protocol, all conforming types automatically gain this method implementation without any additional modification.

Protocol extensions can add implementations to conforming types but can’t make a protocol extend or inherit from another protocol.

Использование @ objc

Главное, что нужно знать в данном вопросе — это то, что @ objc протоколы Swift видны в Objective-C коде. Строго говоря, для этого «волшебное слово» @ objc и существует. Но всё остальное остаётся без изменений

Протоколы такого типа могут быть наследоваными только классами. Для перечислений и структур это сделать нельзя.

То есть только так.


Стоит отметить, что в этом случае появляется возможность определять опциональные функции (@obj optional func), которые при желании можно не реализовывать, как для функции test() в предыдущем примере. Но условно опциональные функции можно реализовать и при помощи расширения протокола с пустой иплементацией.

Множественное наследование и диспетчеризация методов

Как говорилось в начале статьи, протоколы можно множественно наследовать. То есть,


Это полезно, но какие подводные камни здесь скрыты? Всё дело в том, что сложности, по крайней мере на первый взгляд, возникают из-за диспетчеризации методов (method dispatch). Говоря простыми словами, может быть непонятно, какой метод будет вызываться – родительский или из текущего типа.

Чуть выше, мы уже раскрыли тему того, как работает код, он вызывает метод класса. То есть, как ожидается.


Но если попробовать убрать сигнатуру метода из определения протокола, то происходит «магия». Собственно говоря, это вопрос из собеседования: «как сделать, чтобы вызвалась функция из протокола?»


Но если использовать переменную не как протокол, а как класс, то всё будет нормально.


Все дело в статической диспетчеризации методов при расширении протокола. И это надо учитывать. Причем тут множественное наследование? А вот при чем: если взять два протокола с реализованными функциями, такой код не сработает. Чтобы функция была выполнена, потребуется явно делать каст к нужному протоколу. Такой вот отголосок множественного наследования из C++.


Схожая история будет, если наследовать один протокол другим, где есть функции, которые реализуются в расширениях. Компилятор не даст его собрать.


Два последних примера показывают, что полностью заменять протоколы классами не стоит. Можно запутаться в статической диспетчеризации.

Method requirement

These methods are written as part of the protocol’s definition in the same way as for normal instance and type methods, but without curly braces or a method body. Method parameters can’t be specified within a protocol’s definition.

Расширяем протокол поведением по умолчанию

Расширения протокола позволяют задать поведение протокола по умолчанию. Напишите этот код сразу за определением протокола Bird:

Этот код определяет расширение протокола Bird. В этом расширении определяется, что свойство canFly вернёт true в случае, когда тип соответствует протоколу Flyable. Другими словам, всякой Flyable-птице больше не нужно явно задавать canFly.

Теперь удалим let canFly = . из определений FlappyBird, Penguin и SwiftBird. Скомпилируйте код и убедитесь, что всё в порядке.

Займёмся перечислениями

Перечисления в Swift могут соответствовать протоколам. Добавьте следующее определение перечисления:

Определяя соответствующие свойства, UnladenSwallow соответствует двум протоколам — Bird и Flyable. Такими образом, реализуется определение по умолчанию для canFly.

Дженерики и протоколы

Можно сказать, что это — вопрос со звёздочкой, который вовсе и не надо спрашивать. Но кодеры любят сверхабстрактные конструкции, и конечно, парочка ненужных generic class обязательно должны быть в проекте (куда ж без этого). Но программист не был бы программистом, если бы не захотел всё это обернуть это в ещё одну абстракцию. И Swift, являясь хоть и молодым, но динамично развивающимся языком, даёт такую возможность, но в ограниченном варианте. (да, это уже не про мобилки).


Для Swift 4.1 выводится следующее:


Тогда как в Swift 4.2 всё работает, как ожидается:


Также стоит заметить, что можно наследовать протокол только при одном типе связей. Если есть два типа связей, то наследование будет запрещено на уровне компилятора. Подробное объяснение того, что можно, а что нельзя, показано тут.


Но, не смотря на эти сложности, работа со связями в дженериках достаточно удобная.

Соединяем всё вместе

Чтобы как-то объединить столь разных гонщиков, нам нужен общий протокол для гонок. Мы сможем все это сделать, даже не трогая все созданные нами до этого типы, при помощи замечательной вещи, которая называется ретроактивное моделирование. Просто добавьте это в playground:


Вот что мы тут делаем: сначала определяем протокол Racer. Это всё то, что может участвовать в гонках. Затем мы приводим все наши созданные до этого типы к протоколу Racer. И, наконец, мы создаём Array, который содержит в себе экземпляры каждого нашего типа.

Скомпилируйте playground, чтобы все было в порядке.

Example

Here’s an implementation of a class that adopts and conforms to the Identifiable protocol.

Классовые протоколы

В Swift существуют также class-bound (ограниченные классом) протоколы. Для их описания применяется два вида синтаксиса


По словам разработчиков языка, использование этих синтаксисов равнозначно, но ключевое слово class используется только в этом месте, в отличие от AnyObject, который является протоколом.

Тем временем, как мы видим в ходе собеседований, люди часто не могут объяснить, что такое классовый протокол, и зачем он нужен. Его суть заключается в том, что мы получаем возможность использовать некоторый объект, которой был бы протоколом, и одновременно с этим функционировал бы как ссылочный тип. Пример:

В iOS используются управление памятью по методу automatic reference count, что подразумевает наличие сильных и слабых ссылок. И в некоторых случаях следует учитывать, какие именно — сильные (strong) или слабые (weak) – переменные используются в классах.

Проблема заключается в том, что при использовании некоторого протокола в качестве типа, при описании переменной (являющейся сильной ссылкой), может возникнуть циклические связи (retain cycle), приводящие к утечкам памяти, потому что объекты будут держаться везде сильными ссылками. Также неприятности могут возникнуть, если вы всё-таки решили писать код в соответствии с принципами SOLID.


Чтобы не возникали такие ситуации, в Swift используют классовые протоколы, которые позволяют изначально задавать «слабые» переменные. Классовый протокол разрешает держать объект слабой ссылкой. Пример, где это часто стоит учитывать, называется делегат.


Другой пример, где стоит использовать классовые протоколы, – это явное указание, что объект передается по ссылке.

Mutating methods

If you define a protocol instance method requirement that is intended to mutate instances of any type that adopts the protocol, mark the method with the mutating keyword as part of the protocol’s definition. This enables structures and enumerations to adopt the protocol and satisfy that method requirement.

you don’t need to write the mutating keyword when writing an implementation of that method for a class.This keywords is only used by structures and enumerations.

Class example

Swift reports an error at compile-time if a protocol requirement is not fulfilled

Наследование типами

Создавая класс, структуру или перечисление, мы можем обозначить наследование определенного протокола – в этом случае унаследованные параметры будут работать и для нашего класса, и для всех остальных классов, которые наследуют этот класс, даже если мы не имеем к ним доступа.

Кстати, в этом контексте появляется одна очень интересная задачка. Допустим у нас есть протокол. Есть некоторый класс. И класс реализует протокол, а в нем есть функция work(). Что будет, если у нас есть extension протокола, в котором также есть метод work(). Какой из них будет вызван при обращении к методу?


Запущен будет метод класса – таковы уж особенности диспетчеризации методов в Swift. И этот ответ дают многие соискатели. Но вот на вопрос, как сделать так, чтобы в коде был выполнен не метод класса, а метод протокола, знают ответ лишь немногие. Однако и для этой задачи есть решение – оно подразумевает удалени функции из определения протокола и вызов метода следующим образом:

Заключение

Здесь вы можете загрузить полный исходный код playground.

Вы узнали о возможностях протокольно-ориентированного программирования, создавая простые протоколы и увеличивая их возможности их при помощи расширений. Используя реализацию по умолчанию, вы даете протоколам соответсвующее «поведение». Почти как с базовыми классами, но только лучше, так как все это применимо также к структурам и перечислениям.

Вы также увидели, что расширение протоколов применимо и к базовым протоколам Swift.

Вы можете также посмотреть прекрасную лекцию на WWDC, посвященную протокольно-ориентированному программированию.

Как и с любой парадигмой программирования, есть опасность увлечься и начать использовать протоколы налево и направо. Здесь интересная заметка о том, что стоит опасаться решений в стиле «серебряная пуля».

Что такого в протокольно-ориентированном программировании?

Протоколы позволяют группировать похожие методы, функции и свойства применительно к классам, структурам и перечислениям. При этом только классы позволяют использовать наследование от базового класса.

Преимущество протоколов в Swift состоит в том, что объект может соответствовать нескольким протоколам.

Ваш код при при использовании такого метода становиться более модульным. Думайте о протоколах как о строительных блоках функционала. Когда вы добавляете новую функциональность объекту, делая его соответствующим некоему протоколу, вы не делаете совершенно новый объект «с нуля», это было бы слишком долго. Вместо этого, вы добавляете разные строительные блоки до тех пор, пока объект не будет готов.

Переход от базового класса к протоколам решит нашу проблему. С протоколами мы можем создать класс FlyingCar, который соответствует и MotorVehicle и Aircraft. Миленько, да?

Syntax


The required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer

The use of the required modifier ensures that you provide an explicit or inherited implementation of the initializer requirement on all subclasses of the conforming class, such that they also conform to the protocol.

Initializer Requirements

Protocols can require specific initializers to be implemented by conforming types. You write these initializers as part of the protocol’s definition in the same way as for normal initializers, but without curly braces or an initializer body.

Делаем функцию более обобщенной

Предположим, что массив Racers достаточно велик, а нам нужно найти максимальную скорость не во всем массиве, а в какой-то его части. Решение состоит в том, чтобы изменить topSpeed(of:) таким образом, чтобы она принимала в качестве аргумента не конкретно массив, а всё, что соответствует протоколу Sequence.

Заменим нашу реализацию topSpeed(of:) следующим образом:

  1. RacersType — это generic-тип аргумента нашей функции. Он может быть любым, соответствующим протоколу Sequence.
  2. where определяет, что содержимое Sequence должно соответствовать протоколу Racer.
  3. Тело самой функции осталось без изменений.


Теперь наша функция работает с любым типом, отвечающим протоколу Sequence, в том числе и с ArraySlice.

Компараторы протоколов

Другая особенность протоколов Swift — это то, как вы определяете операторы равенства объектов или их сравнения. Напишем следующее:


Имея протокол Score можно писать код, который обращается со всеми элементами этого типа одним образом. Но если завести вполне определенный тип, такой как RacingScore, то вы не спутаете его с другими производными от протокола Score.

Мы хотим, чтобы очки (scores) можно было сравнивать, чтобы понять, у кого максимальный результат. До Swift 3 разработчикам было нужно писать глобальные функции для определения оператора к протоколу. Теперь же мы можем определить эти статические методы в самой модели. Сделаем это, заменив определения Score и RacingScore следующим образом:


Мы заключили всю логику для RacingScore в одном месте. Протокол Comparable требует опеределить реализацию только для функции «меньше, чем». Все остальные функции сравнения будут реализованы автоматически, на основании созданной нами реализации функции «меньше, чем».

Structure Example

Here’s an example of a simple structure that adopts and conforms to the Identifiable protocol

In the above program, the structure adopts the Identifiable protocol as part of the first two lines of its definition.

Class Implementations of Protocol Initializer Requirements

Классовые протоколы

В Swift существуют также class-bound (ограниченные классом) протоколы. Для их описания применяется два вида синтаксиса


По словам разработчиков языка, использование этих синтаксисов равнозначно, но ключевое слово class используется только в этом месте, в отличие от AnyObject, который является протоколом.

Тем временем, как мы видим в ходе собеседований, люди часто не могут объяснить, что такое классовый протокол, и зачем он нужен. Его суть заключается в том, что мы получаем возможность использовать некоторый объект, которой был бы протоколом, и одновременно с этим функционировал бы как ссылочный тип. Пример:

В iOS используются управление памятью по методу automatic reference count, что подразумевает наличие сильных и слабых ссылок. И в некоторых случаях следует учитывать, какие именно — сильные (strong) или слабые (weak) – переменные используются в классах.

Проблема заключается в том, что при использовании некоторого протокола в качестве типа, при описании переменной (являющейся сильной ссылкой), может возникнуть циклические связи (retain cycle), приводящие к утечкам памяти, потому что объекты будут держаться везде сильными ссылками. Также неприятности могут возникнуть, если вы всё-таки решили писать код в соответствии с принципами SOLID.


Чтобы не возникали такие ситуации, в Swift используют классовые протоколы, которые позволяют изначально задавать «слабые» переменные. Классовый протокол разрешает держать объект слабой ссылкой. Пример, где это часто стоит учитывать, называется делегат.


Другой пример, где стоит использовать классовые протоколы, – это явное указание, что объект передается по ссылке.

Расширяем протокол

Вы также можете сделать свой собственный протокол соответствующим другому протоколу из стандартной библиотеки Swift и определить поведение по умолчанию. Замените объявление протокола Bird следующим кодом:


Соответствие протоколу CustomStringConvertible означает, что у вашего типа должно быть свойство description. Вместо того, чтобы добавлять это свойство в типе Bird и во всех производных от него, мы определяем расширение протокола CustomStringConvertible, которое будет ассоциировано только с типом Bird.

Наберите UnladenSwallow.african внизу playground. Скомпилируйте и вы увидите “I can fly”.

Generic-протоколы

В Swift также есть дженерик-протоколы (generic protocol) с абстрактными ассоциативными типами (associated types), которые позволяют определять переменные типов. Такому протоколу можно присваивать дополнительные условия, которые накладываются на ассоциативные типы. Несколько подобных протоколов позволяют выстраивать сложные конструкции, необходимые для формирования архитектуры приложения.

Однако реализовать переменную в виде дженерик-протокола нельзя. Его можно только наследовать. Эти конструкции используются для того, чтобы создавать зависимости в классах. То есть мы можем описать некоторый абстрактный generic-класс, чтобы определить используемые в нем типы.


Следует помнить, что дженерик-протоколы отличаются высоким уровнем абстракции. Поэтому в самих приложениях они могут быть излишними. Но при этом дженерик-протоколы используются при программировании библиотек.

Protocol Inheritance

A protocol can inherit one or more other protocols and can add further requirements on top of the requirements it inherits.

The syntax for protocol inheritance is similar to the syntax for class inheritance, but with the option to list multiple inherited protocols, separated by commas


Множественное наследование и диспетчеризация методов

Как говорилось в начале статьи, протоколы можно множественно наследовать. То есть,


Это полезно, но какие подводные камни здесь скрыты? Всё дело в том, что сложности, по крайней мере на первый взгляд, возникают из-за диспетчеризации методов (method dispatch). Говоря простыми словами, может быть непонятно, какой метод будет вызываться – родительский или из текущего типа.

Чуть выше, мы уже раскрыли тему того, как работает код, он вызывает метод класса. То есть, как ожидается.


Но если попробовать убрать сигнатуру метода из определения протокола, то происходит «магия». Собственно говоря, это вопрос из собеседования: «как сделать, чтобы вызвалась функция из протокола?»


Но если использовать переменную не как протокол, а как класс, то всё будет нормально.


Все дело в статической диспетчеризации методов при расширении протокола. И это надо учитывать. Причем тут множественное наследование? А вот при чем: если взять два протокола с реализованными функциями, такой код не сработает. Чтобы функция была выполнена, потребуется явно делать каст к нужному протоколу. Такой вот отголосок множественного наследования из C++.


Схожая история будет, если наследовать один протокол другим, где есть функции, которые реализуются в расширениях. Компилятор не даст его собрать.


Два последних примера показывают, что полностью заменять протоколы классами не стоит. Можно запутаться в статической диспетчеризации.

На старт

Пока что мы определили несколько типов, соответствующих протоколу Bird. Сейчас же мы добавим нечто совсем другое:


У этого типа нет ничего общего с птицами и полётами. Мы хотим устроить гонку мотоциклистов с пингвинами. Пора выводить эту странную компашку на старт.

Определяем тип, соответствующий протоколу

Добавьте этот код внизу playground:

Этот код определяет новую структуру FlappyBird, которая соответствует и протоколу Bird и протоколу Flyable. Её свойство airspeedVelocity — произведение flappyFrequency and flappyAmplitude. Свойство canFly возвращает true.

Теперь добавьте определения еще двух структур:

Penguin это птица, но не может летать. Хорошо, что мы не пользуемся наследованием и не сделали всех птиц Flyable!

При использовании протоколов вы определяете компоненты функционала и делаете все подходящие объекты соответствующими протоколу

Затем мы определяем SwiftBird, но в нашей игре есть несколько разных её версий. Чем больше номер версии, тем больше её airspeedVelocity, которая определена как вычисляемое свойство.

Однако, здесь есть некоторая избыточность. Каждый тип Bird должен определить явно определить свойство canFly, хотя у нас есть определение протокола Flyable. Похоже, что нам нужен способ определить реализацию методов протокола по умолчанию. Что ж, для этого существуют расширения протоколов (protocol extensions).

Максимальная скорость

Напишем функцию для определения максимальной скорости гонщиков. Добавьте этот код в конце playground:


Здесь мы используем функцию max чтобы найти гонщика с максимальной скоростью и возвращаем её. Если массив пуст, то возвращается 0.0.

Class-Only Protocols

You can limit protocol adoption to class types by adding the AnyObject protocol to a protocol’s inheritance list.

In the above example, StudentDetails protocol can only be adopted by class types.

If a non -class type tries to conform to this protocol, the compiler throws an error.

Example

In the above program, a new protocol, StudentDetails , which inherits from TotalMark and Percentage protocols. Now class, structure, and enumeration adopts StudentDetails protocol must satisfy all of the requirements enforced by TotalMark and Percentage , plus the additional requirements enforced by StudentDetails .

When you use StudentDetails protocol in your class, Structure or enumeration you must be include calculatingTotalMark() , calculatingPercentage() and printingDetails() this functions.

Для чего нужны протоколы в Swift?

Мобильные разработчики часто обходятся вообще без использования протоколов, но при этом теряется возможность работать с некоторыми сущностями абстрактно. Если выделить основные особенности протоколов в Swift, у нас получится следующие 7 пунктов:

  • Протоколы обеспечивают множественное наследование
  • Протоколы не могут хранить состояние
  • Протоколы могут быть унаследованы другими протоколами
  • Протоколы могут применяться к структурам (struct), классам(class) и перечислениям (enum), определяя функционал типов
  • Дженерик протоколы (Generic-protocol) позволяют задавать сложные зависимости между типами и протоколами во время их наследования
  • Протоколы не определяют «сильные» и «слабые» ссылки на переменные
  • В расширениях к протоколам можно описывать конкретные реализации методов, и вычисляемых переменных (computed values)
  • Классовые протоколы разрешают себя наследовать только классам


При этом типы коллекций (сollection), а именно – array, set, dictionary – также можно упаковывать в протокол, потому что они тоже являются структурами. Например, словарь (Dictionary) определяется следующим образом


Обычно в объектно-ориентированном программировании применяется понятие классов, и всем хорошо знаком механизм наследования методов и переменных родительского класса классом-потомком. При этом никто не запрещает ему содержать дополнительные методы и переменные.

В случае с протоколами можно создать куда более интересную иерархию взаимосвязей. Для описания очередного класса можно использовать несколько протоколов одновременно, что позволяет создавать достаточно сложные конструкции, которые будут удовлетворять множеству условий одновременно. С другой стороны, ограничения разных протоколов позволяют сформировать некоторый объект, предназначенный только для узкого применения и содержащий определенный ряд функций.

Реализация протокола в Swift достаточно проста. Синтаксис подразумевает название, ряд методов и параметры (переменные), которые он будет содержать.


Кроме этого в Swift протоколы могут содержать не только наименования методов, но и их реализацию. Код метода в протоколе добавляются через расширения (extension). В документации можно найти много упоминаний расширений, но применительно к протоколам в расширениях можно размещать имя функции и тело функции.


То же самое можно делать с переменными.


Если мы где-то будем применять объект, связанный с протоколом, можно задать в нем переменную с фиксированным или передаваемым значением. Фактически переменная представляет собой небольшую функцию без входных параметров…или с возможностью прямого назначения параметра.

Расширение протокола в Swift позволяет реализовать тело переменной, и тогда она по факту будет представлять собой computed value – вычисляемый параметр с функциями get и set. То есть такая переменная не будет хранить никаких значений, а будет или играть роль функции или функций, или будет играть роль прокси для какой-то другой переменной.

Или если мы берем какой-то класс или структуру и реализуем протокол, то в нем можно использовать обычную переменную:


Стоит отметить, что переменные в определении протокола не могут быть weak. (weak – это вариант реализации переменой).

Есть и более интересные примеры: можно реализовать расширение массива и добавить туда функцию, связанную с типом данных массива. Например, если массив содержит целочисленные значения или имеет формат equitable (пригодные для сравнения), функция может, например, сравнивать все значения ячеек массива.


Небольшое замечание. Переменные и функции в протоколах могут быть статическими (static).

Дженерики и протоколы

Можно сказать, что это — вопрос со звёздочкой, который вовсе и не надо спрашивать. Но кодеры любят сверхабстрактные конструкции, и конечно, парочка ненужных generic class обязательно должны быть в проекте (куда ж без этого). Но программист не был бы программистом, если бы не захотел всё это обернуть это в ещё одну абстракцию. И Swift, являясь хоть и молодым, но динамично развивающимся языком, даёт такую возможность, но в ограниченном варианте. (да, это уже не про мобилки).


Для Swift 4.1 выводится следующее:


Тогда как в Swift 4.2 всё работает, как ожидается:


Также стоит заметить, что можно наследовать протокол только при одном типе связей. Если есть два типа связей, то наследование будет запрещено на уровне компилятора. Подробное объяснение того, что можно, а что нельзя, показано тут.


Но, не смотря на эти сложности, работа со связями в дженериках достаточно удобная.

Делаем функцию более «свифтовой»

По секрету: можно сделать ещё лучше. Добавим это в самом низу:


А вот теперь мы расширили сам протокол Sequence функцией topSpeed(). Она применима только в случае, когда Sequence содержит тип Racer.

Protocol Composition

It can be useful to require a type to conform to multiple protocols at the same time. You can combine multiple protocols into a single requirement with a protocol composition.

Protocol compositions behave as if you defined a temporary local protocol that has the combined requirements of all protocols in the composition. Protocol compositions don’t define any new protocol types.

You can list as many protocols as you need, separating them with ampersands ( & ).

Переопределяем поведение по умолчанию

Наш тип UnladenSwallow, соответствуя протоколу Bird, автоматически получил реализацию для canFly. Нам, однако, нужно, чтобы UnladenSwallow.unknown возвращала false для canFly.

Добавьте внизу следующий код:


Теперь только .african и .european возвратят true для canFly. Проверьте! Добавьте следующий код внизу нашего playground:


Скомпилируйте playground и проверьте полученные значения с указанными в комментариях выше.

Таким образом мы переопределяем свойства и методы почти так же, как используя виртуальные методы в объектно-ориентированном программировании.

Example

Вносим изменения в объект

До сих пор каждый пример демонстрировал, как добавить функционал. Но что, если мы хотим сделать протокол, который что-то изменяет в объекте? Это можно сделать при помощи mutating методов в нашем протоколе.

Добавьте новый протокол:


Здесь мы определяем протокол, который дает нам возможность жульничать (cheat). Каким образом? Произвольно изменяя содержимое boost.

Теперь создадим расширение SwiftBird, которое соответствует протоколу Cheat:


Здесь мы реализуем функцию boost(_:), увеличивая speedFactor на передаваемую величину. Ключевое слово mutating даёт структуре понять, что одно из её значений будет изменено этой функцией.

Protocol Extension

Protocols can be extended to provide method, initializer, subscript, and computed property implementations to conforming types.

Protocols let you describe what methods something should have, but don’t provide the code inside. Extensions let you provide the code inside your methods, but only affect one data type — you can’t add the method to lots of types at the same time.

Наследование типами

Создавая класс, структуру или перечисление, мы можем обозначить наследование определенного протокола – в этом случае унаследованные параметры будут работать и для нашего класса, и для всех остальных классов, которые наследуют этот класс, даже если мы не имеем к ним доступа.

Кстати, в этом контексте появляется одна очень интересная задачка. Допустим у нас есть протокол. Есть некоторый класс. И класс реализует протокол, а в нем есть функция work(). Что будет, если у нас есть extension протокола, в котором также есть метод work(). Какой из них будет вызван при обращении к методу?


Запущен будет метод класса – таковы уж особенности диспетчеризации методов в Swift. И этот ответ дают многие соискатели. Но вот на вопрос, как сделать так, чтобы в коде был выполнен не метод класса, а метод протокола, знают ответ лишь немногие. Однако и для этой задачи есть решение – оно подразумевает удалени функции из определения протокола и вызов метода следующим образом:

Collection extension

In swift, array and sets both conform to a protocol Collection() . so we can write an extension to that protocol to add a method called summarize() to print an array of elements.

Protocol used in class syntax


If a class has a superclass, list the superclass name before any protocols it adopts, followed by a comma

Использование @ objc

Главное, что нужно знать в данном вопросе — это то, что @ objc протоколы Swift видны в Objective-C коде. Строго говоря, для этого «волшебное слово» @ objc и существует. Но всё остальное остаётся без изменений

Протоколы такого типа могут быть наследоваными только классами. Для перечислений и структур это сделать нельзя.

То есть только так.


Стоит отметить, что в этом случае появляется возможность определять опциональные функции (@obj optional func), которые при желании можно не реализовывать, как для функции test() в предыдущем примере. Но условно опциональные функции можно реализовать и при помощи расширения протокола с пустой иплементацией.

Читайте также: