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

в 7:50, , рубрики: swift, Блог компании Яндекс, Программирование, разработка под iOS

Особенности 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<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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js