В первой части мы рассмотрели лишь часть новых возможностей Swift 2:
- фундаментальные конструкции языка, такие, как enums, scoping (область действия), синтаксис аргументов и т.д.
- сопоставление с образцом (pattern matching)
- управление ошибками (error handling)
Во второй части мы рассмотрим оставшиеся:
- расширения (extensions) протокола
- проверка доступности (availability checking)
- взаимодействие с Objective-C и С
Я буду рассматривать новые возможности Swift 2, сопровождая их примерами, код которых находится на Github.
Расширения протокола (Protocol extension)
Расширения протокола стало возможно в Swift 2, что позволило добавлять новые функции (в комплекте с реализацией) к любым классам, структурам и перечислениям, которые реализуют этот протокол.
До Swift 2, как в Objective-C, так и в Swift 1.x, протоколы содержали только декларацию методов. С расширениями протокола в Swift 2, протоколы теперь могут содержать наряду с декларацией, реализацию методов. Этой возможности мы годами ждали для Objective-C, поэтому приятно увидеть ее воплощение в новом языке.
Часто бывает, что некоторую функциональность необходимо добавить всем типам, которые подтверждают определенный протокол (интерфейс). Например, все коллекции могут поддерживать концепцию создания новой коллекции на основе преобразований своих элементов. С протоколами старого стиля мы могли реализовать такую возможность двумя способами: 1) разместить метод в протоколе и потребовать, чтобы каждый тип, подтверждающий этот протокол, реализовал метод, или 2) написать глобальную функцию, которая работает на значениях типов, подтверждающих протокол.
Cocoa (Objective-C) в большинстве случаев предпочитает первый способ решения.
Swift 1.x использовал второй способ решения. Такие глобальные функции, как map оперировали с любой коллекцией CollectionType. Это обеспечивало прекрасное разделение кода при реализации, но ужасный синтаксис и невозможность переопределись (override) реализацию для специализации его под определенный тип.
[js]
let x = filter(map(numbers) { $0 * 3 }) { $0 >= 0 } //— Swift 1
[/js]
С расширениями протоколов появилась третья возможность, которая существенно превосходит две остальные. map может быть реализована в расширении протокола CollectionType. Все типы, которые подтверждают протокол CollectionType, автоматически получат реализации map совершенно бесплатно.
[js]
let x = numbers.map { $0 * 3 }.filter { $0 >= 0 } //— Swift 2
[/js]
Для примера рассмотрим в Swift 2 реализацию нового метода myMap как расширение протокола CollectionType.
В результате сразу же мы можем использовать myMap для массивов Array<T>
для словарей Dictionary <Key : Value>
для множеств Set <T>
для строк String.characters
для слайcов ArraySlice <T>
для страйтов StrideThrough<T>, но не напрямую, а через map, преобразующую последовательность (протокол SequenceType) в коллекцию (протокол CollectionType)
Расширение протоколов лежит в основе нового подхода к конструированию программного обеспечения, заявленного Apple как Протокол-Ориентированное Программирование (ПОП), существующее в Swift наряду с традиционным Объектно-Ориентированное Программированием ( ООП) и элементами Функционального Программирования (ФП). Оно должно преодолеть такие проблемы ООП, как «хрупкий базовый класс» и жесткость наследования (rigidity and fragility of inheritance), «проблему ромба» (“diamond problem”), неявное разделение ссылок на объекты, необходимость частого использования «кастинга» вниз (downcasting) в переопределенных методах. Также как тяжело многим разработчикам описывать полиморфизм словами, а легче показать на примере, продемонстрируем возможности Протокол-Ориентированного Программирования на примерах.
Пример 1. Алгоритм тасования Фишера-Йенса
Для более глубокого рассмотрения этих различий давайте рассмотрим реализацию алгоритма Тасование Фишера–Йенса «перемешивания» элементов коллекции на примере функции shuffle для Swift 1.2 (OOП) и Swift 2 (ПОП). Этот алгоритм часто используется при раздаче карт в карточной игре.
В Swift 1.2 мы бы добавили глобальную функцию shuffle для работы с коллекциями согласно способу 2, то есть когда используется глобальная функция с generics:
Давайте посмотрим на эту функцию подробнее. На вход этой глобальной функции в качестве аргумента подается сама коллекция var list: C и возвращается новая коллекция этого же типа С с «перемешанными» значениями первоначальной коллекции list. Эту глобальную функцию можно использовать для всех типов, которые подтверждают протокол MutableCollectionType и имеют целые индексы. Таких коллекций две: массив Array <T> и слайс ArraySlice <T>
Но посмотрите, какое обращение к этой глобальной функции?
[js]
shuffle(strings1)
shuffle(numbers1)
[/js]
Эта функция не позволяет использовать нотацию с «точкой», в круглых скобках нужно указать саму коллекцию. Это очень неудобно, если у вас идет целая цепочка преобразований, которая приводит к множеству вложенных круглых скобок, а если это все чередуется с методами типа, то вообще — беда. Мы уже видели подобный синтаксис выше.
[js]
let x = filter(map(numbers) { $0 * 3 }) { $0 >= 0 } //— Swift 1
[/js]
Если мы хотим работать с «точечной» нотацией в Swift 1.2, то мы добавляем функцию shuffle в расширение каждого отдельного типа, например, класса Array (это способ 1). Причем мы можем добавить как изменяющий по месту (mutating) метод shuffleInPlace, так и метод shuffle, возвращающий новый массив с «перемешанными» элементами исходного массива (non—mutating) ) :
Последние два метода являются расширением (extension) для массива Array и доступны только для массивов .
Ни Set, ни ArraySlice, никакие другие CollectionType не могут их использовать.
В Swift 2 мы добавляем методы shuffle и shuffleInPlace исключительно для расширения протоколов CollectionType и MutableCollectionType (способ 3):
И с расширением протоколов, методы shuffle и shuffleInPlace могут теперь применяться и к Set, и к Array, и к ArraySlice и любой другой CollectionType без каких-либо дополнительных усилий, причем в нужной нам «точечной» нотации:
При расширении протоколов в Swift 2 мы можем устанавливать ограничения на тип. Как видно из кода, мы вначале выполняем расширение протокола только для изменяемой коллекции MutableCollectionType, которая использует в качестве индексов целые числа, а затем распространяем на CollectionType.
В конце нужно сделать небольшое замечание относительно алгоритма «перемешивания» элементов коллекции. В Swift 2 уже есть эффективная и корректная реализация алгоритма тасования Фишера-Йенса в GameplayKit (который несмотря на название подходит не только для игр). Правда этот метод работает только с массивами.
Пример 2. Прощай pipe (конвейерный) оператор |
> с приходом возможности расширения протоколов.
C появлением Swift и возможности создания пользовательских операторов, в том числе и операторов функционального программирования, предпринимались попытки и довольно успешные, решать некоторые задачи с помощью приемов функционального программирования. В частности, для алгоритма Луна вычисления контрольной цифры пластиковой карты было предложено использовать pipe (конвейерный) оператор |
> , чтобы избежать надоедливого метания между функциями и методами. Но вышел Swift 2 и техника расширения протоколов позволила решить эту задачу еще проще.
Оригинальный алгоритм, описанный разработчиком
1. Цифры проверяемой последовательности нумеруются справа налево.
2. Цифры, оказавшиеся на нечётных местах, остаются без изменений.
3. Цифры, стоящие на чётных местах, умножаются на 2.
4. Если в результате такого умножения возникает число больше 9 (например, 8 × 2 = 16), оно заменяется суммой цифр получившегося произведения (например, 16: 1 + 6 = 7, 18: 1 + 8 = 9)— однозначным числом, то есть цифрой.
5. Все полученные в результате преобразования цифры складываются. Если сумма кратна 10, то исходные данные верны.
Алгоритм состоит из последовательности шагов, каждый из которых будем описывать с помощью расширения либо протокола, либо типа, либо с использованием известных методов.
Во-первых, нужно напомнить, что в Swift 2 String больше не является последовательностью, но String.characters является последовательностью символов, и нам нужно преобразование символа в целое число. Это преобразование мы построим на расширении типа Int. То есть вместо String.toInteger мы получим Int.init(String) :
Это преобразование возвращает Optional, так как в номере кредитной карты могут быть пробелы, а нам нужно дальше проводить арифметические операции над полученными целыми числами, поэтому используем появившийся в Swift 2 метод flatMap, который уберет все пробелы в номере карты
Согласно нашему алгоритму, мы должны рассматривать цифры справа налево, а у нас они следуют слева направо, поэтому расположим нашу последовательность чисел в обратном направлении с помощью метода reverse и тривиального метода map
Но нам нужен не простой map, а map, проводящий преобразования только над каждым N- м членом последовательности. Опять выполняем расширение, но теперь уже не типа, а протокола SequenceType
Получаем следующий результат
Затем нам нужен метод, позволяющий вычислять сумму любой последовательности, содержащей целые числа:
и метод вычисления числа по модулю другого числа
В результате получаем метод luhnchecksum(), который добавляем в тип String и который вычисляет контрольную сумму одной строкой
Теперь очень просто получить результат:
Некоторые особенности расширения протокола
Я думаю, что расширение протоколов может быть своебразным ответом Apple на вопрос о необязательных (optional) методах протокола. Чистые Swift протоколы не могут и не должны иметь необязательные (optional) методы. Мы привыкли к необязательные (optional) методам в Objective-C протоколах, например, для таких вещей, как делегаты:
[objc]
@protocol MyClassDelegate
@optional
— (BOOL)shouldDoThingOne;
— (BOOL)shouldDoThingTwo
@end
[/objc]
Чистый Swift не имеет эквивалента:
До сих пор, все, кто подтверждал этот протокол, должны были реализовать все эти методы. Это конфликтует с идеей делегирования Cocoa как возможности необязательной настройки всех методов делегата, отдавая предпочтение реализации методов по умолчанию. С появлением расширений протокола в Swift 2, разумное поведение по умолчанию может быть обеспечено самим протоколом:
В конечном счете это обеспечивает ту же самую функционаность, что и @optional в Objective-C, но без обязательных проверок в runtime.
Проверка доступности API
Одна профессиональная проблема, которая удручает iOS разработчиков, — это небходимость быть очень внимательными при использовании новых APIs. Например, если вы попытаетесь использовать UIStackView в iOS 8, то ваше приложение закончится аварийно. В давние времена Objective C разработчики написали бы подобный код:
[objc]
NSClassFromString(@"UIAlertController") != nil
[/objc]
Это означает «если класс UIAlertController существует,» и является способом проверки, запускается ли это на iOS 8 или позже. Но из-за того, что Xcode не догадывался об истинной цели этого кода, то он и не гарантировал нам правильность его исполнения. Все изменилось в Swift 2, потому что вы можете явно написать такой код:
[js]
if #available(iOS 9, *) {
let stackView = UIStackView()
// работаем дальше…
}
[/js]
Магия происходит с появлением предложения #available: оно автоматически проверяет, запускаетесь ли вы на версии iOS 9 или более поздней, и если «да», то код с UIStackView будет запущен. Наличие символа «*» после «iOS 9» означает, что это предложение будет выполняться для любой будущей платформы, которую Apple представит.
Предложение #available замечательно еще тем, что оно дает вам возможность писать код в else блоке, потому что Xcode теперь знает, что этот блок будет исполняться, если на приборе версия iOS 8 или младше и сможет предупредить вас, если вы будете использовать здесь новые APIs. Например, если вы написали что-то подобное:
[js]
if #available(iOS 9, *) {
// do cool iOS 9 stuff
} else {
let stackView = UIStackView()
}
[/js]
…то получите ошибку:
Xcode видит, что вы пытаетесь использовать UIStackView там, где он недоступен и он просто не позволит, чтобы это произошло. Таким образом, переключаясь с «доступен ли этот класс» на то, чтобы сказать Xcode о наших действительных намерениях, мы получили гигантскую поддержку нашей безопасности.
Совместимость
Когда вы пишите код на Swift, то есть ряд заранее прописанных правил, которые говорят компилятору, нужно ли и как, экспонировать на Objective-C методы, свойства и т.д. Более того, в вашем распоряжении есть небольшое количество атрибутов, с помощью которых вы можете этим процессом управлять. Вот эти атрибуты:
@IBOutlet
и@IBAction
позволяют Swift свойствам и методам интерпретироваться как outlets и Actions в Interface Builder;dynamic
, который позволяет использовать KVO для заданного свойства;@objc
, который используется для того, чтобы сделать класс или свойство вызываемым из Objective-C.
В Swift 2 появился новый такой атрибут @nonobjc, который явно предотвращает использование свойств или методов от экспонирования в Objective-C. Этот атрибут очень полезен, например, в следующем случае. В приложении Калькулятор (смотри ниже) в класса ViewController, который наследует от Cocoa класса UIViewController, вы определили 3 метода с одинаковым названием performOperation, но разными аргументами. Это нормально, Swift в состоянии различить эти методы, просто основываясь на различных типах аргумента. Но Objective-C так не работает. В Objective-C методы различаются только по именам, а не по типам. Если эти методы выставлены для использования в Objective-C, то вы получите ошибку о том, что в таком виде их использовать в Objecrive-C нельзя. Все дело в том, что наш класс ViewController, который мы создали для интерфейса калькулятора, наследует от Cocoa класса UIViewCiontroller, и компилятор автоматически неявно метит все свойства и методы атрибутом @objc. Если у вас нет намерения использовать методы в Objective-C, то вы должны снабдить их атрибутом @noobjc и ошибка исчезает:
. . . . . . . . . . . . . . . . .
Другая область, в которой Swift 2 пытается улучшить совместимость — это совместимость с указателями C функций. Целью этого улучшения является исправление надоедливого ограничения Swift, которое не дает возможность полностью работать с таким важным С — фреймворком как Core Audio, интенсивно использующим callback функции. В Swift 1.x не было возможности напрямую заменить указатель на С функцию Swift функцией. Вам нужно было писать небольшую «обертку» на C или Objective-C, которая инкапсулирует callback функцию. В Swift 2 стало возможным делать это полностью естественным для Swift 2 образом. Указатели на C функции импортируются в Swift как замыкания. Вы можете передать любое Swift 2 замыкание или функцию с подходящими параметрами в код, который ожидает указателя на C функцию – с одним существенным ограничением: в противоположность замыканиям, указатели на C функции не имеют концепции «захваченного» состояния (они являются просто указателями). В результате для совместимости с указателями на C функции компилятор разрешит использовать только те Swift 2 замыкания, которые не «захватывают» никакой внешний контекст. Swift 2 использует новую нотацию @convention(c) для индикации этого соглашения при вызовах:
Например, для стандартной C функции сортировки qsort это будет выглядеть так:
Очень хороший пример представлен в работе C Callbacks in Swift, в которой показано как получить доступ к элементам CGPath или UIBezierPath с помощью вызова CGPathApply
функции и передачи указателя на callback функцию. CGPathApply затем вызывает этот callback для каждого path элемента.
Теперь пройдемся по всему path и напечатаем описание его элементов:
Или вы можете посчитать, сколько closepath команд в этом path:
В заключении можно сказать, что Swift 2 автоматически обеспечивает совместимость (bridges) указателей C функций и замыканий. Это делает возможной (и очень удобной) работу с большим числом C APIs , которые используют указатели функций в качестве callbacks. Из-за того, что соглашения по вызовам C функций не позволяют этим замыканиям «захватывать» внешнее состояние, вам часто приходится передавать внешние переменные, в доступе к которым нуждается ваше callback замыкание, через void указатель, который многие C APIs предлагают для этой цели. Это делается в Swift 2 немного извилистым путем, но полностью возможно.
Новые возможности Objective-C
Apple представила три новых возможности в Objective-C в Xcode 7 с прицелом использования их для более «гладкой» совместимости с Swift:
- nullability;
- легковесные (lightweight) generics;
__kindof
типы.
Nullability
Эта возможность была представлена уже в Xcode 6.3, но стоит упомянуть о том, что Objective-C теперь позволяет точно характеризовать поведение любых методов и свойств на предмет того, могут ли они быть nil или нет. Это адресовано напрямую требованиям Swift к Optional или не-Optional типам и делает интерфейс Objective-C более выразительным. Существуют три квалификатора для nullability:
nullable
(__nullable
для C указателей), означающий, что указатель может бытьnil
и преобразуется в Swift как Optional тип — ?;nonnull
(__nonnull
для C указателей), означающий, чтоnil
не разрешен и преобразуется в Swift как не Optional тип;null_unspecified
(__null_unspecified
для C указателей), нет информации, о том какое поведение поддерживается; в этом случае такой указатель преобразуется в Swift как автоматически «развернутое» Optional — !.
Квалификаторы, указанные выше, могут использоваться для аннотирования Objective-C классов как в следующем примере:
В этом примере целая область, отмеченная скобками NS_ASSUME_NONNULL_BEGIN и NS_ASSUME_NONNULL_END, выбрана для того, чтобы nonnull имел значение, которое используется по умолчанию. Это позволяет разработчику аннотировать только те элементы, которые не соответствуют значению по умолчанию.
Легковесные (lightweight) generics
Легковесные (lightweight) generics в Objective-C, возможно, являются самыми желательными в Objective-C на протяжении последней декады, особенно для инженеров Apple. Они необходимы для использования с коллекциями типа NSArray, NSDictionary и т.д.. Одним из недостатков коллекций в Objective-C является потеря практически всей информации о типе при портировании их в Swift, по умолчанию в Swift мы получаем коллекцию AnyObject и должны применять down «кастинг» в подавляющем числе случаев. Но теперь можно задекларировать тип элементов массива в Xcode 7 таким образом:
В нашем случае мы декларируем изменяемый массив строк. Если вы попытаетесь записать в него число, то компилятор выдаст предупреждение о несоответствие типов.
Легковесные generics оказались очень полезными для совместимости (interoperability) между Objective-C и Swift в плане представления классов NSArray, NSDictionary и т.д., так как теперь вам не нужно делать множество «кастингов» в вашем Swift коде из-за того, что все фреймворки Apple написаны на Objective-C.
Видите? Теперь subviews не являются массивом [AnyObject], они передаются в Swift как [UIView].
Теперь в Objective-C вы можете декларировать свой собственный generic класс:
И использовать его
В случае несоответствия типов выдается предупреждение. К сожалению, использование своих собственных generic типов имеет преимущество только внутри кода Objective-C и игнорируется Swift.
__kindof типы
__kindof типы относятся к generics, и их появление мотивируется следующим случаем. Как известно, класс UIView имеет свойство subviews, которое представляет собой массив UIView объектов:
[js]
@interface UIView
@property(nonatomic,readonly,copy) NSArray<UIView *> *subviews;
@end
[/js]
Если вы добавляете UIButton к родительскому UIView как самое удаленное на заднем фоне subview, и пытаетесь послать ему сообщение, которое имеет значение только для UIButton, то компилятор выдаст предупреждение. Это хорошо, но мы точно знаем, что расположенное на заднем плане subview является UIButton:
[js]
[view insertSubview:button atIndex:0];
//— warning: UIView may not respond to setTitle:forState:
[view.subviews[0] setTitle:@"Cancel" forState:UIControlStateNormal];
[/js]
Используя __kindof тип, мы можем предоставить некую гибкость системе типизации в Objective-C так, чтобы действовал неявный «кастинг» как superclass, так и любого subclass :
[js]
@interface UIView
@property(nonatomic,readonly,copy) NSArray< _kindof UIView *> *subviews;
@end
//— no warnings here:
[view.subviews[0] setTitle:@"Cancel" forState:UIControlStateNormal];
UIButton *button = view.subviews[0];
[/js]
Легковесные generics и __kindof типы позволяют разработчику убрать id/AnyObject практически везде из большинства своих APIs. id может все еще потребоваться в тех случаях, когда действительно нет информации о том, с каким типом вы имеете дело:
[js]
@property (nullable, copy) NSDictionary<NSString *, id> *userInfo;
[/js]
Ссылки на используемые статьи
New features in Swift 2
What I Like in Swift 2
A Beginner’s guide to Swift 2
Error Handling in Swift 2.0
Swift 2.0: Let’s try?
Video Tutorial: What’s New in Swift 2 Part 4: Pattern Matching
Throw What Don’t Throw
The Best of What’s New in Swift
What’s new in Swift 2
Swift 2.0: API Availability Checking
How do I shuffle an array in Swift?
Swift 2.0 shuffle / shuffleInPlace
C Callbacks in Swift,
Protocol extensions and the death of the pipe-forward operator
Swift protocol extension method dispatch
API Availability Checking in Swift 2
Interacting with C APIs
What’s new in iOS 9: Swift and Objective-C
Xcode 7 Release Notes