«Что нового в 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.

45069c85017d47df86ecb7f54f6e4cd9.png

В результате сразу же мы можем использовать myMap для массивов Array<T>
65f0bb8259c041ac89706cdfbc94c11f.png

для словарей Dictionary <Key: Value>
e3323ce4d930451faebeaa2b88e4f8d7.png

для множеств Set <T>
22e926c5dedb427884284481be5b4bbd.png

для строк String.characters
580a894aca224c59aa0fdfec9d3d5d48.png

для слайcов ArraySlice <T>
c82af1ef38784a48b4b3498aebf62745.png

для страйтов StrideThrough<T>, но не напрямую, а через map, преобразующую последовательность (протокол SequenceType) в коллекцию (протокол CollectionType)
909960e7403e4ef39e764e255f08648f.png

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

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


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

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

28c15190a6824cd683564220d80a3b2e.png

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

d404395fcd0340d98db0b04fc4cc502a.png

Но посмотрите, какое обращение к этой глобальной функции?

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) ):

adcfabb97417425884dbe5c127c6cee5.png

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

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

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

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

91e8febe70944f7ba74eb491acf5be4a.png

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

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

Пример 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):

948101c3b7c942a4be37bdf9e1b8c7e4.png

Это преобразование возвращает Optional, так как в номере кредитной карты могут быть пробелы, а нам нужно дальше проводить арифметические операции над полученными целыми числами, поэтому используем появившийся в Swift 2 метод flatMap, который уберет все пробелы в номере карты

d78194c3a987469eaedc095291ac360e.png

Согласно нашему алгоритму, мы должны рассматривать цифры справа налево, а у нас они следуют слева направо, поэтому расположим нашу последовательность чисел в обратном направлении с помощью метода reverse и тривиального метода map

eae11d9f011b4ad597f7fa693883d9e2.png

Но нам нужен не простой map, а map, проводящий преобразования только над каждым N- м членом последовательности. Опять выполняем расширение, но теперь уже не типа, а протокола SequenceType

7cbc424db784433288eea84889a400f6.png

Получаем следующий результат

9f4e21236e5b4d118851db68f8323f2b.png

Затем нам нужен метод, позволяющий вычислять сумму любой последовательности, содержащей целые числа:

2817551b931a4eefbeaf6bb8214168b2.png

и метод вычисления числа по модулю другого числа

d7d69fe51718420a86d0778a5779b629.png

В результате получаем метод luhnchecksum(), который добавляем в тип String и который вычисляет контрольную сумму одной строкой

40b5c3b51b37469c81ecd98e64b3cc84.png

Теперь очень просто получить результат:

c74ef15b190646f2ad9a9ab1ac06d74d.png

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


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

@protocol MyClassDelegate
    @optional
 
    - (BOOL)shouldDoThingOne;
    - (BOOL)shouldDoThingTwo
 
    @end

Чистый Swift не имеет эквивалента:
7820bc984f9c4637b104c18d991d8dc3.png

До сих пор, все, кто подтверждал этот протокол, должны были реализовать все эти методы. Это конфликтует с идеей делегирования Cocoa как возможности необязательной настройки всех методов делегата, отдавая предпочтение реализации методов по умолчанию. С появлением расширений протокола в Swift 2, разумное поведение по умолчанию может быть обеспечено самим протоколом:

fada063dd30b4278bfef12484261e72a.png

В конечном счете это обеспечивает ту же самую функциональность, что и @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()
}


…то получите ошибку:

569876be9ae94b3da5a9d5293d35918f.png

Xcode видит, что вы пытаетесь использовать UIStackView там, где он недоступен и он просто не позволит, чтобы это произошло. Таким образом, переключаясь с «доступен ли этот класс» на то, чтобы сказать Xcode о наших действительных намерениях, мы получили гигантскую поддержку нашей безопасности.

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


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

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

bbe6f9b01a7e4aadba2db0018a32a8fc.png

В 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 и ошибка исчезает:

033fdc8f08a24a5588491be79a41a8d2.png
.........................
6d066fefc5484c5a9947954688d948a2.png
Другая область, в которой 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 для индикации этого соглашения при вызовах:

ae57e4c9e53a43c8932a8e718a6fdc69.png

Например, для стандартной C функции сортировки qsort это будет выглядеть так:

abee5d5bdfa04902afaf461d4219e0c3.png
Очень хороший пример представлен в работе C Callbacks in Swift, в которой показано как получить доступ к элементам CGPath или UIBezierPath с помощью вызова CGPathApply функции и передачи указателя на callback функцию. CGPathApply затем вызывает этот callback для каждого path элемента.

189d8113b985445d80b4b91cc89b3b03.png

b084611670314bcfa4d46b9728ad8d9c.png

Теперь пройдемся по всему path и напечатаем описание его элементов:

714c00ddab8b4fec97887d40fc78e67d.png

Или вы можете посчитать, сколько closepath команд в этом path:

58188dd57c9547d9b7956e936648171c.png

В заключении можно сказать, что 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 классов, как в следующем примере:

4d406201f0ab4c639b0ec0d639927474.png

В этом примере целая область, отмеченная скобками 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 таким образом:

b5738da337314770ac8753ef955ffea1.png

В нашем случае мы декларируем изменяемый массив строк. Если вы попытаетесь записать в него число, то компилятор выдаст предупреждение о несоответствие типов.
Легковесные generics оказались очень полезными для совместимости (interoperability) между Objective-C и Swift в плане представления классов NSArray, NSDictionary и т.д., так как теперь вам не нужно делать множество «кастингов» в вашем Swift коде из-за того, что все фреймворки Apple написаны на Objective-C.

045de82eb53043b6bd62d6cbdb93ac14.png

Видите? Теперь subviews не являются массивом [AnyObject], они передаются в Swift как [UIView].
Теперь в Objective-C вы можете декларировать свой собственный generic класс:

e351677f5c66410e85dfb7ccf8c689c9.png

И использовать его

470b2a1fc4304d92a3be7e30c8b55f8f.png
В случае несоответствия типов выдается предупреждение. К сожалению, использование своих собственных 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

© Habrahabr.ru