«Что нового в Swift 2?» на примерах. Часть 2.

Screen Shot 2015-10-28 at 3.18.06 PM

В первой части мы рассмотрели лишь часть новых возможностей Swift 2:

  • фундаментальные конструкции языка, такие, как enumsscoping (область действия), синтаксис аргументов и т.д.
  •  сопоставление с образцом (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.

Screen Shot 2015-10-24 at 7.04.39 PM

В результате сразу же мы можем использовать  myMap  для массивов Array<T>
Screen Shot 2015-10-29 at 2.22.16 PM
для словарей Dictionary <Key : Value>
Screen Shot 2015-10-29 at 2.24.27 PM
для множеств Set <T>
Screen Shot 2015-10-29 at 2.26.18 PM
для строк String.characters
Screen Shot 2015-10-29 at 2.28.00 PM
для слайcов ArraySlice <T>
Screen Shot 2015-10-29 at 2.30.40 PM
для страйтов StrideThrough<T>, но не напрямую, а через map, преобразующую последовательность (протокол SequenceType) в коллекцию (протокол CollectionType)

Screen Shot 2015-10-29 at 2.32.35 PM

Расширение протоколов лежит в основе  нового подхода к конструированию программного обеспечения, заявленного Apple как Протокол-Ориентированное Программирование (ПОП), существующее в Swift наряду с традиционным Объектно-Ориентированное Программированием ( ООП) и элементами Функционального Программирования (ФП). Оно должно преодолеть такие проблемы ООП, как «хрупкий базовый класс» и жесткость наследования (rigidity and fragility of inheritance), «проблему ромба» (“diamond problem”), неявное разделение ссылок на объекты, необходимость частого использования «кастинга» вниз (downcasting) в переопределенных методах. Также как тяжело многим разработчикам описывать полиморфизм словами, а легче показать на примере, продемонстрируем возможности Протокол-Ориентированного Программирования на примерах.

Пример 1. Алгоритм тасования Фишера-Йенса

Для более глубокого рассмотрения этих различий давайте рассмотрим реализацию алгоритма Тасование Фишера–Йенса «перемешивания» элементов коллекции на примере функции shuffle для Swift 1.2 (OOП) и Swift 2 (ПОП). Этот алгоритм часто используется при раздаче карт в карточной игре.

В Swift 1.2 мы бы добавили глобальную функцию shuffle для работы с коллекциями согласно способу 2, то есть когда используется глобальная функция с generics:

Screen Shot 2015-10-26 at 12.35.03 PM

Давайте посмотрим на эту функцию подробнее. На вход этой глобальной функции в качестве аргумента подается сама коллекция  var list: C и возвращается новая коллекция этого же типа С с «перемешанными» значениями первоначальной коллекции list. Эту глобальную функцию можно использовать для всех типов, которые подтверждают протокол MutableCollectionType и имеют целые индексы. Таких коллекций две: массив Array <T> и слайс ArraySlice <T>

Screen Shot 2015-10-25 at 3.56.29 PM
Но посмотрите, какое обращение к этой глобальной функции?

[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, возвращающий  новый массив с «перемешанными» элементами исходного массива (nonmutating) ) :

Screen Shot 2015-10-25 at 5.08.04 PM

Последние два метода являются расширением (extension) для массива Array и доступны только для массивов .

Screen Shot 2015-10-25 at 5.24.52 PM

Ни Set, ни ArraySlice, никакие другие CollectionType не могут их использовать.

В Swift 2 мы добавляем методы shuffle и shuffleInPlace исключительно для расширения протоколов CollectionType и MutableCollectionType (способ 3):

Screen Shot 2015-10-26 at 12.39.59 PM
И с расширением протоколов, методы shuffle и shuffleInPlace могут теперь применяться и к Set, и к Array, и к ArraySlice и любой другой CollectionType без каких-либо дополнительных усилий, причем в нужной нам «точечной» нотации:

Screen Shot 2015-10-25 at 5.56.35 PM

При расширении протоколов в Swift 2 мы можем устанавливать ограничения на тип. Как видно из кода, мы вначале выполняем расширение протокола только для изменяемой коллекции MutableCollectionType, которая использует в качестве индексов целые числа, а затем распространяем на CollectionType.

В конце нужно сделать небольшое замечание относительно алгоритма «перемешивания» элементов коллекции. В Swift 2 уже есть эффективная и корректная реализация алгоритма  тасования Фишера-Йенса в GameplayKit (который несмотря на название подходит не только для игр). Правда этот метод работает только с массивами.

Screen Shot 2015-10-25 at 11.32.11 PM

Пример 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) :
Screen Shot 2015-10-26 at 1.48.58 PM
Это преобразование возвращает Optional, так как в номере кредитной карты могут быть пробелы, а нам нужно дальше проводить арифметические операции над полученными целыми числами, поэтому используем появившийся в Swift 2 метод flatMap, который уберет все пробелы в номере карты
Screen Shot 2015-10-29 at 5.06.32 PM
Согласно нашему алгоритму, мы должны рассматривать цифры справа налево, а у нас они следуют слева направо, поэтому расположим нашу последовательность чисел в обратном направлении с помощью метода reverse и тривиального метода map

Screen Shot 2015-10-29 at 5.10.04 PM

