swift_vs_objc

Всем привет!

Время от времени я натыкаюсь на статьи типа «Swift vs Objectivе-C: бла бла бла», в которых в большинстве случаев описаны причины, почему стоит переходить на Swift с Objective-C. Но, т.к. в 90% подобных статей используются лишь общие громкие фрази типа «Swift безопаснее», «Swift быстрее», «меньше кода», «управление памятью», «динамические библиотеки» и т.д. и т.п., я решил написать статейку на эту тему и на пальцах показать разницу между Swift и Obj-C.

Статья предполагает, что читатель имеет опыт написания приложений на Objective-C и немного знаком с Swift.

Я не буду идти по принципу «сначала самое важное, потом все остальное». Я открываю Xcode, создаю проект на Swift и, что вижу, то и анализирую.

Итак, первое, что бросается в глаза, это конечно же

В Swift для класса используется один файл (*.swift) против двух файлов заголовка (*.h) и реализации (*.m) в Objective-C

Лично у меня по этому поводу двоякое мнение, и я бы не сказал с уверенностью, что этот факт — это прямо МЕГА-преимущество. Да, с одной стороны навигация по проекту, поддержка и прочее по идее должны стать легче. Давайте посмотрим визуально на Project Navigator проекта с десятью мифическими классами на Swift и Objective-C

project_navigator_swift project_navigator_objc

Веет какой-то легкостью, не правда ли?

С другой стороны возникает логический вопрос — как в Swift посмотреть паблик интерфейс класса? В Objective-C одним кликом выбрали файл .h и вуаля — вот интерфейс и ничего лишнего. В Swift же надо сделать ряд движений, а именно 1 — включить Assistant Editor, 2 — Выбрать Counterparts

swift_public_interface

Скорее, это вопрос привычки, но в целом, один файл вместо двух, это конечно же лучше.

Едем дальше и что действительно мне с первого взгляда бросайтся в в глаза, и что действительно нравится…

Никакого #import

Предположим, в Class1 нам нужны все остальные классы. В Objective-C нам нужно их все импортировать через #import, в Swift — нет

Objective-C

#import <Foundation/Foundation.h>
#import "Class2.h"
#import "Class3.h"
#import "Class4.h"
#import "Classs5.h"
#import "Class6.h"
#import "Class7.h"
#import "Class8.h"
#import "Class9.h"
#import "Class10.h"

@interface Class1 : NSObject {
    Class2 *class2;
    Class3 *class3;
    Class4 *class4;
    Classs5 *class5;
    Class6 *class6;
    Class7 *class7;
    Class8 *class8;
    Class9 *class9;
    Class10 *class10;
}

@end

Swift

import UIKit

class Class1: NSObject {

    var class2: Class2?
    var class3: Class3?
    var class4: Class4?
    var class5: Class5?
    var class6: Class6?
    var class7: Class7?
    var class8: Class8?
    var class9: Class9?
    var class10: Class10?
}

Это действительно круто!

Да, фреймворки импортровать все-равно надо 😉

А дальше я заметил, что допустил ошибку в названии класса Classs5 в Objective-С и решил сделать привычный рефакторинг -> переименование, и проверить как это работает в Swift тоже. И тут упс…

Can’t refactor Swift code

no_swift_refactor

В Xcode 8.2.1 Swift 3.0 до сих пор нет такой возможности. Я думал, что кривой рефакторинг в Objective-C, который всегда приходилось допиливать ручками, наконец доведут до ума в Swift. Но увы. Тут его вообще отключили. Хочется верить, что это пока.

Где-то я видел такой заголовок

Swift более читаемый язык, чем Objective-C

Предположим. Давайте сосздадим какой-нибудь метод делегата, который должен вызываться при обновлении каких-то данных и, собственно, иметь в качестве параметров три переменные — указатель на себя (делегата в смысле), обновленные данные, и флаг, что обновление завершено.

В Objective-C такой метод выглядел бы так:

- (void)manager:(NSObject *)manager didUpdateData:(NSData *)data updateCompleted:(BOOL)completed {
   // do some job
}

