Как стать автором
Обновить

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

Время на прочтение13 мин
Количество просмотров19K
В первой части мы рассмотрели лишь часть новых возможностей 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
Теги:
Хабы:
+12
Комментарии11

Публикации

Истории

Работа

Swift разработчик
30 вакансий
iOS разработчик
22 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн