Особенности Swift
В рамках Mobile Camp Яндекса наш коллега Денис Лебедев представил доклад о новом языке программирования Swift. В своем докладе он затронул особенности взаимодействия с Objective-C, рассказал про фичи языка, которые показались ему наиболее интересными. А также про то куда сходить на Github, и какие репозитории посмотреть, чтобы понять, что со Swift можно делать в реальном мире.Разработка Swift началась в 2010 году. Занимался ей Крис Латтнер. До 2013 процесс шел не очень активно. Постепенно вовлекалось все больше людей. В 2013 году Apple сфокусировалась на разработке этого языка. Перед презентацией на WWDC о Swift знало порядка 200 человек. Информация о нем хранилась в строжайшем секрете.
Swift — мультипарадигменный язык. В нем есть ООП, можно пробовать некоторые функциональные вещи, хотя адепты функционального программирования считают, что Swift немного не дотягивает. Но, мне кажется, что такая цель и не ставилась, все-таки это язык для людей, а не для математиков. Можно писать и в процедурном стиле, но я не уверен, что это применимо для целых проектов. Очень интересно в Swift все устроено с типизацией. В отличие от динамического Objective-C, она статическая. Также есть вывод типов. Т.е. большинство деклараций типов переменных можно просто опустить. Ну и киллер-фичей Swift можно считать очень глубокое взаимодействие с Objective-C. Позже я расскажу про runtime, а пока ограничимся тем, что код из Swift можно использовать в Objective C и наоборот. Указатели привычные всем разработчикам на Objective-С и С++ в Swift отсутствуют.Перейдем к фичам. Их достаточно много. Я для себя выделил несколько основных.
Namespacing. Все понимают, проблему Objective-C — из-за двухбуквенных и трехбуквенных классов часто возникают коллизии имен. Swift решает эту проблему, вводя очевидные и понятные всем нэймспейсы. Пока они не работают, но к релизу все должны починить. Generic classes & functions. Для людей, которые писали на С++, это достаточно очевидная вещь, но для тех, кто сталкивался в основном с Objective-C, это достаточно новая фича, с которой будет интересно поработать. Named/default parameters. Именованными параметрами никого не удивишь, в Objective-C они уже были. А вот параметры по умолчанию — очень полезная штука. Когда у нас метод принимает пять аргументов, три из которых заданы по умолчанию, вызов функции становится гораздо короче. Functions are first class citizens. Функции в Swift являются объектами первого порядка. Это означает, что их можно передавать в другие методы как параметры, а также возвращать их из других методов. Optional types. Необязательные типы — интересная концепция, которая пришла к нам в слегка видоизмененном виде из функционального программирования. Рассмотрим последнюю фичу немного подробнее. Все мы привыкли, что в Objective-C, когда мы не знаем, что вернуть, мы возвращаем nil для объектов и -1 или NSNotFound для скаляров. Необязательные типы решают эту проблему достаточно радикально. Optional type можно представить как коробку, которая либо содержит в себе значение, либо не содержит ничего. И работает это с любыми типами. Предположим, что у нас есть вот такая сигнатура: (NSInteger) indexOfObjec: (id)object; В Objective-C неясно, что возвращает метод. Если объекта нет, то это может быть -1, NSNotFound или еще какая-нибудь константа известная только разработчику. Если мы рассмотрим такой же метод в Swift, мы увидим Int cj знаком вопроса: func indexOF (object: AnyObject) → Int? Эта конструкция говорит нам, что вернется либо число, либо пустота. Соответственно, когда мы получили запакованный Int, нам нужно его распаковать. Распаковка бывает двух видов: безопасная (все оборачивается в if/else) и принудительная. Последнюю мы можем использовать только если мы точно знаем, что в нашей воображаемой коробке будет значения. Если его там не окажется, будет крэш в рантайме.Теперь коротко поговорим про основные фичи классов, структур и перечислений.Главное отличие классов от структур заключается в том, что они передаются по ссылке. Структуры же передаются по значению. Как говорит нам документация, использование структур затрачивает гораздо меньше ресурсов. И все скалярные типы и булевы переменные реализованы через структуры.
Перечисления хотелось бы выделить отдельно. Они абсолютно отличаются от аналогов в C, Objective-C и других языках. Это комбинация класса, структуры и даже немного больше. Чтобы показать, что я имею в виду, рассмотрим пример. Предположим, что я хочу реализовать дерево с помощью enum. Начнем с небольшого перечисления с тремя элементами (пустой, узел и лист):
enum Tree {
case Empty
case Leaf
case Node
}
Что с этим делать пока неясно. Но в Swift каждый элемент enum может нести какое-то значение. Для этого мы у листа добавим Int, а у узла будет еще два дерева:
enum Tree {
case Empty
case Leaf (Int)
case Node (Tree, Tree)
}
Но так как Swift поддерживает генерики, мы добавим в наше дерево поддержку любых типов:
enum Tree
enum Tree { case Empty case Leaf (Int) case Node (Tree, Tree)
func depth
func depth () → Int {
func _depth
func depth () → Int {
func _depth
func filter(…) → Bool) →
FilterSequenceView
func reverce(source: C) →
ReverseView
var и = 3 b += 1
let a = 3 a += 1 // error Тут начинается интересная вещь. Например, если мы посмотрим на словарь, который объявлен с помощью директивы let, то при попытке изменения ключа или добавления нового, мы получим ошибку. let d = [«key»: 0] d = [«key»] = 3 //error d.updateValue (1, forKey: «key1») //error С массивами все обстоит несколько иначе. Мы не можем увеличивать размер массива, но при этом мы можем изменять любой из его элементов. let c = [1, 2, 3] c[0] = 3 // success c.append (5) // fail На самом деле это очень странно, при попытке разобраться, в чем дело, выяснилось, что это подтвержденный разработчиком языка баг. В ближайшем будущем он будет исправлен, т.к. это действительно очень странное поведение.Расширения в Swift очень похожи на категории из Objective-C, но больше проникают в язык. В Swift не нужно писать импорты: мы можем в любом месте в коде написать расширение, и оно подхватится абсолютно всем кодом. Соответственно, тем же образом можно расширять структуры и енамы, что тоже иногда бывает удобно. При помощи расширений можно очень хорошо структурировать код, это реализовано в стандартной библиотеке.
struct: Foo { let value: Int }
extension Foo: Printable { var description: String { get {return «Foo»} } }
extension Foo: Equatable {
}
func ==(lhs: Foo, rhs: Foo) → Bool { return lhs.value == rhs.value } Далее поговорим о том, чего в Swift нет. Я не могу сказать, что чего-то конкретного мне не хватает, т.к. в продакшене я его пока не использовал. Но есть вещи, на которые многие жалуются.Preprocessor. Понятно, что если нет препроцессора, то нет и тех крутых макросов, которые генерят за нас очень много кода. Также затрудняется кроссплатформенная разработка. Exceptions. Механизм эксепшенов полностью отсутстсует, но можно создаст NSException, и рантайм Objective-C все это обработает. Access control. После прочтения книги о Swift многие пришли в замешательство из-за отсутствия модификаторов доступа. В Objective-C этого не было, все понимали, что это необходимо, и ждали в новом языке. На самом деле, разработчики просто не успели имплементировать модификаторы доступа к бета-версии. В окончательном релизе они уже будут. KVO, KVC. По понятным причинам нет Key Value Observing и Key Value Coding. Swift — статический язык, а это фичи динамичесих языков. Compiler attributes. Отсутствуют директивы компилятора, которые сообщают о deprecated-методах или о том, есть ли метод на конкретной платформе. performSelector. Этот метод из Swift полностью выкосили. Это достаточно небезопасная штука и даже в Objective-C ее нужно использовать с оглядкой. Теперь поговорим о том, как можно мешать Objective-C и Swift. Все уже знают, что из Swift можно вызвать код на Objective-C. В обратную сторону все работает точно так же, но с некоторыми ограничениями. Не работают перечисления, кортежи, обобщенные типы. Несмотря на то, что указателей нет, CoreFoundation-типы можно вызывать напрямую. Для многих стала расстройством невозможность вызывать код на С++ напрямую из Swift. Однако можно писать обертки на Objective-C и вызывать уже их. Ну и вполне естественно, что нельзя сабклассить в Objective-C нереализованные в нем классы из Swift.Как я уже говорил выше, некоторые типы взаимозаменяемы:
NSArray < - > Array; NSDictionary < - > Dictionary; NSNumber — > Int, Double, Float. Приведу пример класса, который написан на Swift, но может использоваться в Objective-C, нужно лишь добавить одну директиву:
@objc class Foo { int (bar: String) { /*…*/} } Если мы хотим, чтобы класс в Objective-C имел другое название (например, не Foo, а objc_Foo), а также поменять сигнатуру метода, все становится чуточку сложнее: @objc (objc_Foo) class Foo{ @objc (initWithBar:) init (bar: String) { /*…*/} } Соответственно, в Objective-C все выглядит абсолютно ожидаемо: Foo *foo = [[Foo alloc] initWithBar:@«Bar»]; Естественно, можно использовать все стандартные фреймворки. Для всех хедеров автоматически генерируется их репрезентация на Swift. Допустим, у нас есть функция convertPoint: — (CGPoint)convertPoint:(CGPoint)point toWindow:(UIWindow *)window Она полностью конвертируется в Swift с единственным отличием: около UIWindow есть восклицательный знак. Это указывает на тот самый необязательный тип, про который я говорил выше. Т.е. если там будет nil, и мы это не проверим, будет крэш в рантайме. Это происходит из-за того, что когда генератор создает эти хедеры, он не знает, может быть там nil или нет, поэтому и ставит везде эти восклицательные знаки. Возможно, скоро это как-нибудь поправят. finc convertPoint (point: CGPoint, toWindow window: UIWindow!) → GCPoint Подробно, говорить о внутренностях и перформансе Swift пока рано, так как неизвестно, что из текущего рантайма доживет до первой версии. Поэтому пока что коснемся этой темы лишь поверхностно. Начнем с того, что все Swift-объекты — это объекты Objective-C. Появляется новый рутовый класс SwiftObject. Методы теперь хранятся не с классами, а в виртуальных таблицах. Еще одна интересная особенность — типы переменных хранятся отдельно. Поэтому декодировать классы налету становится чуть сложнее. Для кодирования метаданных методов используется подход называемый name mangling. Для примера посмотрим на класс Foo с методом bar, возвращающим Bool: class Foo { func bar () → Bool { return false } } Если мы посмотрим в бинарник, для метода barмы увидим сигнатуру следующего вида: _TFC9test3Foo3barfS0_FT_Sb. Тут у нас есть Foo с длиной 3 символа, длина метода также 3 символа, а Sb в конце означает, что метод возвращает Bool. C этим связана не очень приятная штука: дебаг-логи в XCode все попадает именно в таком, виде, поэтому читать их не очень удобно.Наверное все уже читали про то, что Swift очень медленный. По большому счету это так и есть, но давайте попробуем разобраться. Если мы будем компилировать с флагом -O0, т.е. без каких-либо оптимизаций, то Swift будет медленнее С++ от 10 до 100 раз. Если компилировать с флагом -O3, мы получим нечно в 10 раз медленнее С++. Флаг -Ofast не очень безопасен, так как отключает в рантайме проверки переполнения интов и т.п. В продакшене его лучше не использовать. Однако он позволяет повысить производительность до уровня С++.Нужно понимать, что язык очень молодой, он все еще в бете. В будущем основные проблемы с быстродействием будут фикститься. Кроме того, за Swift тянется наследие Objective-C, например, в циклах есть огромное количество ретэйнов и релизов, которые в Swift по сути не нужны, но очень тормозят быстродействие.Дальше я буду рассказывать про не очень связанные друг с другом вещи, с которыми я сталкивался в процессе разработки. Как я уже говорил выше, макросы не поддерживаются, поэтому единственный способ сделать кроссплатформенную вьюшку выглядит следующим образом:
#if os (iOS) typealias View = UView #else typealias View = NSView #endif
class MyControl: View { } Этот if — это не совсем препроцессор, а просто конструкция языка, которая позволяет проверить платформу. Соответственно, у на есть метод, который нам возвращает, на какой мы платформе. В зависимости от этого мы делаем алиас на View. Таким образом мы создаем MyControl, который будет работать и на iOS и на OS X.Следующая фича — сопоставление с образцом — мне очень нравится. Я немного увлекаюсь функциональными языками, там она используется очень широко. Возьмем для примера задачу: у нас есть точка на плоскости, и мы хотим понять, в каком из четырех квадрантов она находится. Все мы представляем, что это будет за код в Objective-C. Для каждого квадранта у нас будут вот такие абсолютно дикие условия, где мы должны проверять попадают ли x и y в эти рамки:
let point = (0, 1)
if point.0 >= 0 && point.0 <= 1 && point.1 >= 0 && point.1 <= 1 { println("I") } ... Swift нам в этом случае нам дает несколько удобных штук. Во-первых, у нас появляется хитрый range-оператор с тремя точками. Соответственно, case может проверить, попадает ли точка в первый квадрант. И весь код будет выглядеть примерно таким образом: let point = (0, 1)
switch point { case (0, 0) println («Point is at the origin») case (0…1, 0…1): println («I») case (-1…0, 0…1): println («II») case (-1…0, -1…0): println («III») case (0…1, -1…0): println («IV») default: println («I don’t know») } На мой взгляд это в десятки раз более читаемо, чем то, что может нам предоставить Objective-C.В Swift есть еще одна абсолютно нишевая штука, которая также пришла из функциональных языков программирования — function currying:
func add (a: Int)(b: Int) → Int { return a + b }
let foo = add (5)(b: 3) // 8
let add5 = add (5) // (Int) → Int let bar = add (b: 3) // 8 Мы видим, что у нас есть функция add с таким хитрым объявлением: две пары скобок с параметрами вместо одной. Это дает нам возможность либо вызвать эту функцию почти что как обычную и получить результат 8, либо вызвать ее с одним параметром. Во втором случае происходит магия: на выходе мы получаем функцию, которая принимает Int и возвращает тоже Int, т.е. мы частично применили нашу функцию add к пятерке. Соответственно, мы можем потом применить функцию add5 с тройкой и получить восьмерку.Как я уже говорил, препроцессор отсутствует, поэтому даже реализовать assert — нетривиальная штука. Предположим, что у нас есть задача написать какой-нибудь свой assert. На дебаг мы его можем проверить, но чтобы код, который в ассерте не выполнится, мы должны передать его как замыкание. Т.е. мы видим, что у нас 5% 2 в фигурных скобках. В терминологии Objective-C — это блок.
func assert (condition:() → Bool, message: String) { #if DEBUG if! condition () { println (message) } #endif }
assert ({5% 2 == 0},»5 isn’t an even number.») Понятно, что ассерты так использовать никто не будет. Поэтому в Swift есть автоматические замыкания. В декларации метода мы видим @autoclosure, соответственно, первый аргумент оборачивается в замыкание, и фигурные скобки можно не писать. func assert (condition: @auto_closure () → Bool, message: String) { #if DEBUG if! condition () { println (message) } #endif }
assert (5% 2 == 0,»5 isn’t an even number.»)
Еще одна незадокументированная, но очень полезная вещь — явное преобразование типов. Swift — типизированный язык, поэтому как в Objective-C совать объекты с id-типом мы не можем. Поэтому рассмотрим следующий пример. Допустим у меня есть структура Box, которая в получает при инициализации какое-то значение, изменять которо нельзя. И у нас есть запакованный Int — единица.
struct Box
init (_ value: T) { _value = value } }
let boxedInt = Box (1) //Box
foo (boxedInt) //success Статическая типизация языка также не позволяет нам бегать по классу и подменять методы, как это можно было делать в Objective-C. Из того, что есть сейчас, мы можем только получить список свойств объекта и вывести их значения на данный момент. Т.е. информации о методах мы получить не можем. struct Foo { var str = «Apple» let int = 13
func foo () { } }
reflect (Foo ()).count // 2
reflect (Foo ())[0].0 // «str»
reflect (Foo ())[0].1summary // «Apple»
Из свифта можно напрямую вызывать С-код. Эта фича не отражена в документации, но может быть полезна.
@asmname («my_c_func»)
func my_c_func (UInt64, CMutablePointer
BDD Testing framework: Quick. Это первое, чего всем не хватало. Фреймворк активно развивается, постоянно добавляются новые матчеры. Reactive programming: RXSwift. Это переосмысление ReactiveCocoa при помощи конструкций, предоставляемых свифтом. Model mapping: Crust. Аналог Mantle для Swift. Позволяет мапить JSON-объекты в объекты свифта. Используется многие интересные хаки, которые могут быть полезны в разработке. Handy JSON processing: SwiftyJSON. Это очень небольшая библиотека, буквально 200 строк. Но она демонстрирует всю мощь перечислений.