Симметрическая разность возможностей Swift и Objective-C

Симметрическая разность возможностей Swift и Objective-C

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

Кроме того, рассказывая о новых возможностях, которые Swift привнёс в разработку, я постарался не забыть упомянуть то, что он потерял по сравнению с Objective-C.

Для каждого пункта я попытался кратко изложить суть отличия, не вдаваясь в детали, а примеры кода — наоборот сделать подробными. В них я прокомментировал все нюансы, даже те, которые не имеют непосредственного отношения к рассматриваемому отличию.

На момент написания статьи текущая версия Swift — 3.0.1.

1. Классы, структуры и перечисления

Классы в Swift не имеют одного общего предка, вроде NSObject в Objective-C. Более того, классы могут не иметь предка вообще.

Структуры в Swift почти настолько же функциональны как классы. Они, как и классы, могут иметь статические и обычные свойства и методы, инициализаторы, индексы (subscripts), расширения и могут реализовывать протоколы. От классов они отличаются тем, что передаются по значению и не имеют наследования.

Перечисления в Swift могут не иметь под собой значений.

Но, если значения есть, то они могут быть не только целыми числами, но и вещественными числами, и строками, и символами. Экземпляры перечислений автоматически не приводятся к типам внутренних значений, поэтому для доступа к ним следует использовать свойство rawValue .

Если перечисление не имеет rawValue , то каждый case перечисления может иметь собственные ассоциированные значения. Их может быть несколько, и они могут быть любых типов.

Перечисления, также как и структуры, передаются по значению. И они имеют те же, приведённые выше, возможности, кроме хранимых свойств. Свойства у перечислений могут быть только вычисляемыми (computed properties).

Подробнее о перечислениях: [1]

Эта богатая функциональность структур и перечислений позволяет нам использовать их вместо классов там, где значения уместнее, чем объекты. Целью этого разделения является упрощение архитектуры приложения. Подробнее об управлении сложностью: [2]

2. Типы функции, методов и замыканий

В Swift функции, методы и замыкания — это first class citizens, то есть они имеют типы и могут быть сохранены в переменных и переданы как параметр в функцию. Типы функций, методов и замыканий определяются только возвращаемым и принимаемыми значениями. То есть, если объявлена переменная определённого типа, то в неё можно сохранить как функцию, так и метод или замыкание. Экземпляры этих типов передаются по ссылке.

Подобная унификация сущностей привела к упрощению их использования. В Objective-C передача объекта и селектора или передача блока решали, в принципе, одну и ту же проблему. В Swift подобный API будет требовать что-то со определённым принимаемыми и возвращаемым значениями, а что именно туда будет передано: функция, метод или замыкание; не имеет значения.

3. Параметры по-умолчанию

Параметры функций и методов могут иметь значения по-умолчанию. Использование параметров по-умолчанию вместо нескольких функций/методов уменьшает количество кода, а меньше кода — меньше багов.

4. Optionals

Переменные никаких типов не могут принимать значения nil . В Swift используется специальный тип Optional , в который “оборачиваются” другие типы, если есть необходимость представить отсутствие значения.

Optional — это перечисления с двумя кейсами: none и some . Optional.some(Wrapped) содержит значение обёрнутого типа как ассоциированное значение. Optional.none эквивалентен литералу nil . В Optional может обернут как ссылочный тип так и передающийся по значению.

Для того чтобы обращаться к свойствам и методам optional значений, сначала нужно эти optional значения развернуть, то есть убедиться, что они не nil . Безусловно, этого можно достичь работая с optional значениями как с обычным перечислениями, например с помощью switch, но в Swift для этого есть более удобные конструкции конструкции: if let , guard let else ; операторы: ? , ! , ?? .

Подобные ограничения делают сложным неожиданное попадание на nil значение, что делает Swift код более надёжным.

5. Вложенные типы

