В рамках 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<T> {
case Empty
case Leaf(T)
case Node(Tree, Tree)
}
Объявление дерева будет выглядеть примерно так:
let tree: Tree<Int> = .Node(.Leaf(1), .Leaf(1))
Здесь мы видим еще одну крутую фичу: мы можем не писать названия перечислений, потому что Swift выводит эти типы на этапе компиляции.
У enum
в Swift есть еще одна интересная особенность: они могут содержать в себе функции, точно так же, как в структурах и классах. Предположим, что я хочу написать функцию, которая вернет глубину нашего дерева.
enum Tree {
case Empty
case Leaf(Int)
case Node(Tree, Tree)
func depth<T>(t: Tree<T>) -> Int {
return 0
}
}
Что мне в этой функции не нравится, так это то, что она принимает параметр дерева. Я хочу сделать так, чтобы функция просто возвращала мне значения, а мне ничего передавать бы не требовалось. Здесь мы воспользуемся еще одной интересной фичей Swift: вложенными функциями. Т.к. модификаторов доступа пока нет – это один из способов сделать функцию приватной. Соответственно, у нас есть _depth
, которая сейчас будет считать глубину нашего дерева.
enum Tree<T> {
case …
func depth() -> Int {
func _depth<T>(t: Tree<T>) -> Int {
return 0
}
return _depth(self)
}
}
Мы видим стандартный свитч, тут нет ничего свифтового, просто обрабатываем вариант, когда дерево пустое. Дальше начинаются интересные вещи. Мы распаковываем значение, которое хранится у нас в листе. Но так как оно нам не нужно, и мы хотим просто вернуть единицу, мы используем подчеркивание, которое означает, что переменная в листе нам не нужна. Дальше мы мы распаковываем узел, из которого мы достаем левую и правую части. Затем вызываем рекурсивно функцию глубины и возвращаем результат. По итогу у нас получается такое вот реализованное на enum
дерево c какой-то базовой операцией.
enum Tree<T> {
case Empty
case Leaf(T)
case Node(Tree, Tree)
func depth() -> Int {
func _depth<T>(t: Tree<T>) -> 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<S :Sequence>(…) -> Bool) ->
FilterSequenceView<S>
func reverce<S :Collection …>(source: C) ->
ReverseView<C>
Т.е. типы 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<T> {
let _value : T
init (_ value: T) {
_value = value
}
}
let boxedInt = Box(1) //Box<Int>
Также у нас есть функция, которая принимает на вход 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<UInt64>) -> 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 строк. Но она демонстрирует всю мощь перечислений.
Автор: elcoyot