В Swift точно такой же метод будет выглядеть так:

func manager(_ manager: NSObject, didUpdateData data: NSData, updateCompleted completed: Bool) {
   // do some job
}

Хорошо. Посмотрим на синтаксис на разных примерах:

Objective-C

// создание экземпляра класса
NSObject *object = [[NSObject alloc] init];

// пример условного оператора
if (success) {
   // do some job
}

// пример добавления обсервера
NSString *notificationName = @"LocalNotificationReceivedNotification";
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(recievedLocalNotification)
                                             name:notificationName
                                           object:nil];

Swift

// создание экземпляра класса
let object = NSObject()

// пример условного оператора
if success {
   // do some job
}

// пример добавления обсервера
let notificationName = Notification.Name(rawValue: MyNotifications.localNotificationReceived)
NotificationCenter.default.addObserver(self,
                                       selector: #selector(recievedLocalNotification),
                                       name: notificationName,
                                       object: nil)

Что можно сказать?

 В Swift нет необходимости ставить точку с запятой (;) в конце каждой строки. Раз.   В Swift нет, как многие называют «скобочного ада» [[[[]]]]. Два.   Также не надо использовать круглые скобки для условных выражений. Три. 

Синтаксис Swift, действительно, гораздо проще и выразительнее, чем Objective-C.

Ок. С тем, что видно невооруженным взглядом вроде разобрались. Открываем методичку от Apple и поехали копнем немного глубже…

Раздел The Basics нам говорит о главном, судя по всему, а именно:

Swift предлагает собственные версии фундаментальных C и Objective-C типов, включая Int, Double, Float, Bool и String. А также собственные типы коллекций Array, Set, Dictionary.

Забегая вперед скажу, что при этом, мы также можем использовать привычный NSString, NSArray, NSSet, NSDictionary.

Swift предлагает не существующий в Objective-C тип Tuple.

Swift предлагает опциональные типы, которые обрабатывают отсутствие значения. По простому — опциональные типы это как nil в Objective-C, но они могут работать с любыми типами.

Т.е. в ObjC примитивы, например int, не могут быть равны nil, а в Swift могут. Об этом чуть позже.

Ну и в завершение основ The Basics мы видим, что

Swift — типобезопасный язык со строгой типизацией.

Вот об этом и поговорим дальше.

Попробуем переменной одного типа присвоить переменную другого типа.

Objective-C

type_nosafe_objc

Что произойдет при выполнении данного кода? Во-первых, компилятор выдаст нам предупреждение «Incompatible pointer types assigning to ‘NSString *’ from ‘NSNumber *’. Но, мы спокойно соберем сборку и запустимся на выполнение, в результате чего переменная string станет типа NSNumber и будет указывать на ту же область памяти, что и number.

В итоге, наше приложение не упало, все прекрасно работает, но на самом деле, это жуткий баг, который может привести к непредсказуемому поведению, и затрате кучи времени на поиски и отладку.

Если мы попытаемся сделать тоже самое в Swift,

type_safe_swift

мы просто не соберемся. Компилятор выдаст ошибку «Cannot assign value of type ‘NSNumber’ to ‘NSString'».

Swift — язык со строгой типизацией. Язык со строгой типизацией призывает вас иметь четкое представление о типах значений с которыми может работать ваш код. Если часть вашего кода ожидает String, вы не сможете передать ему Int по ошибке.

Поскольку Swift имеет строгую типизацию, он выполняет проверку типов при компиляции кода и отмечает любые несоответствующие типы как ошибки. Это позволяет в процессе разработки ловить, и как можно раньше, исправлять ошибки.

Продолжая говорить о безопасности, стоит поговорить про

Опциональный Тип (Optional)

Что такое Опциональный Тип и зачем он нужен на конкретном примере.

Предположим у нас есть следущий код в Objective-C

    NSString *string = @"1234567890";
    
    int ln = [string length];

У нас есть строка, и нам нужно знать ее длину. Все просто. ln = 10.

Теперь представим, что эта строка нам приходит с сервера, и в какой-то момент она не пришла, т.е. она nil

    NSString *string = nil;
    
    int ln = [string length];