В Swift можно объявлять вложенные типы, то есть классы, структуры и перечисления могут быть объявлены внутри друг друга.

Функции тоже можно объявлять внутри других функций. Но фактически внутренние функции — это замыкания, и они могут захватывать контекст внешней функции.

6. Кортежи

Ещё новые типы в Swift — это кортежи. Кортежи позволяют объединять несколько значений любых типов в одно составное значение. Кортежи передаются по значению.

7. Getters, setters and property observers

В отличие от Objective-C, в Swift getter и setter можно определять только для вычисляемых свойств. Конечно, для хранимых свойств как getter и setter можно использовать методы или вычисляемое свойство.

Для задач, решение которых требует отслеживать изменение значения свойства, появился новый механизм — property observers. Их можно определять для любого хранимого свойства. Они бывают двух видов: willSet (вызывается перед изменением значением свойства) и didSet (вызывается сразу после установки нового значения).

Следует заметить, что для структур property observers вызываются не только на непосредственное изменение значения свойства, для которого они объявлены, но и на изменение вложенных свойств любой глубины.

Для ленивой инициализации, которую в Objective-C можно реализовать через getter, в Swift есть модификатор свойств lazy .

8. Изменяемость свойств и коллекций

В Swift свойства типов могут быть константами. Причём, если тип свойства, объявленного как константа, является классом, то есть типом передающимся по ссылке, то неизменяемой будет только сама ссылка. То есть нельзя будет присвоить новый объект этому свойству, а изменять свойства этого объекта — можно. Для типов, передающихся по значению, любое изменение будет недопустимо.

Так как в Swift все коллекции являются структурами, то их изменяемость определяется не типом, как в Objective-C, а способом объявления — константа или переменная. Коллекций в стандартной библиотеке три: массив, множество и словарь.

9. Протоколы с ассоциированными типами

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

В то время как обычные протоколы можно использовать как конкретный тип:

Протоколы с ассоциированными типами так использовать нельзя.

Как видно из сообщения об ошибке, протоколы с ассоциированными значениями можно использовать только как ограничение на generic тип.

С некоторыми размышления на тему, почему это так и как с этим жить, можно ознакомиться здесь: [3].

В целом про протоколы с ассоциированными значениями неплохо рассказано в этой серии статей: [4].

10. Расширения протоколов

Расширения (extensions) классов, структур и перечислений в Swift в основном похожи на категории и расширения из Objective-C, то есть они позволяют добавлять поведение типу, даже если нет доступа к его исходному коду. Расширения типов позволяют добавлять вычисляемые свойства, методы, инициализаторы, индексы (subscripts), вложенные типы и реализовывать протоколы.

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

Кроме того, в расширении протокола можно реализовывать методы, которых нет в требованиях протокола, и реализующие протокол типы эти методы тоже получат. Но использовать расширения протоколов таким образом следует с осторожностью из-за статического связывания. Подробнее здесь: [5].

Более того можно конкретизировать расширения протоколов так, чтобы не все типы реализующие протокол получили реализацию по умолчанию. Условия могут требовать чтобы тип наследовался от определённого класса или реализовывал определённые протоколы. Условия могут накладываться на сам тип, реализующий протокол и на ассоциированные типы. В случае, если разные расширения предоставляют реализацию одного и того же метода, и тип удовлетворяет условиям нескольких расширений, то он получит ту реализацию, условие расширения которой было более конкретно. Если такого нет, то тип не получит никакой реализации.

В целом о расширении протоколов можно посмотреть в WWDC15 “Protocol-Oriented Programming in Swift” by Dave Abrahams [6]

11. Generics

В отличие от Objective-C, в Swift generic могут быть не только классы, но и структуры, перечисления и функции.

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

12. Пространство имён

В Objective-C для избежания конфликтов имён приходится использовать префиксы в названиях классов и протоколов.

