В процессе изучения Swift мы уже неоднократно встречались с протоколами, но каждый раз касались их поверхностно, без подробного изучения механизмов взаимодействия с ними.
Протоколы содержат перечень свойств, методов и сабскриптов, которые должны быть реализованы в объектном типе. Другими словами, они содержат требования к наличию определенных элементов внутри типа данных. Протокол сам непосредственно не реализует какой-либо функционал, он лишь является своеобразным набором правил и требований к типу. Любой объектный тип данных может принимать протокол. Наиболее важной функцией протокола является обеспечение целостности объектных типов путем указания требований к их реализации.
Протоколы объявляются независимо от каких-либо элементов программы, так же как и объектные типы данных.
Синтаксис
protocol ИмяПротокола {
// тело протокола
}
Для объявления протокола используется ключевое слово protocol, после которого указывается имя создаваемого протокола.
Для того чтобы принять протокол к исполнению каким-либо объектным типом, необходимо написать его имя через двоеточие сразу после имени реализуемого типа:
struct ИмяПринимающейСтруктуры: ИмяПротокола{
// тело структуры
}
После указания имени протокола при объявлении объектного типа данный тип обязан выполнить все требования протокола. Вы можете указать произвольное количество принимаемых протоколов.
Если класс не только принимает протоколы, но и наследует некоторый класс, то имя суперкласса необходимо указать первым, а за ним через запятую — список протоколов:
class ИмяПринимающегоКласса: ИмяСуперКласса, Протокол1, Протокол2{
// тело класса
}
Протокол может потребовать соответствующий ему тип реализовать свойство экземпляра или свойство типа (static), имеющее конкретные имя и тип данных. При этом протокол не указывает на вид свойства (хранимое или вычисляемое). Также могут быть указаны требования к доступности и изменяемости параметра.
Если у свойства присутствует требование доступности и изменяемости, то в качестве данного свойства не могут выступать константа или вычисляемое свойство «только для чтения». Требование доступности обозначается с помощью конструкции {get}, а требование доступности и изменяемости — с помощью конструкции {get set}.
Создадим протокол, содержащий ряд требований к принимающему его типу (листинг 33.1).
Листинг 33.1
1 protocol SomeProtocol {
2 var mustBeSettable: Int { get set }
3 var doesNotNeedToBeSettable: Int { get }
4 }
Протокол SomeProtocol требует, чтобы в принимающем типе были реализованы два изменяемых (var) свойства типа Int. При этом свойство mustBeSettable должно быть и доступным для чтения, и изменяемым, а свойство doesNotNeedToBeSettable — как минимум доступным для чтения.
Протокол определяет минимальные требования к типу, то есть тип данных обязан реализовать все, что описано в протоколе, но он может не ограничиваться этим набором элементов. Так, для свойства doesNotNeedToBeSettable из предыдущего листинга может быть реализован не только геттер, но и сеттер.
Для указания в протоколе на свойство типа необходимо использовать модификатор static перед ключевым словом var. Данное требование выполняется даже в том случае, если протокол принимается структурой или перечислением (листинг 33.2).
Листинг 33.2
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
В данном примере свойство типа someTypeProperty должно быть обязательно реализовано в принимающем типе данных.
В следующем примере мы создадим протокол и принимающий его требования класс (листинг 33.3).
Листинг 33.3
protocol FullyNamed {
var fullName: String { get }
}
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Wick")
В данном примере определяется протокол FullyNamed, который обязывает структуру Person иметь доступное свойство fullName типа String.
Протокол может требовать реализации определенного метода экземпляра или метода типа. Форма записи для этого подобна указанию требований к реализации свойств.
Для требования реализации метода типа необходимо использовать модификатор static. Также протокол может описывать изменяющий метод. Для этого служит модификатор mutating.
ПРИМЕЧАНИЕ Если вы указали ключевое слово mutating перед требованием метода, то вам уже не нужно указывать его при реализации метода в классе. Данное ключевое слово требуется только в реализации структур.
При реализации метода в типе данных необходимо в точности соблюдать все требования протокола, в частности имя метода, наличие или отсутствие входных аргументов, тип возвращаемого значения, модификаторы (листинг 33.4).
Листинг 33.4
protocol RandomNumberGenerator {
func random() -> Double
static func getRandomString()
mutating func changeValue(newValue: Double)
}
В данном примере реализован протокол RandomNumberGenerator, который содержит требования реализации трех методов. Метод экземпляра random() должен возвращать значение типа Double. Метод getRandomString должен быть методом типа, при этом требования к возвращаемому им значению не указаны. Метод changeValue(_:) должен быть изменяющим и должен принимать в качестве входного параметра значение типа Double.
Данный протокол не делает никаких предположений относительно того, как будет вычисляться случайное число, ему важен лишь факт выполнения требований.
Дополнительно протокол может предъявлять требования к реализации инициализаторов. Необходимо писать инициализаторы точно так же, как вы пишете их в объектном типе, опуская фигурные скобки и тело инициализатора.
Требования к инициализаторам могут быть выполнены в соответствующем классе в форме назначенного или вспомогательного инициализатора. В любом случае перед объявлением инициализатора в протоколе необходимо указывать модификатор required. Это гарантирует, что вы реализуете указанный инициализатор во всех подклассах данного класса.
ПРИМЕЧАНИЕ Нет нужды обозначать реализацию инициализаторов протокола модификатором required в классах, которые имеют модификатор final.
Реализуем протокол, содержащий требования к реализации инициализатора, и класс, выполняющий требования протокола (листинг 33.5).
Листинг 33.5
protocol Named{
init(name: String)
}
class Person : Named {
var name: String
required init(name: String){
self.name = name
}
}
Протокол сам по себе не несет какой-либо функциональной нагрузки, он лишь содержит требования к реализации объектных типов. Тем не менее протокол является полноправным типом данных.
Используя протокол в качестве типа данных, вы указываете на то, что записываемое в данное хранилище значение должно иметь тип данных, который соответствует указанному протоколу.
Так как протокол является типом данных, вы можете организовать проверку соответствия протоколу с помощью оператора is, который мы обсуждали при изучении темы приведения типов. При проверке соответствия возвращается значение true, если проверяемый экземпляр соответствует протоколу, и false в противном случае.
Расширения могут взаимодействовать не только с объектными типами, но и с протоколами.
Вы можете использовать расширения для добавления требований по соответствию некоторого объектного типа протоколу. Для этого в расширении после имени типа данных через двоеточие необходимо указать список новых протоколов (листинг 33.6).
Листинг 33.6
protocol TextRepresentable {
func asText() -> String
}
extension Int: TextRepresentable {
func asText() -> String {
return String(self)
}
}
5.asText() // "5"
В данном примере протокол TextRepresentable требует, чтобы в принимающем объектном типе был реализован метод asText(). С помощью расширения мы добавляем требование о соответствии типа Int к данному протоколу, при этом, поскольку где-то ранее был реализован сам тип данных, в обязательном порядке указывается реализация данного метода.
С помощью расширений мы можем не только указывать на необходимость соответствия новым протоколам, но и расширять сами протоколы, поскольку протоколы являются полноценными типами данных.
При объявлении расширения необходимо использовать имя протокола, а в его теле указывать набор требований с их реализациями. После расширения протокола описанные в нем реализации становятся доступны в экземплярах всех классов, которые приняли данный протокол к исполнению.
Напишем расширение для реализованного ранее протокола TextRepresentable (листинг 33.7).
Листинг 33.7
extension TextRepresentable {
func description() -> String {
return "Данный тип поддерживает протокол TextRepresentable"
}
}
5.description()
Расширение добавляет новый метод в протокол TextRepresentable. При этом ранее мы указали, что тип Int соответствует данному протоколу. В связи с этим появляется возможность обратиться к указанному методу для любого значения типа Int.
Протокол может наследовать один или более других протоколов. При этом он может добавлять новые требования поверх наследуемых, — тогда тип, принявший протокол к реализации, будет вынужден выполнить требования всех протоколов в структуре. При наследовании протоколов используется тот же синтаксис, что и при наследовании классов.
Работа с наследуемыми протоколами продемонстрирована в листинге 33.8.
Листинг 33.8
protocol SuperProtocol {
var someValue: Int { get }
}
protocol SubProtocol: SuperProtocol {
func someMethod()
}
struct SomeStruct: SubProtocol{
let someValue: Int = 10
func someMethod() {
// тело метода
}
}
Протокол SuperProtocol имеет требования к реализации свойства, при этом он наследуется протоколом SubProtocol, который имеет требования к реализации метода. Структура принимает к исполнению требования протокола SubProtocol, а значит, в ней должны быть реализованы и свойство, и метод.
Вы можете ограничить протокол таким образом, чтобы его могли принимать к исполнению исключительно классы (а не структуры и перечисления). Для этого у протокола в списке наследуемых протоколов необходимо указать ключевое слово class. Данное слово всегда должно указываться на первом месте в списке наследования.
Пример создания протокола приведен в листинге 33.9. В нем мы изменим протокол SubProtocol таким образом, чтобы его мог принять исключительно класс.
Листинг 33.9
protocol SubProtocol: class, SuperProtocol {
func someMethod()
}
Иногда бывает удобно требовать, чтобы тип соответствовал не одному, а нескольким протоколам. В этом случае, конечно же, можно создать новый протокол, наследовать в него несколько необходимых протоколов и задействовать имя только что созданного протокола. Однако для решения данной задачи лучше воспользоваться композицией протоколов, то есть скомбинировать несколько протоколов.
Синтаксис
Протокол1 & Протокол2 ...
Для композиции протоколов необходимо указать имена входящих в данную композицию протоколов, разделив их оператором & (амперсанд).
В листинге 33.10 приведен пример, в котором два протокола комбинируются в единственное требование.
Листинг 33.10
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(celebrator: Named & Aged) {
print("С Днем рождения, \(celebrator.name)! Тебе уже
\(celebrator.age)!")
}
let birthdayPerson = Person(name: "Джон Уик", age: 46)
wishHappyBirthday(celebrator: birthdayPerson)
// выводит "С Днем рождения, Джон Уик! Тебе уже 46!"
В данном примере объявляются два протокола: Named и Aged. Созданная структура принимает оба протокола и в полной мере выполняет их требования.
Входным аргументом функции wishHappyBirthday(celebrator:) должно быть значение, которое удовлетворяет обоим протоколам. Таким значением является экземпляр структуры Person, который мы и передаем.