Симетрична різниця можливостей Swift і Objective-C

image
У цій статті я розповім про відмінність можливостей, які надають iOS-розробникам мови Swift і Objective-C. Безумовно, розробники, які цікавилися новою мовою від Apple, вже бачили чимало подібних статей, тому я вирішив акцентувати увагу на тих відмінностях, які дійсно впливають на процес розробки і на архітектуру програми. Тобто, ті відмінності, які слід знати, щоб використовувати мову максимально ефективно. Я спробував скласти найбільш повний список, що задовольняє цим критеріям.
Крім того, розповідаючи про нові можливості, які Swift привніс в розробку, я намагався не забути згадати те, що він втратив у порівнянні з Objective-C.
Для кожного пункту я спробував коротко викласти суть відмінності, не вдаючись у деталі, а приклади коду — навпаки зробити докладними. В них я прокоментував всі нюанси, навіть ті, які не мають безпосереднього відношення до розглянутого відмінності.
На момент написання статті поточна версія Swift — 3.0.1.

1. Класи, структури та перерахування
Класи в Swift не мають одного загального предка, начебто NSObject в Objective-C. Більш того, класи можуть не мати предка взагалі.
Структури в Swift майже настільки ж функціональні класи. Вони, як і класи, можуть мати статичні і звичайні властивості і методи, инициализаторы, індекси (subscripts), розширення і можуть реалізовувати протоколи. Від класів вони відрізняються тим, що передаються за значенням і не мають успадкування.
// Приклад структури, демонструє її можливості.
/* Клас визначається так само. 
Тобто, якщо в прикладі замінити слово struct на class, 
весь код залишиться коректним. */
struct Rocket {
// Властивість типу масив елементів типу Stage.
// Ключовим словом var визначається змінна, словом let - константа.
var stages: [Stage]

/* Статичне властивість типу Int. 
Тип визначається компілятором з ініціалізації. */
static let maxAllowedStages = 4

/* Метод, який не приймає аргументів і повертає значення.
Це скорочений запис для launch() -> Void або launch() -> ()
Де Void насправді typealias для ()
А () позначає порожній кортеж.
Так що насправді цей метод повертає порожній кортеж. */
func launch() {
// ...
}

// Статичний метод типу ([Stage]) -> Double
static func calculateHeight(for stages: [Stage]) -> Double {
/* Метод reduce визначений для колекцій в Swift. 
Він перетворює колекцію у одне значення.
Для цього він приймає початкове значення і замикання, 
яка викликається для кожного елемента колекції. 
Вона має, спираючись на вже аккумулированном значенні і 
елемент колекції, обчислити нове значення акумулятора. */
return stages.reduce(0) { (accumulator, stage) -> Double in
return accumulator + stage.height
}
}

/* Failable инициализатор, 
тобто той, який може замість примірника повернути nil.
Звичайні инициализаторы оголошуються без знака після слова init. */
init?(stages: [Stage]) {
if stages.count > Rocket.maxAllowedStages {
return nil
}
self.stages = stages
}

/* Індекс (subscript) дозволяє звертатися до об'єкта через квадратні дужки
як до масиву або до словника.
rocket[1] = stage */
/* Тут ми просто делегуємо індекс внутрішнього масиву, 
але можна написати свою логіку. */
/* Один тип може визначати кілька індексів, 
але вони повинні приймати різні типи. */
subscript (index: Int) -> Stage {
get {
return stages[index]
}
set(newValue) {
stages[index] = newValue
}
}
}

/* Оголосимо протокол, що визначає вимоги до об'єкту для транспортування на поїзді:
можливість бути розібраним по вагонах і зібраним назад. */
protocol TransportableByTrain {
func placeOnTrain() -> [Carriage]
init(train: [Carriage])
}

/* Розширення дозволяють додавати до існуючих класів, структур і перерахуваннями 
властивості, методи, инициализаторы, індекси, вкладені типи і реалізацію протоколів. */
extension Rocket: TransportableByTrain {
func placeOnTrain() -> [Carriage] {
return stages.map { stage in
Carriage return(content: stage)
}
}

init(train: [Carriage]){
let stages = train.map {
$0.content as! Stage
}
self.init(stages: stages)!
}
}

Перерахування в Swift можуть не мати під собою значень.
// Перерахування без rawValue.
enum LaunchState {
case preparing, ready, launching, failed, succeeded
}

Але, якщо значення є, то вони можуть бути не тільки цілими числами, а й речовими числами, і рядками та символами. Примірники перерахувань автоматично не приводяться до типів внутрішніх значень, тому для доступу до них слід використовувати властивість
rawValue
.
// Перерахування з rawValue.
enum LaunchEvent: Int {
case poweredOn = 1, fuelLoaded, oxidizerLoaded, countAutoSequenceStarted,
goForlaunchVerification, preLaunchChecks, pressurizePropellantTanks,
ignitionSequenceStart, liftoff
}
let lastEvent = LaunchEvent.liftoff
lastEvent.rawValue // 9
/* Перерахування з `rawValue` автоматично отримують failable инициализатор 
з відповідного типу. */
let firstEvent = LaunchEvent(rawValue: 1) //LaunchSequence.poweredOn
let nonexistentEvent = LaunchEvent(rawValue: 0) //nil