В Swift у каждого модуля есть своё пространство имён, и в случае пересечения названий типов к нужному можно обратиться через имя его модуля. Возможности объявить своё пространство имён, чтобы разделить типы внутри одного модуля, пока нет.

13. Контроль доступа

В Objective-C контроль доступа осуществляется разнесением интерфейса по двум файлам. Интерфейсы публичных свойств и методов указываются в заголовочном файле, а интерфейсы приватных — в файле реализации.

В Swift нет разделения объявления типа на два файла, и контроль доступа осуществляется с помощью специальных модификаторов.

  • open — доступ из этого модуля и из модулей импортирующих этот модуль.
  • public — полноценный доступ из этого модуля, а из модулей импортирующих этот модуль доступ без возможности наследования классов и переопределения методов.
  • internal — доступ только из этого модуля.
  • fileprivate — доступ только из этого файла.
  • private — доступ только из этого объявления или расширения.

14. Обработка ошибок

В Objective-C используется два механизма для обработки ошибок: NSException и NSError . Механизм исключений с NSException это бросание и ловля ошибок конструкциями @try , @catch , @finally ; а механизм с NSError это передача указателя на NSError* и последующая обработка установленного значения. Причём в Cocoa редко приходится ловить NSException , потому что NSException обычно используется для неисправимых ошибок, а для ошибок, требующих обработки, используется NSError .

В Swift есть нативный механизм обработки ошибок do-try-catch , который заменил NSError . Следует заметить что в этот механизме нет блока finally . Вместо него следует использовать блок defer , код которого исполняется при выходе из scope, и он, в принципе, не связан с обработкой ошибок и может быть использован в любом месте.

Что касается NSException , то, из-за совместимости с Objective-C, они работают, но ловить их в Swift нельзя.

15. Управление памятью

В Swift, как и в Objective-C, используется подсчёт ссылок, но автоматический подсчёт ссылок отключить нельзя. А при работе с низкоуровневым процедурным API все возвращаемые объекты оборачиваются в структуру Unmanaged . Счётчиком ссылок такого объекта можно управлять вручную через методы структуры: retain() , release() , autorelease() ; но, чтобы получить доступ к такому объекту нужно развернуть его, передав управление подсчётом ссылок Swift’у. Для этого есть два метода: takeRetainedValue() — возвращает ссылку с декрементом счётчика, и takeUnretainedValue() — просто возвращает ссылку.

Подробнее об Unmanaged : [7]

16. Потокобезопасность

В Swift пока не никакого нативного механизма для потокобезопасности. Нет модификаторов свойств atomic и nonatomic из Objective-C. Впрочем, доступны как примитивы синхронизации вроде семафоров и мьютексов, так и Grand Central Dispatch и NSOperation из Cocoa.

17. Препроцессор

