Как стать автором
Обновить

Swift. Протоколы

Время на прочтение8 мин
Количество просмотров14K
Предисловие

Материал является примером, который демонстрирует возможности языка и не претендует на звание «хороший код».

Материал предназначен начинающим разработчикам для общего ознакомления.

Весь свой информационный мусор, я коллекционирую на своей стене в ВК, так что добро пожаловать.

Немного от себя

Вот уж не знаю почему, но многих протоколы пугают, их боятся использовать, понимать и в целом избегают как огня, что является в корне неверным выбором. Иными словами, если Вам не нравятся протоколы, Вы просто не умеете их готовить.

Если разобраться поглубже, язык Swift, является крайне продуманным и собрал в себе все лучшее из многообразия языков. Он быстрый, продуманный и очень оптимизированный. Каждый его инструмент, всегда преследует определенную цель и не всегда понятно с первого раза, какую же такую цель преследует изучаемый инструмент, но поверьте, цель есть всегда. Так и у протоколов есть масса возможностей и перекочевали они с Objective-C не просто так. Начнем мы с простого и плавно перейдем к более сложному (хотя протоколы весьма просты в усвоении и очень даже функциональны).

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

Что такое протокол

Давайте начнем с постановки простой, фейковой задачи. Нам необходимо разработать функцию, которая получает некий класс/структуру, берет свойство count и возвращает нам квадрат этого значения, пускай в Int. Эту функцию мы будем передавать другим разработчикам, чтобы они могли её использовать.

Казалось бы, в чем проблема? Но давайте подумаем... Какой тип данных указать во входных параметрах функции? Any? Не очень удачное решение т.к. никто не знает какой тип данных будет использован в ней и приведение типов выполнить не получится.

Можно конечно решить ситуацию посредством дженериков, но это тема для другой публикации.

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

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

protocol MyFirstProtocol {
    var count: Int { get }
}

func multipliesNumberBySelf(_ type: MyFirstProtocol) -> Int {
    type.count * type.count
}

Внезапно и удивительно, но оказывается, что протокол является типом данных. Мы можем его создать, описать и использовать. В момент создания протокола, Вы просто описываете содержимое потенциального типа, который будет его использовать (будет следовать протоколу, будет подписан на протокол и т.д.), а потом в момент создания типа, указываете протокол, чтобы сказать компилятору «привет, я класс и я следую протоколу», если тип не соответствует протоколу, Xcode выдаст подсказку и в большинстве случаев, даже предложит исправить несоответствие. В коде выше, как Вы уже вероятно догадались, мы создали простой протокол, который декларирует простое свойство count и его тип данных, не более (конструкция { get }, говорит о том, что свойство должно быть доступно для чтения) А потом, с его помощью(протокола), создали функцию из первоначальной задачи. Давайте теперь создадим класс, который соответствует протоколу.

class MegaClass: MyFirstProtocol {
    let count: Int = 5
}

синтаксис такой же как при наследовании классов

Так как у нас были указания исключительно о необходимости наличия свойства count для чтения, нам никто не запрещает использовать константу и класс выше соответствует декларации. Теперь создадим структуру под наш протокол.

struct MegaStruct : MyFirstProtocol {
    var count: Int = 8
    var name: String = "Dart Vader"
}

У нее есть, еще свойство, помимо count, но это не является нарушением протокола т.к. протокол говорить только о том, что должно быть, а не то, чего быть не должно. Давайте попробуем это с нашей функцией.

Все работает. Давайте добавим классу MegaClass, свойство property для наглядности. Теперь обратите внимание на важный момент. Если мы будем обращаться к типу, который соответствует протоколу внутри функции, даже при наличии иных свойств/методов, нам будет доступно только описанное в протоколе. Грубо говоря, протокол определяет, что конкретно есть у типа и дает возможность с этим взаимодействовать.

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