В данном случае ln будет равен нулю. Т.е. мы можем подумать, что пришла строка, и ее длина ноль символов, хотя на самом деле, строки нет. Это две принципиальные разницы. И в этом вся суть опционального типа — показать, имеет ли переменная или константа значение или не имеет.

Как этот кейс будет выглядеть в Swift?

        let string: NSString? = nil
        
        let ln = string?.length

Во-первых если мы объявляем пременную и она может принимать nil, она будет автоматически конвертирована в опциональный тип NSString? (знак ? говорит о том, что string типа Optional NSString)

Во-вторых string?.length вернет Optional Int.

И в данном случае ln будет равен nil, говоря нам о том, что мы не получили значение длины строки.

Ну и дальше нужно понимать, что такое Forced Unwrapping,  Optional Binding и Implicitly Unwrapped.

Про переменные и константы

Отдельно стоит сказать про переменные и константы.

В Swift для объявления переменной используется ключевое слово var, для объявления константы — let.

Например:

let name = «James»

var age = 25

В данном примере мы объявили константу name, которая далее нигде в коде не сможет быть изменена, и переменную age, которая может быть измененна.

Вспомним следующие классы из Objective-C: NSString и NSMutableString, NSArray и NSMutableArray, NSDictionary и NSMutableDictionary и т.д.

Понимаете к чему я?

В Swift изменяемость определяется ключевыми словами let и var.

Например:

let array = Array() —  неизменяемый массив

var array = Array() — изменяемый массив, и т.д.

Проверка доступности API

В Swift есть встроенная поддержка для проверки доступности API

if #available(iOS 10, macOS 10.12, *) {
    // Используйте API iOS 10 для iOS и используйте API macOS 10.12 на macOS
} else {
    // Используйте более старые API для iOS и macOS
}

Дженерики (Generics)

Тут я просто приведу пример из методички Apple

«Дженерики одна из самых мощных особенностей Swift, и большая часть всех библиотек Swift построена на основе дженериков. На самом деле вы используете дженерики все время, даже если вы этого не осознаете. Например, коллекции Swift Array или Dictionary являются универсальными. Вы можете создать массив, который содержит значения типа Int или массив, который содержит значения String, или на самом деле любой другой массив, который может содержать любой другой тип. Аналогично вы создаете словарь, который может содержать значения разных типов, и нет никакого ограничения по типу хранящихся значений.»

Приведем обычную, стандартную, неуниверсальную функцию swapTwoInts(_:_:), которая меняет два Int местами:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Функция swapTwoInts(_:_:) полезная, но она применима только для значений типа Int. Если вы хотите поменять местами два значения типа String или два значения Double, то вам придется написать больше функций, к примеру, swapTwoStrings(_:_:) или swapTwoDoubles(_:_:), которые показаны ниже:

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}
 
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Дженерик функции могут работать с любыми типами. Ниже приведена дженерик версия функции swapTwoInts(_:_:), которая теперь называется swapTwoValues(_:_:):

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Дженерик версия использует заполнитель имени типа (называется T в нашем случае) вместо текущего имени типа (Int, String, Double…). Заполнитель имени типа ничего не говорит о том, чем должно являться T, но он говорит о том, что и a и b должны быть одного типа T, не зависимо от того, что такое T. Текущий тип T будет определяться каждый раз, как вызывается функция swapTwoValues(_:_:).

Другое отличие в том, что за именем дженерик функции (swapTwoValues(_:_:)) идет заполнитель имени типа (Т) в угловых скобках (<T>). Угловые скобки говорят Swift, что T является заполнителем имени типа внутри определения функции swapTwoValues(_:_:). Так как T является заполнителем, то Swift не смотрит на текущее значение T.

Функция swapTwoValues(_:_:) теперь может быть вызвана точно так же как и функция swapTwoInts, за исключением того, что в нее можно передавать значения любого типа, до тех пор пока они одного типа. Каждый раз при вызове функции swapTwoValues(_:_:), тип Т выводится из типов, которые передаются в эту функцию.

продолжение следует…