«Что нового в Swift 2?» на примерах. Часть 2
В первой части мы рассмотрели лишь часть новых возможностей Swift 2:
- фундаментальные конструкции языка, такие, как enums, scoping (область действия), синтаксис аргументов и т.д.
- сопоставление с образцом (pattern matching)
- управление ошибками (error handling)
Во второй части мы рассмотрим оставшиеся:
- расширения (extensions) протокола
- проверка доступности (availability checking)
- взаимодействие с Objective-C и С
Я буду рассматривать новые возможности Swift 2, сопровождая их примерами, код которых находится на Github.
Расширения протокола (Protocol extensions)
Расширения протокола стали возможны в Swift 2, что позволило добавлять новые функции (в комплекте с реализацией) к любым классам, структурам и перечислениям, которые реализуют протокол.
До Swift 2, как в Objective-C, так и в Swift 1.x, протоколы содержали только декларацию методов. С расширениями протокола в Swift 2, протоколы теперь могут содержать наряду с декларацией, реализацию методов. Этой возможности мы годами ждали для Objective-C, поэтому приятно увидеть ее воплощение в новом языке.
Часто бывает, что некоторую функциональность необходимо добавить всем типам, которые подтверждают определенный протокол (интерфейс). Например, все коллекции могут поддерживать концепцию создания новой коллекции на основе преобразований своих элементов. С протоколами старого стиле мы могли реализовать такую возможность двумя способами: 1) разместить метод в протоколе и потребовать, чтобы каждый тип, подтверждающий этот протокол, реализовал метод, или 2) написать глобальную функцию, которая работает на значениях типов, подтверждающих протокол.
Cocoa (Objective-C) в большинстве случаев предпочитает первый способ решения.
Swift 1.x использовал второй способ решения. Такие глобальные функции, как map оперировали с любой коллекцией CollectionType. Это обеспечивало прекрасное разделение кода при реализации, но ужасный синтаксис и невозможность переопределить (override) реализацию под определенный тип.
let x = filter(map(numbers) { $0 * 3 }) { $0 >= 0 } //-- Swift 1
С расширениями протоколов появилась третья возможность, которая существенно превосходит две остальные. map может быть реализована в расширении протокола CollectionType. Все типы, которые подтверждают протокол CollectionType, автоматически получат реализации map совершенно бесплатно.
let x = numbers.map { $0 * 3 }.filter { $0 >= 0 } //-- Swift 2
Для примера рассмотрим в 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>
Но посмотрите, какое обращение к этой глобальной функции?
shuffle(strings1)
shuffle(numbers1)
Эта функция не позволяет использовать нотацию с «точкой», в круглых скобках нужно указать саму коллекцию. Это очень неудобно, если у вас идет целая цепочка преобразований, которая приводит к множеству вложенных круглых скобок, а если это все чередуется с методами типа, то вообще — беда. Мы уже видели подобный синтаксис выше.
let x = filter(map(numbers) { $0 * 3 }) { $0 >= 0 } //-- Swift 1
Если мы хотим работать с «точечной» нотацией в 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 протоколах, например, для таких вещей, как делегаты:
@protocol MyClassDelegate
@optional
- (BOOL)shouldDoThingOne;
- (BOOL)shouldDoThingTwo
@end
Чистый Swift не имеет эквивалента:
До сих пор, все, кто подтверждал этот протокол, должны были реализовать все эти методы. Это конфликтует с идеей делегирования Cocoa как возможности необязательной настройки всех методов делегата, отдавая предпочтение реализации методов по умолчанию. С появлением расширений протокола в Swift 2, разумное поведение по умолчанию может быть обеспечено самим протоколом:
В конечном счете это обеспечивает ту же самую функциональность, что и @optional в Objective-C, но без обязательных проверок в runtime.
Проверка доступности API
Одна профессиональная проблема, которая удручает iOS разработчиков, — это необходимость быть очень внимательными при использовании новых APIs. Например, если вы попытаетесь использовать UIStackView в iOS 8, то ваше приложение закончится аварийно. В давние времена Objective C разработчики написали бы подобный код:
NSClassFromString(@"UIAlertController") != nil
Это означает «если класс UIAlertControllerl существует,» и является способом проверки, запускается ли это на iOS 8 или позже. Но из-за того, что Xcode не догадывался об истинной цели этого кода, то он и не гарантировал нам правильность его исполнения. Все изменилось в Swift 2, потому что вы можете явно написать такой код:
if #available(iOS 9, *) {
let stackView = UIStackView()
// работаем дальше...
}
Магия происходит с появлением предложения #available: оно автоматически проверяет, запускаетесь ли вы на версии iOS 9 или более поздней, и если «да», то код с UIStackView будет запущен. Наличие символа «*» после «iOS 9» означает, что это предложение будет выполняться для любой будущей платформы, которую Apple представит.
Предложение #available замечательно еще тем, что оно дает вам возможность писать код в else блоке, потому что Xcode теперь знает, что этот блок будет исполняться, если на приборе версия iOS 8 или младше и сможет предупредить вас, если вы будете использовать здесь новые APIs. Например, если вы написали что-то подобное:
if #available(iOS 9, *) {
// do cool iOS 9 stuff
} else {
let stackView = UIStackView()
}
…то получите ошибку:
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 функции сортировки 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
Новые возможности 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. Они действуют только на уровне компилятора, в runtime их нет.
__kindof типы
__kindof типы относятся к generics, и их появление мотивируется следующим случаем. Как известно, класс UIView имеет свойство subviews, которое представляет собой массив UIView объектов:
@interface UIView
@property(nonatomic,readonly,copy) NSArray< UIView *> *subviews;
@end
Если вы добавляете UIButton к родительскому UIView как самое удаленное на заднем плане subview, и пытаетесь послать ему сообщение, которое имеет значение только для UIButton, то компилятор выдаст предупреждение. Это хорошо, но мы точно знаем, что расположенное на заднем плане subview является UIButton и хотим послать ему сообщение:
[view insertSubview:button atIndex:0];
//-- warning: UIView may not respond to setTitle:forState:
[view.subviews[0] setTitle:@"Cancel" forState:UIControlStateNormal];
Используя __kindof тип, мы можем предоставить некую гибкость системе типизации в Objective-C так, чтобы действовал неявный «кастинг» как superclass, так и любого subclass:
@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];
Легковесные generics и __kindof типы позволяют разработчику убрать id/AnyObject практически везде из большинства своих APIs. id может все еще потребоваться в тех случаях, когда действительно нет информации о том, с каким типом вы имеете дело:
@property (nullable, copy) NSDictionary<NSString *, id> *userInfo;
Ссылки на используемые статьи
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