Якщо перерахування не має
rawValue
, то кожен case перерахування може мати власні асоційовані значення. Їх може бути кілька, і вони можуть бути будь-яких типів.
// Перерахування з асоційованими значеннями.
enum LaunchError {
case compromisedHullIntegrity(stage: Stage)
case engineMalfunction(engine: Engine, malfunction: Malfunction)
case unrecognizedError
}

Перерахування, також як і структури, передаються за значенням. І вони мають ті ж, наведені вище, можливості, крім збережених властивостей. Властивості у перерахувань можуть бути тільки обчислюваними (computed properties).
// Статичне обчислюване властивість у перерахування.
extension LaunchEvent {
static var sequence: [LaunchEvent] {
return Array(1...9).map { LaunchEvent(rawValue: $0)! }
}
}

Детальніше про перерахування: [1]
Ця багата функціональність структур і перерахувань дозволяє нам використовувати їх замість класів там, де значення доречніше, ніж об'єкти. Метою цього поділу є спрощення архітектури. Детальніше про управління складністю: [2]
2. Типи функцій, методів і замикань
В Swift функції, методи і замикання — це first class citizens, тобто вони мають типи і можуть бути збережені в змінних і передані як параметр в функцію. Типи функцій, методів і замикань визначаються тільки її обчислене і прийнятими значеннями. Тобто, якщо оголошена змінна певного типу, то в неї можна зберегти як функцію, так і метод або замикання. Примірники цих типів передаються по посиланню.
Подібна уніфікація сутностей привела до спрощення їх використання. В Objective-C передача об'єкта і селектора або передача блоку вирішували, в принципі, одну і ту ж проблему. В Swift подібний API буде вимагати що-то з певним приймаються і її обчислене значеннями, а що саме туди буде передано: функція, метод або замикання; не має значення.
/* Метод reduce, який ми використовували в calculateHeight має наступний тип.
(Result, (Result, Element) throws -> Result) rethrows -> Result
Але в цьому прикладі ми опустимо деталі, пов'язані з обробкою помилок: throw і
rethrows; а generic тип Result і асоційований тип Element замінимо на
конкретні типи Double і Stage.
З такими припущеннями можна сказати, що метод reduce має наступний вигляд.
(Double, (Double, Stage) -> Double) -> Double
Метод приймає два параметри: перший типу Double, а другий - замикання,
приймає Double і Stage, а повертає Double. І сам метод, у свою
чергу, теж повертає Double.*/

// Найбільш повний запис виклику цього методу з замиканням виглядає так:
let totalHeight = stages.reduce(0, { (accumulator: Double, stage: Stage) -> Double in
return accumulator + stage.height
})

/* Але зазвичай використовується більш короткий запис.
По-перше, типу методу reduce компілятор вже знає повертаються і приймаються 
значення параметра-замикання, так що їх можна опустити.
По-друге, якщо замикання - останній параметр у списку, то його можна винести
за дужки. */

let totalHeight = stages.reduce(0) { accumulator, stage in
return accumulator + stage.height
}

/* Подібна запис найбільш поширена. 
Але на цьому можливості скорочення не обмежені.
Можна не приводити своїх імен для параметрів замикання. В такому випадку
звертатися до них можна через $0, $1 і так далі по порядку.
Якщо замикання містить тільки один вираз, то можна опустити ключове
слово return. */
let totalHeight = stages.reduce(0) { $0 + $1.height }

/* Більш того, оператори в swift теж володіють тими ж типами, що і функції,
методи і замикання. 
У нашому прикладі оператор + не визначений для типу Double та Stage, але якщо б
ми з масиву ступенів [Stage] отримали б масив висот [Double], і вже у нього
викликали метод reduce, то другий параметр мав би тип (Double, Double) -> Double. 
А для Double оператор + визначений, так що замість замикання ми могли б просто
передати його. */
let totalHeight = stages.map{ $0.height }.reduce(0, +)

3. Параметри за замовчуванням
Параметри функцій і методів можуть мати значення за замовчуванням.
Використання параметрів за замовчуванням замість декількох функцій/методів зменшує кількість коду, а менше коду — менше багів.
enum Destination {
case lowEarthOrbit, geostationaryEarthOrbit, transLunarInjection
}

class RocketFactory {
/* Значення за замовчуванням зазначаються в оголошенні після типу параметра через
знак '='. */
func makeRocket(destination: Destination = Destination.lowEarthOrbit, 
payloadMass: Double = 6450) -> Rocket {
//...
}
}

let rocketFactory = RocketFactory()
// Параметри зі значеннями за замовчуванням можна опускати при виклику.
let soyuz = rocketFactory.makeRocket()
let protonM = rocketFactory.makeRocket(destination: Destination.geostationaryEarthOrbit)
let saturnV = rocketFactory.makeRocket(destination: Destination.transLunarInjection, payloadMass: 48600)

4. Optionals
Змінні ніяких типів не можуть приймати значення
nil
. В Swift використовується спеціальний тип
Optional
, який «обертаються» інші типи, якщо є необхідність представити відсутність значення.
Optional
— це перерахування з двома кейсами:
none
та
some
.
Optional.some(Wrapped)
містить значення обгорненого типу як асоційоване значення.
Optional.none
еквівалентний литералу
nil
.
В
Optional
може обгорнутий як посилальний тип так і передається за значенням.
struct Launchpad {
// Зручна запис для optional типу.
var rocket: Rocket?
/* Без синтаксичного цукру це б виглядало наступним чином:
var rocket: Optional<Rocket> */
}