В отличие от Objective-C препроцессора в Swift нет. Впрочем Swift код может быть скомпилирован основываясь на условии вычисления конфигураций сборки (build configurations). Эти конфигурации могут учитывать логические фляги компилятора ( -D <#flag#> ) и результат специальных функций, которые могут проверять ОС — os() с аргументом из списка: OSX , iOS , watchOS , tvOS , Linux ; архитектуру — arch() : x86_64 , arm , arm64 , i386 ; версию языка — swift() : >= и номер версии. Для этого используются директивы: #if , #elseif , #else , #endif . Apple docs.

18. Библиотеки другого языка

Можно ли использовать Objective-C библиотеки и фреймворки в Swift проектах? Да, до тех пор пока они не требуют доступа в runtime к вашим чистым Swift классам. Например, OCMock работает только со Swift классами, которые наследуются от NSObject .

Можно ли использовать Swift библиотеки и фреймворки в Objective-C проектах? Только если они спроектированы таким образом, чтобы поддерживать Objective-C. Чтобы Swift классы были видны из Objective-C, они должны наследоваться от NSObject . Уникальные для Swift возможности не будут доступны. Apple docs.

Динамизм Objective-C

Когда мы говорим о динамизме Objective-C мы имеем в виду следующие базовые возможности [8]:

  1. Приложение может узнавать свою структуру в runtime. Например, какие методы и свойства есть у объекта или получение ссылки на протокол, класс или метод из строки.
  2. Приложение может что-то делать основываясь на том, что оно знает о своей структуре. Например, создавать экземпляры класса, имя которого было неизвестно на этапе компиляции, или обращаться к методам и свойствам, которые тоже были не известны при компиляции.
  3. Приложение может изменять свою структуру. Например, в runtime добавлять методы классам или объявлять новые классы.

Используя эти возможности, в Cocoa были реализованы множество полезных механизмов. В Swift нет такого динамизма, но когда мы разрабатываем под iOS, мы всё равно стоим на плечах Cocoa и можем использовать эти возможности, правда с некоторыми ограничениями.

Но ведь Swift это open source язык, который существует не только в Apple экосистеме, где есть Cocoa. Эти возможности были бы полезны и там. Кроме того, хоть в ближайшие года, скорее всего, Cocoa никуда не уйдёт, можно предположить, что когда-нибудь в будущем Apple будут заменять фреймворки Cocoa чем-то новым, написанном на чистом Swift. Как они будут решать те проблемы, которые в Objective-C решались его динамизмом? Рассмотрим некоторые такие, базирующиеся на динамизме, возможности, как они используются в Swift + Cocoa, и какие альтернативы есть в чистом Swift.

19. “Target/action” messaging

“Target/action” messaging используется для отправления команд из интерфейса в код. Эта возможность объявить метод у объекта в responder chain и одним движением соединить его с UI элементом в Interface Builder в 1988 году стала выдающимся улучшением по сравнению с одной монолитной функцией, которая вызывалась при любом действии от пользователя.

Swift + Cocoa. Все responder классы наследуются от UIResponder из UIKit, так что всё, конечно, работает.

В чистом Swift нет механизмов самоанализа для реализации подобной возможности. Без динамизма не получиться найти какой объект в responder chain реализует определёный в runtime метод и вызвать его. [9]

20. Key-Value Coding

Возможность обращаться к свойствам объектов, используя строки как идентификаторы.

Swift + Cocoa. В Swift KVC работает только для классов, которые наследуются от NSObject .

Альтернативы в чистом Swift? Только read-only, через отражения (reflection) используя структуру Mirror . [10]

21. Key-Value Observing

Возможность “подписаться” на обновления какого-либо свойства.

Swift + Cocoa. KVO работает только если и наблюдатель, и наблюдаемый объект наследуются от NSObject , и наблюдаемое свойство отмечено как dynamic , для предотвращения статической оптимизации.

Нативных аналогов в Swift нет, но, в принципе, реализуемо, например, с помощью property observers. Любая реализация: своя или уже готовая библиотека, например Observable-Swift, добавляет при использовании лишний код, когда в Cocoa KVO всё работало как есть.

22. NotificationCenter

Возможность одним объектам подписываться на оповещения, а другим отправлять эти оповещения.

Swift + Cocoa. Использование API с селектором накладывает следующие ограничения. Наблюдатель должен быть экземпляром класса (не структурой и не перечислением) и метод, вызываемый при получении уведомления должен быть доступен для Objective-C (класс наследоваться от NSObject или метод отмечен @objc ). От этих ограничений можно избавиться, используя API с замыканием, но он всё равно возвращает объект-токен типа NSObjectProtocol , с помощью которого следует отписываться от оповещений. Так что привязка к Cocoa пока остаётся.

Чистый Swift. Для реализации шаблона “Наблюдатель” динамизм не нужен, так что аналог в чистом Swift может быть реализован. Более того, потеря префикса NS в названии говорит о том, что следует ожидать, что NotificationCenter будет добавлен в Swift Foundation, то есть отбросит Cocoa динамизм.

23. UndoManager

Возможность регистрировать изменения как обратимые, чтобы позволить пользователю перемещаться назад и вперёд по ним.

Swift + Cocoa. Существует 3 способа регистрировать изменения: с помощью селектора, через NSInvocation или через замыкание. Все три способа применимы только для экземпляров классов. Для способа с селектором есть дополнительное ограничение: то же, что и описано выше про NotificationCenter : метод должен быть доступен для Objective-C. Способ с NSInvocation наиболее сильно опирается на динамизм, ведь, по сути, это перехват сообщений, так что класс должен наследоваться от NSObject . Способ с замыканием же доступен только начиная с iOS 9.

Чистый Swift. Существующий на данный момент UndoManager целиком опирается на динамизм, но, также как и NotificationCente r, реализация подобного функционала возможна в чистом Swift, и в будущем следует ожидать UndoManager в Swift Foundation.

Вообще, все API с селекторами, для которых динамизм не важен, то есть вызываемый код известен на момент компиляции, могут быть заменены на чистый Swift API, использующий замыкания или делегирование.

Для тех задач, которые, действительно зависят от динамизма Objective-C, решений в чистом Swift пока нет. Как Swift решит эти проблемы пока не ясно. Может быть это будет добавлением динамизма, может — совсем по-новому. Важно то, чтобы это решение не было узкоспециализированном, ведь когда Objective-C разрабатывался никто не знал о KVC, CoreData, bindings, HOM, UndoManager и т.д. И ничто из этого списка не требовало специальной поддержки языком/компилятором. Мы хотим, чтобы Swift не только решал эти проблемы, но и не ограничивал нас в разработке новых подобных возможностей. [11]

Статическая типизация в Swift

Так какие же преимущества даёт строгая статическая типизация в Swift? Ради чего был потерян динамизм?

1. Надёжность. Писать надёжный код в Swift проще, чем в Objective-C. Статическая типизация позволяет использовать систему типов, чтобы сделать нежелательное поведение невозможным. То есть отлавливать возможные ошибки на этапе компиляции. Несколько интересных приёмов можно подсмотреть здесь: [12] и [13].

2. Скорость работы? Безусловно, статическое связывание позволяет компилятору проводить более агрессивную оптимизацию, и чистый Swift работает быстрее, чем Objective-C. Но, вряд ли, в iOS разработке можно получить существенную выгоду от этого, пока практически всё базируется на динамическом Cocoa.

Говоря о системе типов, хочется отметить следующее. В Swift никакой тип автоматически не приводится к Bool , и оператор присваивания не возвращает значения.

Заключение

Кроме возможностей Swift и Objective-C различает ещё несколько моментов.

Во-первых, source stability. Хоть разработчики языка и попытались собрать максимальное количество изменений, нарушающих совместимость в версии 3.0, я думаю, ни для кого не секрет, что Swift находится ещё в активном развитии, и подобные изменения неизбежны. Впрочем, сейчас каждое предложение на такое изменение требует убедительного обоснования и подробного обсуждения. Также, для работы со старой кодовой базой в язык будут введён флаг компилятора для версии языка и расширение для Availability API. [14]

Во-вторых, Swift ещё нет ABI совместимости. Это значит, что динамические библиотеки скомпилированные в другой версии языка не будут работать.

Всё это означает, что перед тем как переносить свой проект на новую версию языка, вам придётся дождаться пока все Swift библиотеки и фреймворки, которые вы используете, тоже не перейдут на новую версию.

Обеспечение совместимости со Swift 3 и стабилизация ABI являются основными целями для Swift 4, который выйдет в конце 2017 года. [15]

Ну и напоследок, список моментов, которые тоже отличают Swift от Objective-C, но, по моему мнению, недостаточно влияют на разработку, чтобы внести их в основной список.

📎📎📎📎📎📎📎📎📎📎