Основы Swift / 17.1. Протоколы - Основы


Видео


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

В дополнение к определению требований, которые соответствующие требования должны реализовать, Вы можете расширить протокол для реализации части этих требований или для реализации дополненной функциональности, которой могут пользовать типы, им удовлетворяющие.
Синтаксис протоколов
Вы можете определить протоколы в виде, схожем с классами, структурами и перечислениями:
@{17.1\1}
protocol SomeProtocol {
    // здесь будет реализация протокола
}
@{17.1\2}
Пользовательские типы утверждают, что они адаптировали конкретный протокол с помощью размещения имени протокола после имени типа, разделённого двоеточием, в качестве их определения. Многие протоколы могут быть перечислены с отделением запятыми:
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // здесь будет определение структуры
}
@{17.1\3}
Если класс имеет надкласс, то перечислите имя надкласса перед любыми адаптируемые протоколами, разделив их запятыми:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // здесь идёт определение класса
}
Требования свойств
Протокол может потребовать у любого соответствующего типа предоставить свойство объекта или типа с конкретным именем и типом. Протокол не указывает, должно ли это свойство быть хранимым или вычисляемым: он только указывает имя и тип требуемого свойства. Протокол также указывает, должно ли быть каждое свойство получаемым или получаемым и устанавливаемым.

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

Требования свойств всегда задаются в качестве переменных свойств, предваряемых ключевым словом var. Получаемые и задаваемые свойства указываются с помощью { get set } после объявления их типа, а получаемые свойства указываются написанием {get}.
@{17.1\4}
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}
@{17.1\5}
Всегда предваряйте свойство типа в требованиях ключевым словом static, когда определяйте его в протоколе. Это правило работает даже для требований типов, которые могут быть предварены ключевыми словами class или static, когда они реализуются классом:
protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}
@{17.1\6\1}
Здесь приведён пример протокола с единственным требованием свойства объекта:
protocol FullyNamed {
    var fullName: String { get }
}
Протокол FullyNamed требует у соответствующего типа предоставляет полностью-квалифированное имя. Протокол не определяет ничего больше о природе подходящего типа: он только указывает, что тип должен предоставлять полное имя для себя. Протокол постановляет, что любой тип FullyNamed должен иметь задаваемое свойство объекта с названием fullName, которое имеет тип String.
@{17.1\6\2}
Здесь представлен пример простой структуры, которая адаптирует и соответствует протоколу FullyNamed:
struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName равен "John Appleseed"
Этот пример определяет структуру с названием Person, которая представляет конкретного человека с именем. Она утверждает, что адаптирует протокол FullyNamed в качестве первой сроке определения.

Объект Person имеет единственное хранимое свойство с названием fullName, чей тип - String. Это совпадает с единственным требованием FullyNamed-протокола и означает, что Person корректно соответствует протоколу. (Swift выдаст ошибку времени компиляции, если требование протокола не удовлетворено.)
@{17.1\6\3}
Здесь представлен более сложный класс, который также адаптирует и соответствует протоколу FullyNamed:
class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName равен "USS Enterprise"
Этот класс реализует требуемое свойство fullName в качестве вычисляемого свойства только-для-чтения для звездолёта. Каждый объект класса Starship хранит обязательное name и опциональное prefix. Свойство fullName использует значение prefix, если оно существует и дополняет его в начало name для создания полного имени звездолёта.
Требования методов
Протоколы могут потребовать реализации соответствующим типом реализации определённых методов объектов и типа. Эти методы записываются в качестве части определения протокола в том же самом виде, что обычные методы типы или объекта, но без фигурных скобок и тела метода. Вариадик-параметры разрешены с теми же правилами, что и в обычных методах. Значения по-умолчанию однако не могут определены для параметров методов в определении протокола.

Как и в случае с требованиями свойства типа, Вы всегда предваряете требования метода типа с помощью ключевого слова static, когда они определяются в протоколе. Это верно, даже если требования метода типа префиксируются ключевыми словами class или static, когда они реализуются классом:
@{17.1\7}
protocol SomeProtocol {
    static func someTypeMethod()
}
@{17.1\8\1}
Следующий пример определяет протокол с единственным требованием на метод объекта:
protocol RandomNumberGenerator {
    func random() -> Double
}
Протокол RandomNumberGenerator требует от любого подходящего типа иметь метод объекта с названием random, который возвращает значение Double при всяком своём вызове. И хотя это не определено в протоколе, но принимается, что это значение будет числом от 0.0 до (невключительно) 1.0.