В общем протокол простыми словами это - тип данных, декларация описывающая тип и средство обобщения различных типов т.к. его можно использовать с class, struct, enum. Теперь я думаю понятно, когда вы будете передавать свой код другому разработчику, он всегда сможет привести свои типы к соответствию с необходимым протоколом без каких-либо сложностей.

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

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

//Второй протокол
protocol MySecondProtocol {
    var number: Int { get }
}

//Протокол, который наследует другие протоколы
protocol ChildrenProtocol: MySecondProtocol, MyFirstProtocol {}

//Подписываемся на соблюдение нескольких протоколов
class classForMultipleInheritance: MySecondProtocol, MyFirstProtocol {
    var number: Int = 2
    var count: Int = 2
}

//Подписываемся на протокол, который наследует другие протоколы
class classForInheritingMultipleProtocol : ChildrenProtocol {
    var number: Int = 2
    var count: Int = 2
}

Если класс имеет суперкласс в виде класса, который соответствует какому-либо протоколу, то естественно он так же будет ему соответствовать.

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

протоколы Папа, Мама и общий протокол Ребенок.

Некий класс Класс, который соответствует протоколу Ребенок у которого есть подкласс Подкласс.

Некая функция/массив/что-либо еще, умеющие работать с типами, которые соответствуют протоколу Мама

Мы можем передать в функцию/массив и т.д. которые требуют соответствие протоколу Мама как Класс так и Подкласс т.к. они оба соответствую ему (протоколу Мама) через наследование. Эта логичная концепция конечно бывает сложна для понимания, но на самом деле очень проста. Необходимо просто переварить. А еще лучше просто побаловаться в коде. Ничего не заменит практику.

Если классу необходимо наследовать какой-либо класс и соответствовать протоколу/протоколам, то первым указывается суперкласс, а далее идут протоколы.

class ParentClass {}

class ChildClass: ParentClass, MyFirstProtocol, MySecondProtocol {
    var count: Int = 3
    var number: Int = 3
}

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

protocol InitProtocol {
    var property: String { get }
    init(_ property: String)
}

class ExampleClass : InitProtocol {
    var property: String
    
    required init(_ property: String) {
        self.property = "свойство"
    }
}

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

protocol MethodProtocol {
    func method(_ value: String) -> Int
}

class ExampleClass1 : MethodProtocol {
    func method(_ value: String) -> Int {
        5
    }
}
class ExampleClass2 : MethodProtocol {
    func method(_ value: String) -> Int {
        if value.count > 5 {
            return 10
        } else {
            return 5
        }
    }
}
class ExampleClass3 : MethodProtocol {
    func method(_ value: String) -> Int {
        self.secondMethod()
    }
    func secondMethod() -> Int {
        10
    }
}

обратите внимание - для удобства восприятия, я помечаю типы цифрами, никогда не делайте так в коде

мы определяем тип метода, а что в нем происходит, протокол не интересует

Если метод должен изменять значения, то его следует пометить как mutating, это не обязательное требование, но его игнорирование заблокирует нормальное использование такого протокола со структурами.

protocol MethodProtocol {
    mutating func setProperty(_ value: Int)
}

class ExampleClass : MethodProtocol {
    var property: Int = 0
    func setProperty(_ value: Int){
        self.property = value
    }
}

struct ExampleStruct : MethodProtocol {
    var property: Int = 0
    mutating func setProperty(_ value: Int){
        self.property = value
    }
}

Хотя протоколы можно использовать с различными типами, мы можем ограничить протокол только для использования с классами

protocol MyProtocol: AnyObject { }

ранее использовалось для декларирования слабых ссылок (в Swift 5 вызовет ошибку)

protocol MyProtocol: AnyObject {
    weak var property: MyClass? { get set }
}

обратите внимание констукция { get set } говорит о том, что свойство должно уметь менять значение т.е. константой его объявлять нельзя, только переменной. И как обсуждалось ранее, get разрешает использовать константы. Использовать set без get нельзя.

