Вступление
С выпуском Swift 2 Apple добавила ряд новых функций и возможностей в язык программирования Swift. Однако одним из самых важных был пересмотр протоколов. Улучшенная функциональность, доступная в протоколах Swift, позволяет создавать новый тип программирования — протоколно-ориентированное программирование. Это в отличие от более распространенного объектно-ориентированного стиля программирования, к которому многие из нас привыкли.
В этом уроке я собираюсь показать вам основы протоколно-ориентированного программирования в Swift и его отличие от объектно-ориентированного программирования.
Предпосылки
Это руководство требует, чтобы вы работали с Xcode 7 или выше, что включает поддержку версии 2 языка программирования Swift.
1. Основы протокола
Если вы еще не знакомы с протоколами, они являются способом расширения функциональности существующего класса или структуры. Протокол может рассматриваться как проект или интерфейс, который определяет набор свойств и методов. Класс или структура, соответствующие протоколу, необходимы для заполнения этих свойств и методов значениями и реализациями соответственно.
Следует также отметить, что любое из этих свойств и методов может быть обозначено как необязательное, что означает, что для их реализации не требуются соответствующие типы. Определение протокола и соответствие класса в Swift может выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
|
protocol Welcome {
var welcomeMessage: String { get set }
optional func welcome()
}
class Welcomer: Welcome {
var welcomeMessage = «Hello World!»
func welcome() {
print(welcomeMessage)
}
}
|
2. Пример
Для начала откройте Xcode и создайте новую игровую площадку для iOS или OS X. Как только Xcode создаст игровую площадку, замените ее содержимое следующим:
01
02
03
04
05
06
07
08
09
10
11
|
protocol Drivable {
var topSpeed: Int { get }
}
protocol Reversible {
var reverseSpeed: Int { get }
}
protocol Transport {
var seatCount: Int { get }
}
|
Мы определяем три протокола, каждый из которых содержит свойство. Далее мы создаем структуру, которая соответствует этим трем протоколам. Добавьте следующий код на игровую площадку:
1
2
3
4
5
|
struct Car: Drivable, Reversible, Transport {
var topSpeed = 150
var reverseSpeed = 20
var seatCount = 5
}
|
Возможно, вы заметили, что вместо создания класса, соответствующего этим протоколам, мы создали структуру. Мы делаем это, чтобы избежать одной из типичных проблем, присущих объектно-ориентированному программированию, ссылкам на объекты .
Представьте, например, что у вас есть два объекта, A и B. A создает некоторые данные самостоятельно и сохраняет ссылку на эти данные. А затем делится этими данными с B по ссылке, что означает, что оба объекта имеют ссылку на один и тот же объект. Без знания A B каким-то образом изменяет данные.
Хотя это может показаться не большой проблемой, может случиться так, что А не ожидал, что данные будут изменены. Объект А может найти данные, с которыми он не знает, как обращаться или обрабатывать. Это общий риск ссылок на объекты.
В Swift структуры передаются по значению, а не по ссылке . Это означает, что в вышеприведенном примере, если данные, созданные A, были упакованы в виде структуры вместо объекта и переданы B, данные будут скопированы вместо общего доступа по ссылке. Это приведет к тому, что и А, и В будут иметь собственную уникальную копию одного и того же фрагмента данных. Внесенное B изменение не повлияет на копию, управляемую A.
Drivable
компонентов Drivable
, Reversible
и Transport
на отдельные протоколы также обеспечивает более высокий уровень настройки, чем традиционное наследование классов. Если вы читали мой первый учебник о новой платформе GameplayKit в iOS 9, то эта ориентированная на протокол модель очень похожа на структуру сущностей и компонентов, используемую в платформе GameplayKit.
Принимая этот подход, пользовательские типы данных могут наследовать функциональность от нескольких источников, а не от одного суперкласса. Учитывая то, что мы получили, мы могли бы создать следующие классы:
- класс с компонентами
Drivable
иReversible
протоколов - класс с компонентами
Drivable
иTransportable
протоколов - класс с компонентами
Reversible
иTransportable
протоколов
При объектно-ориентированном программировании наиболее логичным способом создания этих трех классов будет наследование от одного суперкласса, который содержит компоненты всех трех протоколов. Этот подход, однако, приводит к тому, что суперкласс является более сложным, чем необходимо, и каждый из подклассов наследует больше функциональных возможностей, чем необходимо.
3. Расширения протокола
Все, что я показал вам до сих пор, стало возможным в Swift с момента его выпуска в 2014 году. Те же концепции, ориентированные на протоколы, могли даже применяться к протоколам Objective-C. Однако из-за ограничений, которые существовали в протоколах, настоящее программирование, ориентированное на протокол, было невозможно до тех пор, пока в язык Swift не было добавлено несколько ключевых функций. Одна из наиболее важных из этих функций — это расширения протокола. в том числе условные расширения .
Во-первых, давайте расширим протокол Drivable
и добавим функцию, чтобы определить, является ли конкретный Drivable
быстрее другого. Добавьте на свою игровую площадку следующее:
01
02
03
04
05
06
07
08
09
10
|
extension Drivable {
func isFasterThan(item: Drivable) -> Bool {
return self.topSpeed > item.topSpeed
}
}
let sedan = Car()
let sportsCar = Car(topSpeed: 250, reverseSpeed: 25, seatCount: 2)
sedan.isFasterThan(sportsCar)
|
Вы можете видеть, что когда код игровой площадки выполняется, он выводит значение false
в качестве вашего sedan
автомобиль имеет topSpeed
150
по умолчанию, что меньше, чем у sportsCar
.
Возможно, вы заметили, что мы предоставили определение функции, а не объявление функции. Это кажется странным, потому что протоколы должны содержать только объявления. Правильно? Это еще одна очень важная особенность расширений протокола в Swift 2 — поведение по умолчанию . Расширяя протокол, вы можете обеспечить реализацию по умолчанию для функций и вычисляемых свойств, чтобы классы, соответствующие протоколу, не обязаны это делать.
Далее мы собираемся определить другое Drivable
протокола Drivable
, но на этот раз мы определим его только для типов значений, которые также соответствуют протоколу Reversible
. Это расширение затем будет содержать функцию, которая определяет, какой объект имеет лучший диапазон скоростей. Мы можем достичь этого с помощью следующего кода:
1
2
3
4
5
6
7
|
extension Drivable where Self: Reversible {
func hasLargerRangeThan(item: Self) -> Bool {
return (self.topSpeed + self.reverseSpeed) > (item.topSpeed + item.reverseSpeed)
}
}
sportsCar.hasLargerRangeThan(sedan)
|
Ключевое слово Self
, написанное с большой буквы «S», используется для представления класса или структуры, соответствующей протоколу. В приведенном выше примере ключевое слово Self
представляет структуру Car
.
После запуска кода детской площадки, Xcode выведет результаты на боковой панели справа, как показано ниже. Обратите внимание, что у sportsCar
большая дальность, чем у sedan
.
4. Работа со стандартной библиотекой Swift
Хотя определение и расширение ваших собственных протоколов может быть очень полезным, истинная сила расширений протоколов проявляется при работе со стандартной библиотекой Swift. Это позволяет добавлять свойства или функции к существующим протоколам, таким как CollectionType
(используется для таких вещей, как массивы и словари) и Equatable
(возможность определять, равны ли два объекта или нет). С помощью условных расширений протокола вы также можете предоставить очень специфические функциональные возможности для конкретного типа объекта, соответствующего протоколу.
На нашей площадке мы собираемся расширить протокол CollectionType
и создать два метода: один для получения средней максимальной скорости автомобилей в массиве Car
а другой — для средней скорости движения задним ходом. Добавьте следующий код на вашу игровую площадку:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
extension CollectionType where Self.Generator.Element: Drivable {
func averageTopSpeed() -> Int {
var total = 0, count = 0
for item in self {
total += item.topSpeed
count++
}
return (total/count)
}
}
func averageReverseSpeed<T: CollectionType where T.Generator.Element: Reversible>(items: T) -> Int {
var total = 0, count = 0
for item in items {
total += item.reverseSpeed
count++
}
return (total/count)
}
let cars = [Car(), sedan, sportsCar]
cars.averageTopSpeed()
averageReverseSpeed(cars)
|
Расширение протокола, которое определяет метод averageTopSpeed
использует преимущества условных расширений в Swift 2. В отличие от averageReverseSpeed
функция averageReverseSpeed
мы определяем непосредственно под ней, является еще одним способом достижения аналогичного результата с использованием averageReverseSpeed
Swift. Лично я предпочитаю более чистое расширение протокола CollectionType
, но это зависит от личных предпочтений.
В обеих функциях мы перебираем массив, суммируем общую сумму и затем возвращаем среднее значение. Обратите внимание, что мы вручную сохраняем количество элементов в массиве, потому что при работе с CollectionType
а не с обычными элементами типа Array
, свойство count
является Self.Index.Distance
типа Self.Index.Distance
а не Int
.
Как только ваша игровая площадка выполнит весь этот код, вы увидите среднюю максимальную скорость вывода 183
и среднюю скорость реверса 21
.
5. Важность классов
Несмотря на то, что протоколно-ориентированное программирование является очень эффективным и масштабируемым способом управления вашим кодом в Swift, все еще есть совершенно веские причины для использования классов при разработке в Swift:
Обратная совместимость
Большинство SDK для iOS, watchOS и tvOS написаны на Objective-C с использованием объектно-ориентированного подхода. Если вам нужно взаимодействовать с любым из API, включенных в эти SDK, вы вынуждены использовать классы, определенные в этих SDK.
Ссылка на внешний файл или элемент
Компилятор Swift оптимизирует время жизни объектов в зависимости от того, когда и где они используются. Стабильность объектов на основе классов означает, что ваши ссылки на другие файлы и элементы будут оставаться согласованными.
Ссылки на объекты
Временами ссылки на объекты — это именно то, что вам нужно, например, если вы вводите информацию в конкретный объект, такой как графический рендер. Использование классов с неявным совместным использованием важно в подобных ситуациях, потому что вы должны быть уверены, что средство рендеринга, которому вы отправляете данные, все равно будет таким же, как и раньше.
Вывод
Надеемся, что к концу этого урока вы увидите потенциал программирования, ориентированного на протоколы, в Swift и как его можно использовать для оптимизации и расширения вашего кода. Хотя эта новая методология кодирования не полностью заменит объектно-ориентированное программирование, она приносит ряд очень полезных, новых возможностей.
От поведения по умолчанию до расширений протокола, протоколно-ориентированное программирование в Swift будет принято многими будущими API и полностью изменит наш взгляд на разработку программного обеспечения.
Как всегда, обязательно оставляйте свои комментарии и отзывы в комментариях ниже.