Протокол RandomNumberGenerator не делает никаких предположений о том, как каждое случайное число генерируется, он просто требует от генератора предоставить стандартный способ для генерации нового случайного числа.
@{17.1\8\2}
Здесь представлена реализация класса, что реализует протокол RandomNumberGenerator. Этот класс реализует псевдослучайный генератор чисел с алгоритмом, известным как линейный конгруэнтный генератор:
class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Выведет "Here's a random number: 0.37464991998171"
print("And another one: \(generator.random())")
// Выведет "And another one: 0.729023776863283"
Требования мутирующих методов
Иногда необходимо для метода изменять (или мутировать) экземпляр, которому он принадлежит. Для методов объектов на типах-значениях (что значит, на структурах и перечислениях) Вы размещаете ключевое слово mutating перед ключевым словом метода func для указания на то, что ему разрешено изменять объект, которому он принадлежит, и любые его свойства.

Если Вы определите требование метода объекта в протоколе, в котором подразумевается, что он может изменять любой тип, который адаптирует протокол, то пометьте этот метод ключевым словом mutating в качестве части определения протокола. Это позволит структурам и перечислениям адаптировать протокол и удовлетворять этому требованию метода.

Если Вы пометите требование протокола на метод объекта в качестве mutating, то Вам не нужно писать ключевое слово mutating при записи реализации этого метода для класса. Ключевое слово mutating используется только структурами и перечислениями.
@{17.1\9\1}
Пример ниже определяет протокол с названием Togglable, который определяет единственный метод объекта с названием toogle(). Как ясно из его имени, метод toogle() должен переключать или инвертировать состояние любого удовлетворяющего типа, обычно изменяя свойство этого типа.

Метод toogle() помечен ключевым словом mutatuing как часть определения протокола Togglable для указания на то, что метод должен скорее всего изменять состояние подходящего объекта, когда он вызывается:
protocol Togglable {
    mutating func toggle()
}
Если Вы реализуете протокол Togglable для структуры или перечисления, то эти структура или перечисление смогут удовлетворять протоколу, предоставляя реализацию метода toogle(), который так же помечен как mutating.
@{17.1\9\2}
Пример ниже определяет перечисление с названием OnOffSwitch. Это перечисление переключает между двумя состояниями, соответствующими кейсам перечисления on и off. Реализация перечисления toogle помечена как mutating для соответствия требованиям протокола Togglable:
enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch теперь равен .on
Требования конструкторов
Протоколы могут требовать реализации специфичных инициализаторов соответствующим типом. Вы можете написать эти инициализаторы как часть определения протокола в том же виде, что и обычные инициализаторы, но без фигурных скобок и тела инициализатора:
@[17.1\10\1}
protocol SomeProtocol {
    init(someParameter: Int)
}
Реализация требований инициализаторов классом
Вы можете реализовать требование протокола на конструктор на соответствующем классе либо основным, либо вспомогательным инициализатором. В обоих случаях Вы доходны пометить реализацию модификатором required:
@{17.1\10\2}
class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // реализация конструктора идёт здесь
    }
}
Использование модификатора required гарантирует, что Вы предоставите явную или унаследованную реализацию требуемого конструктора для всех подклассов соответствующего класса, так чтобы они тоже удовлетворяли протоколу.

Вам не нужно маркировать реализации конструкторов протокола с модификатором required для классов с модификатором final, так как финальные классы не могут быть унаследованы.
@{17.1\11}
Если подкласс перезаписывает основной инициализатор надкласса и также реализует совпадающий с требований инициализатора из протокола, то пометьте его реализацию обоими модификаторами required и override:
protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // реализация конструктора идёт здесь
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // реализация конструктора идёт здесь
    }
}
Требования на проваливаемые конструкторы
Требование на проваливаемый инициализатор может быть удовлетворено проваливаемым или непроваливаемым инициализатором у адаптирующего типа. Требование на непроваливаемый инициализатор может быть удовлетворено непроваливаемым инициализатором или неявно распакуемым/ проваливаемым инициализатором.