Особенности Swift

d1c7a9400b299447f388ffaa39af4984.pngВ рамках 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 { case Empty case Leaf (T) case Node (Tree, Tree) } Объявление дерева будет выглядеть примерно так: let tree: Tree = .Node (.Leaf (1), .Leaf (1)) Здесь мы видим еще одну крутую фичу: мы можем не писать названия перечислений, потому что Swift выводит эти типы на этапе компиляции.У enum в Swift есть еще одна интересная особенность: они могут содержать в себе функции, точно так же, как в структурах и классах. Предположим, что я хочу написать функцию, которая вернет глубину нашего дерева.

enum Tree { case Empty case Leaf (Int) case Node (Tree, Tree)

func depth(t: Tree) → Int { return 0 } } Что мне в этой функции не нравится, так это то, что она принимает параметр дерева. Я хочу сделать так, чтобы функция просто возвращала мне значения, а мне ничего передавать бы не требовалось. Здесь мы воспользуемся еще одной интересной фичей Swift: вложенными функциями. Т.к. модификаторов доступа пока нет — это один из способов сделать функцию приватной. Соответственно, у нас есть _depth, которая сейчас будет считать глубину нашего дерева. enum Tree { case…

func depth () → Int { func _depth(t: Tree) → Int { return 0 } return _depth (self) } } Мы видим стандартный свитч, тут нет ничего свифтового, просто обрабатываем вариант, когда дерево пустое. Дальше начинаются интересные вещи. Мы распаковываем значение, которое хранится у нас в листе. Но так как оно нам не нужно, и мы хотим просто вернуть единицу, мы используем подчеркивание, которое означает, что переменная в листе нам не нужна. Дальше мы мы распаковываем узел, из которого мы достаем левую и правую части. Затем вызываем рекурсивно функцию глубины и возвращаем результат. По итогу у нас получается такое вот реализованное на enum дерево c какой-то базовой операцией. enum Tree { case Empty case Leaf (T) case Node (Tree, Tree)

func depth () → Int { func _depth(t: Tree) → Int { switch t { case .Empty: return 0 case .Leaf (let_): return 1 case .Node (let lhs, let rhs): return max (_depth (lhs), _depth (rhs)) } } return _depth (self) } } Интересная штука с этим enum заключается в том, что этот написанный им код, должен работать, но не работает. В текущей версии из-за бага enum не поддерживает рекурсивные типы. В будущем это все заработает. Пока для обхода этого бага используются разные хаки. Про один из них я расскажу чуть позже.Следующий пункт моего моего рассказа — это коллекции, представленные в стандартной библиотеке массивом, словарями и строкой (коллекция чаров). Коллекции, как и скаляры, являются структурами, они также взаимозаменяемы со стандартными foundation-типами, такими как NSDictionary и NSArray. Кроме того, мы видим, что по какой-то странной причине нет типа NSSet. Вероятно, им слишком редко пользуются. В некоторых операциях (например, filter и reverse) есть ленивые вычисления:

func filter(…) → Bool) → FilterSequenceView

func reverce(source: C) → ReverseView Т.е. типы FilterSequenceView и ReverseView — это не обработанная коллекция, а ее представление. Это говорит нам о том, что у этих методов высокая производительность. В том же Objective-C таких хитрых конструкций не встретишь, так как во времена создания этого языка о таких концепциях никто еще не думал. Сейчас lazy-вычисления проникают в языки программирования. Мне нравится эта тенденция, иногда это бывает очень эффективно.Следующую фичу заметили уже, наверное, все, кто как-то интересовался новым языком. Но я все равно про нее расскажу. В Swift есть встроенная неизменяемость переменных. Мы можем объявить переменную двумя способами: через var и let. В первом случае переменные могут быть изменены, во втором — нет.

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 { let _value: T

init (_ value: T) { _value = value } }

let boxedInt = Box (1) //Box Также у нас есть функция, которая принимает на вход Int. Соответственно, boxedInt мы туда передать не можем, т.к. компилятор нам скажет, что Box не конвертируется в Int. Умельцы немного распотрошили внутренности свифта и нашли функцию, позволяющую конвертировать тип Box в значение, которое он в себе скрывает: extension Box { @conversion Func __conversion () → T { return _value } }

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) → CInt; Swift, конечно, компилируемый язык, но это не мешает ему поддерживать скрипты. Во-первых, есть интерактивная среда выполнения, запускаемая при помощи команды xcrun swift. Кроме того, можно писать скрипты не на привычных скриптовых языках, а непосредственно на Swift. Запускаются они при помощи команды xcrun -i 'file.swift'.Напоследок я расскажу о репозиториях, на которые стоит посмотреть:

BDD Testing framework: Quick. Это первое, чего всем не хватало. Фреймворк активно развивается, постоянно добавляются новые матчеры. Reactive programming: RXSwift. Это переосмысление ReactiveCocoa при помощи конструкций, предоставляемых свифтом. Model mapping: Crust. Аналог Mantle для Swift. Позволяет мапить JSON-объекты в объекты свифта. Используется многие интересные хаки, которые могут быть полезны в разработке. Handy JSON processing: SwiftyJSON. Это очень небольшая библиотека, буквально 200 строк. Но она демонстрирует всю мощь перечислений.

© Habrahabr.ru