Для того щоб звертатися до властивостей і методів optional значень, спочатку потрібно ці optional значення розгорнути, тобто переконатися, що вони не
nil
. Безумовно, цього можна досягти працюючи з optional значеннями як із звичайним перерахуваннями, наприклад з допомогою switch, але в Swift для цього є більш зручні конструкції конструкції:
if let
,
guard let else
; оператори:
?
,
!
,
??
.
/* найпростіший, але небезпечний спосіб розгорнути optional змінну - це
оператор '!'. Він поверне optional тип у випадку успіху, але викличе runtime error і
падіння додатка, якщо всередині nil. */
launchpad.rocket!.launch()

func start() {
/* Типовим способом розгортати optional значення є використання
конструкції if let. */
if let rocket = launchpad.rocket {
/* Таким чином, всередині успішної гілки ми отримуємо нову змінну 
не optional типу. */
rocket.launch()
} else {
// А в гілці else можемо обробити відсутність значення в optional змінної.
abortStart()
}
}

/* В ситуаціях, коли продовжувати виконання не має сенсу, якщо optional
значення дорівнює nil, для розгортання optional значень зручно використовувати
конструкцію guard let else. */
func start2() {
/* На відміну від if let вона оголошує нову змінну з не optional типом в поточному
контексті, а всередині else блоку слід обробити потрапляння на nil і вийти з
контексту. */
guard let rocket = launchpad.rocket else {
abortStart()
return
}
rocket.launch()
}

/* Є спосіб звертатися до властивостей і методів optional значення, не розгортаючи
його. Це оператор '?'. При такому виклику ми не отримуємо ніякої зворотного зв'язку про
наявності або відсутності значення.
Обчислене значення такого виклику завжди буде optional. */
launchpad.rocket?.launch()

var possibleLaunchpad: Launchpad?

// Таким чином можна пов'язувати кілька optional значень.
possibleLaunchpad?.rocket?.launch()
possibleLaunchpad?.rocket?.stages //Return type: [Stages]?

/* Ще для роботи з optional значеннями є оператор '??'. Він має два операнда.
Перший - optional значення, а другий - не optional того ж типу.
Якщо перший операнд не nil, то значення розгортається і повертається, інакше
повертається другий операнд. */
let certainRocket = possibleLaunchpad?.rocket ?? rocketFactory.makeRocket()

Такі обмеження роблять складним несподіване потрапляння на nil значення, що робить Swift код більш надійним.
5. Вкладені типи
В Swift можна оголошувати вкладені типи, тобто класи, структури та перерахування можуть бути оголошені всередині один одного.
/* Якщо б ми захотіли виділити запуск в окрему сутність, то змогли б
отримати щось подібне. */
struct Launch {
enum State {
case preparing, ready, launching, failed, succeeded
}
// Всередині контексту до вкладеного типу можна звертатися просто по імені.
var state: State = .preparing
}
/* Зовні до вкладеного типу, якщо він доступний, можна звертатися через ім'я
зовнішнього типу. */
let launchState: Launch.State

Функції теж можна оголошувати всередині інших функцій. Але фактично внутрішні функції — це замикання, і вони можуть захоплювати контекст зовнішньої функції.
6. Кортежі
Ще нові типи Swift — це кортежі. Кортежі дозволяють об'єднувати кілька значень будь-яких типів в одне складне значення. Кортежі передаються за значенням.
/* Задаючи тип кортежу, значенням можна давати імена, щоб надалі
звертатися за ним, а не по номеру. */
var launchEventMark: (event: LaunchEvent, timeMark: Int) = (.ignitionSequenceStart, 6600)
launchEventMark.event
launchEventMark.timeMark
// Втім, звернення за номером у такому разі не зникає.
launchEventMark.0
launchEventMark.1
/* Слід зауважити, що якщо ми оголосимо кортеж зі значеннями тих же типів, але без
назв, то отримаємо кортеж того ж типу і зможемо призначити попереднє
значення нової змінної. */
var anotherMark: (LaunchEvent, Int) = launchEventMark
anotherMark.0
anotherMark.event // error: type has no member 'event'

7. Getters, setters and property observers
На відміну від Objective-C, Swift getter і setter можна визначати тільки для обчислюваних властивостей. Звичайно, для збережених властивостей як getter і setter можна використовувати методи обчислюване або властивість.
// Реалізація getter'а і setter'а з допомогою обчислюваного властивості.
class ThrustController {
init(minThrust: Double, maxThrust: Double, currentThrust: Double) {
self.minThrust = minThrust
self.maxThrust = maxThrust
thrust = currentThrust
}

var minThrust: Double
var maxThrust: Double

private var _thrust = 0.0

var thrust: Double {
get {
return _thrust
}
set {
if newValue > maxThrust {
_thrust = maxThrust
} else if newValue < minThrust { //
_thrust = maxThrust
} else {
_thrust = newValue
}
}
/* За замовчуванням, присвоєне значення з setter'а доступно за іменем
newValue, але можна дати йому своє ім'я: */
// set(thrustInput) { ... }
}
}
/* Але, взагалі, обчислювані властивості зазвичай використовуються в ситуаціях, коли
значення може бути обчислено на підставі інших властивостей, а не для 
валідації значень. */

Для завдань, рішення яких вимагає відстежувати зміна значення властивості, з'явився новий механізм — property observers. Їх можна визначати для будь-якого зберігає властивості. Вони бувають двох видів:
willSet
(викликається перед зміною значенням властивості) і
didSet
(викликається відразу після установки нового значення).
protocol ThrustObserver: class {
func thrustWillChange(from oldValue: Double, to newValue: Double)
func thrustDidChange(from oldValue: Double, to newValue: Double)
}

class ThrustMeter {
weak var observer: ThrustObserver?

var thrust: Double = 0.0 {
willSet {
observer?.thrustWillChange(from: thrust, to: newValue)
}
didSet {
observer?.thrustDidChange(from: oldValue, to: thrust)
}
// Як і у випадку з set, імена newValue і oldValue можна замінити на свої.
// willSet(newThrust) { ... }
// didSet(oldThrust) { ... }
}
}

Слід зауважити, що для структур property observers викликаються не тільки на безпосереднє зміна значення властивості, для якого вони оголошені, але і на зміну вкладених властивостей будь-якої глибини.
Для ледачої ініціалізації, яку в Objective-C можна реалізувати через getter, Swift є модифікатор властивостей
lazy
.
8. Змінність властивостей і колекцій
В Swift властивості типів можуть бути константами. Причому, якщо тип властивості, оголошеного як константа, є класом, тобто типом передається по посиланню, то буде незмінною лише сама посилання. Тобто не можна буде надати новий об'єкт цій властивості, а змінювати властивості цього об'єкта — можна. Для типів, що передаються за значенням, будь-яка зміна буде неприпустимо.
class ThrustMeterClass {
var thrust: Double

init(thrust: Double) {
self.thrust = thrust
}
}

struct ThrustMeterStruct {
var thrust = 0.0
}

let thrustMeterClass = ThrustMeterClass(thrust: 0)
thrustMeterClass = ThrustMeterClass(thrust: 50) //Error
thrustMeterClass.thrust = 50 //OK

let thrustMeterStruct = ThrustMeterStruct(thrust: 0)
thrustMeterStruct = ThrustMeterStruct(thrust: 50) //Error
thrustMeterStruct.thrust = 50 //Error

Так як в Swift всі колекції є структурами, то їх змінність визначається не типом, як в Objective-C, а способом оголошення — константа або змінна. Колекцій в стандартній бібліотеці три: масив, безліч і словник.
let immutableArray = [1, 2, 3]
var mutableArray = [1, 2, 3]

9. Протоколи з асоційованими типами
У той час як звичайні протоколи практично нічим не відрізняються від аналогів з Objective-C, протоколи з асоційованими типами — це абсолютно нова конструкція в Swift. Протоколи можуть оголошувати асоційовані типи і використовувати їх як placeholder у своїх вимогах до методів і властивостей. А то, що реалізує цей протокол, вже повинен буде вказати який реальний тип буде використаний.
// Оголосимо кілька порожніх звичайних протоколів для наступних прикладів.
protocol Fuel {}
protocol Oxidizer {}

// Оголосимо кілька типів, які реалізують ці протоколи.
struct Hydrazine: Fuel {}
struct ChlorineTrifluoride: Oxidizer {}
struct Kerosene: Fuel {}
struct Oxygen: Oxidizer {}

// Протокол з асоційованими значеннями.
protocol Bipropellant {
/* На типами можна накладати вимоги реалізації
протоколів або успадкування класів. */
associatedtype TFuel: Fuel
associatedtype TOxidizer: Oxidizer

func burn(_ fuel: TFuel, with oxidizer: TOxidizer)
}

// Оголосимо тип з реалізацією такого протоколу.
struct KoxPropellant: Bipropellant {
/* Вказати компілятору, який саме конкретний тип буде використаний в цій
реалізації протоколу, можна з допомогою typealias. Але в даному випадку це не
обов'язково так як він може сам це вивести з сигнатури методу burn. */
typealias TOxidizer = Oxygen
typealias TFuel = Kerosene

func burn(_ fuel: Kerosene, with oxidizer: Oxygen) {
print("Burn of kerosene with oxygen.")
}
}

/* Протоколи також можуть успадковуватися від інших протоколів як це було і в
Objective-C. */
protocol Hypergolic: Bipropellant {}

struct HctPropellant: Bipropellant, Hypergolic {
typealias TOxidizer = ChlorineTrifluoride
typealias TFuel = Hydrazine

func burn(_ fuel: Hydrazine, with oxidizer: ChlorineTrifluoride) {
print("Burn of hydrazine with chlorine trifluoride.")
}
}

У той час як звичайні протоколи можна використовувати як конкретний тип:
struct AnyFuelTank {
/* В таку змінну можна зберегти значення будь-якого типу, що реалізує
протокол Fuel. */
var content: Fuel
}

var fuelTank = AnyFuelTank(content: kerosene)
fuelTank.content = hydrazine

Протоколи з асоційованими типами так використовувати не можна.
struct RocketEngine {
// Такий запис не компилируюется.
var propellant: Bipropellant
/* Error: Protocol 'Bipropellant' can only be used as a generic constraint
because it has Self or associated type requirements */
}

Як видно з повідомлення про помилку, протоколи з асоційованими значеннями можна використовувати тільки як обмеження на generic тип.
struct RocketEngine<TBipropellant: Bipropellant> {
var propellant: TBipropellant
}

З деякими роздуми на тему, чому це так і як з цим жити, можна ознайомитися тут: [3].
У цілому про протоколи з асоційованими значеннями непогано розказано в цій серії статей: [4].
10. Розширення протоколів
Розширення (extensions) класів, структур та перерахувань до Swift в основному схожі на категорії і розширення з Objective-C, тобто вони дозволяють додавати поведінка типу, навіть якщо немає доступу до вихідного коду. Розширення типів дозволяють додавати обчислювані властивості, методи, инициализаторы, індекси (subscripts), вкладені типи і реалізовувати протоколи.
Розширення протоколів є новою можливістю в Swift. Вони дозволяють надавати типами, що реалізує цей протокол, реалізацію властивостей і методів за замовчуванням. Тобто в розширенні протоколу описуються не вимоги, а вже конкретна реалізація, яку отримають типи, що реалізують цей протокол. Зрозуміло, типи можуть перекрити цю реалізацію. Подібна реалізація за замовчуванням дозволяє замінити необов'язкові вимоги протоколу, які в Swift існують тільки в рамках сумісності з Objective-C.
// Усі типи, що реалізують цей протокол, отримають цю імплементацію методу.
extension Bipropellant {
func burn(_ fuel: TFuel, with oxidizer: TOxidizer) {
print("Common burn.")
}
}

Крім того, у розширенні протоколу можна реалізовувати методи, яких немає у вимогах протоколу, і реалізують протокол типи ці методи теж отримають. Але використовувати розширення протоколів таким чином, слід з обережністю із-за статичного зв'язування. Детальніше тут: [5].
Більш того можна конкретизувати розширення протоколів так, щоб не всі типи реалізують протокол отримали реалізацію за замовчуванням. Умови можуть вимагати, щоб тип дістався у спадок від певного класу чи реалізовував певні протоколи. Умови можуть накладатися на сам тип, що реалізує протокол і на типами. У разі, якщо різні розширення надають реалізацію одного і того ж методу, і тип задовольняє умовам декількох розширень, то він отримає ту реалізацію, умова розширення якої було більш конкретно. Якщо такого немає, то тип не отримає ніякої реалізації.
/* Так як це розширення більш конкретизована, ніж попереднє, то типи
задовольняють вимозі отримають цю імплементацію методу. */
extension Bipropellant where Self.TOxidizer == Oxygen {
func burn(_ fuel: TFuel, with oxidizer: TOxidizer) {
print("Burn with oxygen as oxidizer.")
}
}

/* Можна накладати кілька вимог через кому. Вимоги
об'єднуються логічним В. */
extension Bipropellant where Self: Hypergolic, Self.TFuel == Hydrazine {
func burn(_ fuel: TFuel, with oxidizer: TOxidizer) {
print("Self-ignited burn of hydrazine.")
}
}

/* Так як згадані раніше типи KoxPropellant і HctPropellant визначають свої
реалізації методу burn, то вони не отримують реалізації з розширень
протоколу Bipropellant. */
let koxPropellant = KoxPropellant()
koxPropellant.burn(kerosene, with: oxygen) // Burn of kerosene with oxygen.

let hctPropelant = HctPropellant()
hctPropelant.burn(hydrazine, with: chlorineTrifluoride) // Burn of hydrazine with chlorine trifluoride.

// Але якщо для них не визначати метод burn, то вони отримали б такі реалізації.
koxPropellant.burn(kerosene, with: oxygen) // Burn with oxygen as oxidizer.
hctPropelant.burn(hydrazine, with: chlorineTrifluoride) // Self-ignited burn of hydrazine.

У цілому про розширення протоколів можна подивитися в WWDC15 «Protocol-Oriented Programming in Swift» by Dave Abrahams [6]
11. Узагальнення
На відміну від Objective-C, Swift generic можуть бути не тільки класи, але й структури, перерахування та функції.
В Swift умови на generic тип можуть накладатися ті ж умови, що і в Objective-C, тобто успадковуватися від певного класу або реалізовувати певні протоколи. Крім них, якщо у умовах є вимога на реалізацію протоколу з асоційованими типами, то можна накладати аналогічні умови і на них.
// Без обмежень Tank може бути спеціалізований для будь-якого типу.
struct Tank<TContent> {
var content: TContent
}

// Обмеження на реалізацію протоколу.
struct FuelTank<TFuel: Fuel> {
var content: TFuel
}

// Обмеження на реалізацію протоколу і асоційований тип.
struct OxygenEngine<TBipropellant: Bipropellant> where TBipropellant.TOxidizer == Oxygen {
var fuel: TBipropellant.TFuel
var oxidizer: Oxygen
}

12. Простір імен
В Objective-C для уникнення конфліктів імен доводиться використовувати префікси в назвах класів і протоколів.
В Swift у кожного модуля є свій простір імен, і в разі перетину назв типів до потрібного можна звернутися через ім'я його модуля. Можливості оголосити свій простір імен, щоб розділити типи межах одного модуля, поки немає.
13. Контроль доступу
В Objective-C контроль доступу здійснюється рознесенням інтерфейсу з двох файлів. Інтерфейси публічних властивостей і методів зазначаються в заголовочном файлі, а інтерфейси приватних — в файлі реалізації.
В Swift немає поділу оголошення типу на два файлу, і контроль доступу здійснюється з допомогою спеціальних модифікаторів.
Swift 3:
  • open
    — доступ з цього модуля та модулів імпортують цей модуль.
  • public
    — повноцінний доступ з цього модуля, а з модулів імпортують цей модуль доступ без можливості успадкування класів і перевизначення методів.
  • internal
    — доступ тільки з цього модуля.
  • fileprivate
    — доступ тільки з цього файлу.
  • private
    — доступ тільки з цього оголошення або розширення.
14. Обробка помилок
В Objective-C використовується два механізму для обробки помилок:
NSException
та
NSError
. Механізм винятків з
NSException
це кидання і ловля помилок конструкціями
@try
,
@catch
,
@finally
; а механізм з
NSError
це передача вказівника на
NSError*
і подальша обробка встановленого значення. Причому в Cocoa рідко доводиться ловити
NSException
, тому що
NSException
зазвичай використовується для невиправних помилок, а для помилок, які потребують обробки, використовується
NSError
.
В Swift є нативний механізм обробки помилок
do-try-catch
, який замінив
NSError
. Слід зауважити що в цей механізмі немає блоку
finally
. Замість нього слід використовувати блок
defer
, код якого виконується при виході з scope, і він, в принципі, не пов'язаний з обробкою помилок і може бути використаний в будь-якому місці.
Що стосується
NSException
, то, за сумісності з Objective-C, вони працюють, але ловити їх в Swift не можна.
protocol Technology {}

/* В якості помилки можна використовувати будь-який тип, достатньо реалізувати
протокол Error. Він не містить ніяких вимог, але дозволяє використовувати
тип в конструкціях throw і catch. */
// Зазвичай для помилок використовуються перерахування.
enum ConstructingError: Error {
case notEnoughFunding(shortage: Double)
case neccessaryTehcnologyIsNotAvailable(technology: Technology)
case impossibleWithModernTechnology
}

class ThrowingRocketFactory {
var funds: Double = 0.0
func estimateCosts() -> Double {
return 0.0
}

/* Метод, який може кидати помилки, повинен бути позначений ключовим
словом throws після аргументів. */
func makeRocket(for destination: Destination, withPayloadMass payloadMass: Double) throws -> Rocket {
//...
if funds <= 0 {
throw ConstructingError.notEnoughFunding(shortage: estimateCosts())
}
//...
}
//...
}

let factory = ThrowingRocketFactory()
let destination = Destination.lowEarthOrbit
let payloadMass = 0.0

// Викликати кидають методи можна тільки з допомогою ключового слова try.
// Є кілька речей, які можна робити з кидають методами та їх помилками.

/* 1) Помилку можна кинути далі. Для цього метод, в якому викликається
кидає метод, теж позначається як throws. */
func getRocket(forDestination destination: Destination, payloadMass: Double) throws -> Rocket {
let rocketFactory = ThrowingRocketFactory()
let rocket = try rocketFactory.makeRocket(for: destination, withPayloadMass: payloadMass)
return rocket
}

/* 2) Помилку можна зловити. Для цього кидає виклик методу повинен знаходитися
у блоці do, а наступні блоки catch повинні зловити всі можливі помилки. */
do {
let rocket = try factory.makeRocket(for: destination, withPayloadMass: payloadMass)
} catch ConstructingError.notEnoughFunding(let shortage) {
print("Find money: \(shortage)")
} catch ConstructingError.neccessaryTehcnologyIsNotAvailable(let technology) {
print("Find alternatives for: \(technology)")
} catch {
print("Impossible to create such rocket.")
}

/* 3) Результат кидає методу можна перетворити в optional, відмовившись від
інформації про помилку. */
if let rocket = try? factory.makeRocket(for: destination, withPayloadMass: payloadMass) {
//...
}

/* 4) Можна проігнорувати можливість помилки. Але якщо все-таки вона виникне,
це потягне за собою падіння додатка в runtime. */
let rocket = try! factory.makeRocket(for: destination, withPayloadMass: payloadMass)

/* Для демонстрації блоку defer припустимо, що для замовлення ракет треба спочатку
відкривати зв'язок з заводом, а потім обов'язково закривати при будь-якому результаті. */
extension ThrowingRocketFactory {
func openCommunications() {/* ... */}
func closeCommunications() {/* ... */}
}

do {
factory.openCommunications()
defer {
/* Код всередині блоку defer буде викликаний в момент покидання поточного scope, 
у цьому прикладі, блоку do. І не важливо чи буде це відбуватися за
кинутої помилки або в ході звичайного виконання команд. */
factory.closeCommunications()
}
let rocket = try factory.makeRocket(for: destination, withPayloadMass: payloadMass)
} catch {
// ...
}

15. Управління пам'яттю
В Swift, як і в Objective-C, використовується підрахунок посилань, але автоматичний підрахунок посилань відключити не можна. А при роботі з низькорівневим процедурних API всі повертаються об'єкти обертаються в структуру
Unmanaged
. Лічильником посилань такого об'єкта можна керувати вручну через методи структури:
retain()
,
release()
,
autorelease()
, але, щоб отримати доступ до такого об'єкту потрібно розгорнути його, передавши управління підрахунком посилань Swift'у. Для цього є два методи:
takeRetainedValue()
повертає посилання з декрементом лічильника
takeUnretainedValue()
— просто повертає посилання.
Докладніше
Unmanaged
: [7]
16. Потокобезопасность
В Swift поки не ніякого нативного механізму для потокобезопасности. Немає модифікаторів властивостей
atomic
та
nonatomic
з Objective-C. Втім, доступні як примітиви синхронізації начебто семафорів і м'ютексів, так і Grand Central Dispatch та
NSOperation
з Cocoa.
17. Препроцесор
На відміну від Objective-C препроцесора в Swift немає. Втім Swift код може бути скомпільований грунтуючись на умови обчислення конфігурацій збірки (build configurations). Ці конфігурації можуть враховувати логічні фляги компілятора (
D <#flag#>
), і результат спеціальних функцій, які можуть перевіряти ОС —
os()
з аргументом зі списку:
OSX
,
iOS
,
watchOS
,
tvOS
,
Linux
; архітектуру —
arch()
:
x86_64
,
arm
,
arm64
,
i386
; версію мови —
swift()
: >= і номер версії. Для цього використовуються директиви:
#if
,
#elseif
,
#else
,
#endif
.
Apple docs.
18. Бібліотеки іншої мови
Можна використовувати Objective-C бібліотеки і фреймворки в Swift проектах? Так, до тих пір поки вони не вимагають доступу в runtime до ваших чистим Swift класів. Наприклад, OCMock працює тільки зі Swift класами, які успадковуються від
NSObject
.
Можна використовувати Swift бібліотеки і фреймворки в Objective-C проектах? Тільки якщо вони спроектовані таким чином, щоб підтримувати Objective-C. Щоб Swift класи були видні з Objective-C, вони повинні успадковуватися від
NSObject
. Унікальні для Swift можливості не будуть доступні.
Apple docs.
Динамізм Objective-C
Коли ми говоримо про динамізмі Objective-C ми маємо на увазі наступні базові можливості [8]:
  1. Додаток може пізнавати свою структуру в runtime. Наприклад, які методи і властивості є в об'єкта або отримання посилання на протокол, клас або метод із рядка.
  2. Додаток може щось робити грунтуючись на тому, що воно знає про свою структуру. Наприклад, створювати екземпляри класу, ім'я якого було невідомо на етапі компіляції, або звертатися до методів і властивостей, які теж були не відомі при компіляції.
  3. Додаток може змінювати свою структуру. Наприклад, в runtime додавати методи класів або оголошувати нові класи.
Використовуючи ці можливості, в Cocoa були реалізовані безліч корисних механізмів. В Swift немає такого динамізму, але коли ми розробляємо під iOS, ми все одно стоїмо на плечах Cocoa і можемо використовувати ці можливості, правда з деякими обмеженнями.
Але ж Swift це open source мова, яка існує не тільки в Apple екосистемі, де є Cocoa. Ці можливості були б корисні і там. Крім того, хоч в найближчі роки, швидше за все, Cocoa нікуди не піде, можна припустити, що коли-небудь в майбутньому Apple будуть замінювати фреймворки Cocoa чимось новим, написаному на чистому Swift. Як вони будуть вирішувати ті проблеми, які в Objective-C вирішувалися його динамізмом? Розглянемо деякі такі, що базуються на динамізмі, можливості, як вони використовуються в Swift + Cocoa, і які альтернативи є у чистому Swift.
19. «Target/action» messaging
«Target/action» messaging використовується для відправлення команд з інтерфейсу в код. Ця можливість оголосити метод у об'єкта в responder chain і одним рухом з'єднати його з UI елементом у Interface Builder в 1988 році стала видатним поліпшенням у порівнянні з однією монолітною функцією, яка викликалася при будь-якій дії від користувача.
Swift + Cocoa. Всі responder класи успадковуються від
UIResponder
з UIKit, так що всі, звичайно, працює.
В чистому Swift немає механізмів самоаналізу для реалізації такої можливості. Без динамізму не вийде знайти якийсь об'єкт в responder chain реалізує визначений в runtime метод і викликати його. [9]
20. Key-Value Coding
Можливість звертатися до властивостей об'єктів, використовуючи рядки як ідентифікатори.
Swift + Cocoa. В Swift KVC працює тільки для класів, які успадковуються від
NSObject
.
Альтернативи в чистому Swift?
Лише read-only, через відображення (reflection) використовуючи структуру
Mirror
. [10]
21. Key-Value Observing
Змогу підписатися на оновлення якої-небудь властивості.
Swift + Cocoa. KVO працює тільки якщо і спостерігач, і спостережуваний об'єкт успадковуються від
NSObject
, і спостережуване властивість зазначено
dynamic
, для запобігання статичної оптимізації.
Нативних аналогів в Swift немає, але, в принципі, реалізована, наприклад, з допомогою property observers. Будь-яка реалізація: своя або вже готова бібліотека, наприклад, Вами-Swift, додає при використанні зайвий код, коли в Cocoa KVO все працювало як є.
22. NotificationCenter
Можливість одним об'єктам підписатись на оповіщення, а іншим відправляти ці оповіщення.
Swift + Cocoa. Використання API з селектором накладає наступні обмеження. Спостерігач повинен бути екземпляром класу (не структурою і не перерахуванням) і метод, що викликається при отриманні повідомлення повинен бути доступний для Objective-C (клас успадковуватися від
NSObject
або метод відзначений
@objc
). Від цих обмежень можна позбутися, використовуючи API з замиканням, але він все одно повертає об'єкт-токен типу
NSObjectProtocol
, за допомогою якого слід відписуватися від сповіщень. Так що прив'язка до Cocoa поки залишається.
Чистий Swift. Для реалізації шаблону «Спостерігач» динамізм не потрібен, так що аналог в чистому Swift може бути реалізований. Більше того, втрата префікса NS у назві говорить про те, що слід очікувати, що
NotificationCenter
буде додано до Swift Foundation, тобто відкине Cocoa динамізм.
23. UndoManager
Можливість реєструвати зміни як оборотні, щоб дозволити користувачеві переміщатися вперед і назад по них.
Swift + Cocoa. Існує 3 способу реєструвати зміни: з допомогою селектора, через
NSInvocation
або через замикання. Всі три способи застосовні тільки для екземплярів класів. Для способу з селектором є додаткове обмеження: те ж, що й описана вище про
NotificationCenter
: метод повинен бути доступним для Objective-C. Спосіб з
NSInvocation
найбільш сильно спирається на динамізм, адже, по суті, це перехоплення повідомлень, так що клас повинен спадкуватися від
NSObject
. Спосіб з замиканням ж доступний тільки починаючи з iOS 9.
Чистий Swift. Існуючий на даний момент
UndoManager
цілком спирається на динамізм, але, також як і
NotificationCente
r, реалізація подібного функціоналу можлива в чистому Swift, і в майбутньому слід очікувати
UndoManager
в Swift Foundation.

Взагалі, всі API з селекторами, для яких динамізм не важливий, тобто викликається код відомий на момент компіляції, можуть бути замінені на чистий Swift API, що використовує замикання або делегування.
Для тих завдань, які, дійсно залежать від динамізму Objective-C, рішень у чистому Swift поки немає. Як Swift вирішить ці проблеми поки не ясно. Може бути це буде додаванням динамізму, може зовсім по-новому. Важливо те, щоб це рішення не було вузькоспеціалізованому, адже коли Objective-C розроблявся ніхто не знав про KVC, CoreData, bindings, HOM, UndoManager і т. д. І ніщо з цього списку не вимагало спеціальної підтримки мовою/компілятором. Ми хочемо, щоб Swift не тільки вирішував ці проблеми, але і не обмежував нас в розробці нових подібних можливостей. [11]
Статична типізація в Swift
Так які ж переваги дає сувора статична типізація в Swift? Заради чого був втрачений динамізм?
1. Надійність. Писати надійний код Swift простіше, ніж в Objective-C. Статична типізація дозволяє використовувати систему типів, щоб зробити небажану поведінку неможливим. Тобто відловлювати можливі помилки на етапі компіляції. Кілька цікавих прийомів можна подивитися тут: [12], [13].
2. Швидкість роботи? Безумовно, статичне зв'язування дозволяє компілятору проводити більш агресивну оптимізацію, і чистий Swift працює швидше, ніж Objective-C. Але, навряд чи, в iOS розробці можна отримати істотну вигоду від цього, поки практично всі базується на динамічному Cocoa.
Говорячи про систему типів, хочеться відзначити наступне. В Swift ніякої тип автоматично не приводиться до
Bool
, і оператор присвоювання не повертає значення.
Висновок
Крім можливостей Swift і Objective-C розрізняє ще кілька моментів.
По-перше, source stability. Хоч розробники мови і спробували зібрати максимальну кількість змін, що порушують сумісність у версії 3.0, я думаю, ні для кого не секрет, що Swift знаходиться ще в активному розвитку, і подібні зміни неминучі. Втім, зараз кожна пропозиція на таку зміну вимагає переконливого обґрунтування і докладного обговорення. Також, для роботи зі старої кодової базою в мову будуть введений прапор компілятора для версії мови і розширення для Availability API. [14]
По-друге, Swift ще немає ABI сумісності. Це означає, що динамічні бібліотеки скомпільовані в іншій версії мови не будуть працювати.
Все це означає, що перед тим, як переносити свій проект на нову версію мови, вам доведеться дочекатися поки всі Swift бібліотеки і фреймворки, які ви використовуєте, теж не перейдуть на нову версію.
Забезпечення сумісності зі Swift 3 і стабілізація ABI є основними цілями для Swift 4, який вийде в кінці 2017 року. [15]
Ну і наостанок, список моментів, які теж відрізняють Swift від Objective-C, але, на мою думку, недостатньо впливають на розробку, щоб внести їх в основний список.
  • Перевантаження операторів і можливість визначення нових.
  • Посилений
    switch-case
    .
  • Строгі правила инициализаторов.
  • Індекси (subscripts).
  • Availability API.
  • Playgrounds.
  • Swift — open source.
  • Висновок типів.
  • Інтервали.
  • Option sets.
  • Labeled loops.
  • Autoclosures.
  • Інтерполяція рядків.
Посилання
1. Advanced & Practical Enum usage in Swift
2. Controlling Complexity in Swift — or — Making Friends with Value Types
3. Protocols Associated with Types
4. Swift: Associated Types
5. The Ghost of Swift Bugs Future
6. Protocol-Oriented Programming in Swift
7. Unmanaged
8. A Definition of Dynamic Programming in the Cocoa World
9. Pimp My Code, Book 2: Swift and Dynamism
10. The Swift Reflection API and what you can do with it
11. What's Missing in the Discussion about Dynamic Swift
12. The Type System is Your Friend
13. Enums as constants
14. Availability by Swift version
15. Looking back on Swift 3 ahead and to Swift 4
Джерело: Хабрахабр

0 коментарів

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