Особливості 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 рядків. Але вона демонструє всю міць перерахувань.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.