сейчас практическое применение может понадобится, например для использования опциональных методов. Это невозможно в Swift реализации, но было возможно в Objective-c, а так как Swift обратно совместим с ним, мы все же можем использовать эту возможность.

import Foundation

@objc protocol MyProtocol {
    @objc optional func method()
}

не забудте импортировать Foundation для доступа к @objc

но в таком случае у нас нет жесткой необходимости использовать AnyObject т.к. подобный протокол и так не будет функционировать со структурами.

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

import Foundation

@objc protocol MyProtocol {
    @objc optional func method()
    @objc optional func secondMethod()
}

class MyClass: MyProtocol {
    func method(){
        //
    }
}

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

Ну вот мы и добрались до истинной мощи протоколов. Казалось бы, ну декларация, ну удобная, но мне и так нормально, сами используйте свои протоколы, мне и так хорошо. Работаю я один, свой код знаю, все пучком. Нет! Это ошибочное суждение!Уверен сейчас, ваше мировоззрение изменится раз и навсегда, а первая мысль будет «черт, почему я не использовал это раньше...», начнем.

protocol Example1 {
    var property1: Int { get }
}
protocol Example2 {
    var property2: Int { get }
}
protocol SuperProtocol: Example1, Example2 {}

struct StructExample: SuperProtocol {
    var property1: Int = 5
    var property2: Int = 5
}

class ClassExample: SuperProtocol {
    var property1: Int = 5
    var property2: Int = 5
}

var myClass = ClassExample()
var myStruct = StructExample()

Сейчас вам должно уже быть все понятно. Есть два протокола, который декларируют по свойству. Протокол который наследуется от них и два типа, класс со структурой. А теперь добавим вот такой код

extension Example1 {
    func method() {
        print("мое свойство property1 - \(self.property1)")
    }
}

extension Example2 {
    mutating func setProperty(_ value: Int) {
        self.property2 = value
    }
}

extension SuperProtocol {
    var sumProperty: Int { self.property1 + self.property2 }
}

Однако здравствуйте, я думаю вы уже поняли, все эти возможности появляются у наших типов, которые соответствуют протоколу в доступе.

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

protocol SuperProtocol: Example1, Example2 {
    init()
}

добавим инициализатор в протокол, это заставит нас изменить наш класс (структуры не касается т.к. структуры не наследуются)

class ClassExample: SuperProtocol {
    var property1: Int
    var property2: Int
    
    required init() {
        self.property1 = 5
        self.property2 = 5
    }
}

и расширим наш SuperProtocol дополнительным инициализатором

extension SuperProtocol {
    var sumProperty: Int { self.property1 + self.property2 }
    
    init(property2: Int){
        self.init() //необходимо
        self.property2 = property2
    }
}

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

Нюансы и особенности работы с протоколами

  1. Диспетчеризация методов, посмотрите на код

protocol ProtocolExample {}

class ClassExample: ProtocolExample {
    func method() {
        print("метод класса")
    }
}

extension ProtocolExample {
    func method() {
        print("метод протокола")
    }
}

как думаете, какой метод будет вызван?

method dispatch работает таким образом, что отдает приоритет типу, а константа myClass относится к типу ClassExample т.к. неявное присваивание типа, определяет её как класс, а не протокол. Но если мы явно укажем тип ProtocolExample, то будет вызван метод протокола.

let myClass: ProtocolExample = ClassExample()

Соответственно во всех функциях/методах/массивах и т.д., во всем, что работает с типом протокола, приоритет будет за методом протокола.

2. Можно перечислять несколько протоколов для взаимодействия

3. В протоколах нельзя определять доступы (private и т.д.), но можно сделать вот так

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

Итоги по протоколам:

  • Используются с class, struct, enum

  • Не хранят состояние

  • Могут быть унаследованы другими протоколами

  • Поддерживают множественное наследование

  • В расширениях можно описывать логику

  • Не указывают слабые ссылки

  • Не контролируют доступ без расширений

See you later...

Теги:
Хабы:
+1
Комментарии8

Публикации

Изменить настройки темы

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн