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

в 9:58, , рубрики: iOS разработка, objective-c, swift, Блог компании Аркадия, возможности, разработка под iOS, сравнение

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
            return Carriage(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. Generics

В отличие от 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. Любая реализация: своя или уже готовая библиотека, например Observable-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 целиком опирается на динамизм, но, также как и NotificationCenter, реализация подобного функционала возможна в чистом 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 with Associated 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 and ahead to Swift 4

Автор: Аркадия

Источник

Поделиться новостью

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