Но нам нужен не простой map, а map, проводящий преобразования только над каждым N- м членом последовательности. Опять выполняем расширение, но теперь уже не типа, а протокола SequenceType
Screen Shot 2015-10-26 at 2.49.29 PM
Получаем следующий результат
Screen Shot 2015-10-29 at 5.13.37 PM
Затем нам нужен метод, позволяющий вычислять сумму любой последовательности, содержащей целые числа:
Screen Shot 2015-10-26 at 2.56.02 PM
и метод вычисления числа по модулю другого числа

Screen Shot 2015-10-26 at 3.12.13 PM
В результате получаем метод  luhnchecksum(), который добавляем в тип String и который вычисляет контрольную сумму одной строкойScreen Shot 2015-10-26 at 3.45.45 PM
Теперь очень просто получить результат:
Screen Shot 2015-10-26 at 3.50.56 PM

Некоторые особенности расширения протокола

Я думаю, что расширение протоколов может быть своебразным ответом Apple на вопрос о необязательных (optional) методах протокола. Чистые Swift протоколы не могут и не должны иметь необязательные (optional) методы. Мы привыкли к необязательные (optional) методам в Objective-C протоколах, например, для таких вещей, как делегаты:

[objc]
@protocol MyClassDelegate
@optional

— (BOOL)shouldDoThingOne;
— (BOOL)shouldDoThingTwo

@end
[/objc]

Чистый Swift не имеет эквивалента:
Screen Shot 2015-10-26 at 9.03.41 PM
До сих пор, все, кто подтверждал этот протокол, должны были реализовать все эти методы. Это конфликтует с идеей делегирования Cocoa как возможности необязательной настройки всех методов делегата, отдавая предпочтение реализации методов по умолчанию. С появлением расширений протокола в Swift 2, разумное поведение по умолчанию может быть обеспечено самим протоколом:
Screen Shot 2015-10-26 at 9.01.31 PM
В конечном счете это обеспечивает ту же самую функционаность, что и  @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]

…то получите ошибку:
Screen Shot 2015-10-26 at 10.38.50 PM
Xcode видит, что вы пытаетесь использовать UIStackView там, где он недоступен и он просто не позволит, чтобы это произошло. Таким образом, переключаясь с «доступен ли этот класс» на то, чтобы сказать Xcode о наших действительных намерениях, мы получили гигантскую поддержку нашей безопасности.

Совместимость

Когда вы пишите код на Swift, то есть ряд заранее прописанных правил, которые говорят компилятору, нужно ли и как, экспонировать на Objective-C методы, свойства и т.д. Более того, в вашем распоряжении есть небольшое количество атрибутов, с помощью которых вы можете этим процессом управлять. Вот эти атрибуты:

  • @IBOutlet и @IBAction позволяют Swift свойствам и методам интерпретироваться как outlets и Actions в  Interface Builder;
  • dynamic, который позволяет использовать KVO для заданного свойства;
  • @objc, который используется для того, чтобы сделать класс или свойство вызываемым из Objective-C.

Screen Shot 2015-10-26 at 7.03.04 PM
В 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 и ошибка исчезает:
Screen Shot 2015-10-26 at 7.07.49 PM
. . . . . . . . . . . . . . . . .
Screen Shot 2015-10-26 at 7.07.36 PM

Другая область, в которой 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) для индикации этого соглашения при вызовах:
Screen Shot 2015-10-28 at 10.24.56 AM
Например, для стандартной C функции сортировки qsort это будет выглядеть так:
Screen Shot 2015-10-28 at 10.20.06 AM
Очень хороший пример представлен в работе  C Callbacks in Swift, в которой показано как получить доступ к элементам CGPath или UIBezierPath с помощью вызова CGPathApply функции и передачи указателя на callback функцию. CGPathApply затем вызывает этот callback для каждого path элемента.
Screen Shot 2015-10-28 at 2.41.54 PM
Screen Shot 2015-10-28 at 11.05.00 AM
Теперь пройдемся по всему path и напечатаем описание его элементов:
Screen Shot 2015-10-28 at 11.08.34 AM
Или вы можете посчитать, сколько closepath команд в этом path:
Screen Shot 2015-10-28 at 11.20.34 AM
В заключении можно сказать, что 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 классов как в следующем примере:
Screen Shot 2015-10-29 at 7.53.50 PM
В этом примере целая область, отмеченная скобками  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 таким образом:
Screen Shot 2015-10-29 at 10.02.51 PM
В нашем случае мы декларируем изменяемый массив строк. Если вы попытаетесь записать в него число, то компилятор выдаст предупреждение о несоответствие  типов.
Легковесные generics оказались очень полезными для совместимости (interoperability) между Objective-C и Swift в плане представления классов NSArrayNSDictionary и т.д., так как теперь вам не нужно делать множество «кастингов» в вашем Swift коде из-за того, что все фреймворки Apple написаны на Objective-C.
Screen Shot 2015-10-29 at 10.11.03 PM
Видите? Теперь subviews не являются массивом [AnyObject], они передаются в Swift как [UIView].
Теперь в Objective-C вы можете декларировать свой собственный  generic класс:
Screen Shot 2015-10-29 at 8.58.48 PM
И использовать его
Screen Shot 2015-10-29 at 10.19.35 PM
В случае несоответствия типов выдается предупреждение. К сожалению, использование своих собственных 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