Swift и Objective-C: 10 отличий
В июне 2014 года в мире Apple случилось то, чего не ожидал никто: компания Apple представила новый объектно-ориентированный язык программирования — Swift, который пришел на смену Objective-C, используемого ранее для разработки приложений для OSX & iOS.
В нашей компании это встретили с большим энтузиазмом и сразу решили его «пощупать». И это не удивительно, так как сразу после появления Swift появились и слухи о его простоте, доступности и удобстве, что немаловажно для больших проектов.
Выполнив парочку проектов — iOS приложений на Swift, я решил поделиться нашими наблюдениями. В частности — тем, что понравилось больше всего. Результат представляю в виде десяти отличий с примерами кода.
1. Playgrounds
Действительно классная штука, когда нужно с чем-то поэкспериментировать, что-то проверить или попробовать. Новое преимущество в XCode 6, которое заключается в том, что программисту предоставляется пространство для экспериментов.
Выглядит оно как простое окно редактора, куда можно писать код. Код этот сразу же компилируется и выполняется. Не нужно собирать проект, а потом запускать эмулятор, чтобы посмотреть. Все происходит мгновенно: пишешь код — видишь результат.
Специальная панель справа умеет показывать практически все — от простых строк до конечного вида какого-либо элемента управления, от простых изображений до графика изменения значений переменной в процессе обработки. Красиво и удобно.
2. Классы больше не разбиваются на интерфейс и реализацию
Это вдвое уменьшает количество файлов в проекте, что в свою очередь упрощает навигацию по нему.
Swift:
class MyClass { var a: Int var b: String init(a: Int, b: String) { self.a = a self.b = b } } var mc = MyClass(a: 2, b: "string"
Objective-C:
//Myclass.h #import <Foundation.h> @interface MyClass: NSObject - (id) initWithA: (int)a andB: (NSString *)b; @end //Myclass.m #import "MyClass.h" @interface MyClass() @property (assign, nonatomic) int a; @property (strong, nonatomic) NSString *b; @end @implementation MyClass @synthesize a; @synthesize b; - (id) initWithA: (int)a andB: (NSString *)b { self = [super init]; if (self) { _a = a; _b = b; } return self; } @end MyClass *mc = [[MyClass alloc] initWithA: 2 andB @"string"];
Из данных отличий мы видим, как упростился код — в Swift он стал не таким громоздким, удобным для написания и применения.
3. Упрощен синтаксис создания полей и свойств классов
Свойства больше не нуждаются в instance variables. Раньше, в последних версиях ObjC, эти iVars создавались автоматически, но их можно было прописывать и вручную. Теперь же их нельзя создать в принципе, а значит о них больше не нужно помнить и думать.
Свойства в Swift могут быть изменяемыми (объявляется как var myField) и константами (объявляется как let myField).
Если пример выше изменить как:
class MyClass { let a: Int // Сделали поле «а» константой var b: String init(a: Int, b: String) { self.a = a self.b = b } }
то после объявления и инициализации класса:
var mc = MyClass(a: 2, b: "string"
изменить это поле уже не получится:
mc.a = 3 //error: Cannot assign 'a' to mc
тогда как другое поле изменяется без проблем:
mс.b = "another string"
На Objective-C напрямую это можно сделать, только прописав вручную сеттер для поля и использовав в нем NSAssert. Но даже в этом случае можно будет использовать доступ к полю напрямую (не через свойство) внутри класса:
//Myclass.h #import <Foundation.h> @interface MyClass: NSObject - (id) initWithA: (int)a andB: (NSString *)b; - (void) anotherMethod; @end //Myclass.m #import "MyClass.h" @interface MyClass() @property (assign, nonatomic) int a; @property (strong, nonatomic) NSString *b; @end @implementation MyClass @synthesize a; @synthesize b; // мы собираемся перегрузить сеттер, но хотим использовать стандартный геттер - (id) initWithA: (int)a andB: (NSString *)b { self = [super init]; if (self) { _a = a; _b = b; } return self; } - (void) setA:(int)a { NSAssert(false, "You are not allowed to assign to `a`"); } - (void) anotherMethod { _a = 25; // } @end MyClass *mc = [[MyClass alloc] initWithA: 2 andB @"string"]; [mc anotherMethod]; //константа изменилась
Если мы хотим действительно неизменную константу в Objective-C, мы не можем инициализировать ее в конструкторе класса, а должны сделать это при её объявлении:
@interface MyClass() { const int _myConst = 42; } @property (assign, nonatomic) int a; @property (strong, nonatomic) NSString *b; @end
4. Появились Optional Types
var a: Int? = 10 a = nil a = 20
Они используются, когда значения по какой-то причине может не быть. Также добавлены средства для удобной работы с этими типами, такие как условное присваивание:
if let b = a { println(b) //вызовется только если a имеет значение }
и условный вызов:
var myrez = myclass?.mymethond() //вызовется только если myclass имеет значение; в противном случае вернет nil
Для сравнения, в Objective-C эти строки выглядели бы так:
NSNumber *a = [NSNumber numberWithInt:10]; a = nil; a = [NSNumber numberWithInt:20]; if ((a)) { int b = [a intValue]; NSLog(@"%d", b); }
При этом NSNumber — класс-наследник NSObject, и его инициализация — затратная операция.
Также другое значение имеет nil. В Objective-C он означает, что указатель никуда не указывает, а в Swift он означает «ничего».
5. Вывод типов (Type inference)
Не нужно прописывать тип, когда он понятен. Следующие строчки кода эквивалентны:
var s1: String = "It's just software" var s2 = "Everything is possible" // NSString s1 = @"There is no type inference in ObjC and you have to type the type every time"
Так как мы используем строковую константу, понятно, что тип переменной — String.
Если мы впоследствии попытаемся использовать ее по-другому, получим ошибку:
s2 = 10 //error: Type 'String' does not conform to protocol 'IntegerLiteralConvertible'
6. Доработано управление памятью
Модель осталась той же: Automatic Reference Counting (ARC), остались strong & weak references. Но — слабые ссылки теперь представлены с помощью Optional values, что логично. Плюс к ним добавились unowned references, которые используются, когда применение strong недопустимо из-за создания циклических ссылок (которые приводят к утечкам памяти), а применение weak недопустимо по логическим причинам (мы не хотим, чтобы поле было опциональным и изменяемым).
Например:
class Person { var card: CreditCard? } class CreditCard { let holder: Person init (holder: Person) { self.holder = holder } }
В таком виде мы создали циклические ссылки: person -> creditCard, creditCard -> person.
Если мы изменим тип ссылки на weak:
class Person { var card: CreditCard? } class CreditCard { weak var holder: Person? init (holder: Person) { self.holder = holder } }
То получим нарушения бизнес-логики:
— карта не может существовать без владельца, а у нас CreditCard.person может быть nil, т.к. для weak-ссылки используется Optional type (holder: Person?);
— владелец карты в нашей модели может меняться (var holder), а в реальности — нет
Чтобы исправить ситуацию, нужно использовать unowned:
class Person { var card: CreditCard? } class CreditCard { unowned let holder: Person init (holder: Person) { self.holder = holder } }
Теперь бизнес-логика сохранена, и управление памятью происходит правильно: когда удаляется карта, владелец остается; при удалении владельца удаляется и его карта.
Для сравнения, финальный вариант конструкции в Objecitve-C выглядел бы так:
//Person.h #import <Foundation.h> @class CreditCard; //forward declaration of CreditCard class to use in the property @interface Person.h : NSObject @property (nonatomic, strong) CreditCard *card; @end //Person.m #import "Person.h" @implementation Person @synthesize card; @end //CreditCard.h #import <Foundation.h> #import "Person.h" @interface CreditCard: NSObject @property (nonatomic, unsafe_unretained, readonly) Person *holder; //!!! - (id) initWithHolder: (Person *)holder; @end; //CreditCard.m #import "CreditCard.h" @implementation CreditCard @synthesize holder; - (id) initWithHolder: (Person *)aHolder { self = [super init]; if (self) { _holder = aHolder; } return self; } @end
При этом свойство holder, которое мы объявили для держателя карты, имеет тип ссылки unsafe_unretained. Это значит, что когда мы удалим объект держателя карты, ссылка в объекте карты будет продолжать показывать на прежнюю область памяти, образуя так называемый daggling pointer, при попытке разыменования которого мы получим runtime error.
В Swift этого не случится, т.к. unowned reference is still safe, т.е. будет установлена в nil при удалении объекта, на который она показывает.
7. «Прокачан» switch
Он теперь умеет делать выбор не только по int или enum (которые в С/ObjC представлялись с помощью того же int), а по всем новым вкусностям, таким как кортежи, диапазоны значений, списки значений, wildcards, а также по строкам, классам и структурам. Теперь он по умолчанию не проваливается (можно заставить) и требует наличия default-case.
Например:
let somePoint = (-0.5, 0.5) switch somePoint { case (0, 0): println("(0, 0) is at the origin") case (_, 0): //_ is a wildcard and matches anyting println("(\(somePoint.0), 0) is on the x-axis") case (0, _): println("(0, \(somePoint.1)) is on the y-axis") case (1,1), (-1,-1), (-1,1), (1,-1): // список значений println("(\(somePoint.0), \(somePoint.1)) is in the corner of the half-sized square") case (_,_) where abs(somePoint.0) == abs(somePoint.1): // wildcard с условием println("(\(somePoint.0), \(somePoint.1)) is on the diagonal") case (-2...2, -2...2): // диапазон значений println("(\(somePoint.0), \(somePoint.1)) is inside the box") default: println("(\(somePoint.0), \(somePoint.1)) is outside of the box") } //prints "(-0.5, 0.5) is on the diagonal"<pre> Теперь то же самое на Objective-C: <pre>struct Point { float x; float y; } struct Point somePoint; somePoint.x = -0.5; somePoint.y = 0.5; if (somePoint.x == 0 && somePoint.y == 0) { NSLog(@"(0, 0) is at the origin"); } else if (somePoint.y == 0) { NSLog(@"(%d,0) is on the x-axis", somePoint.x); } else if (somePoint.x == 0) { NSLog(@"(0,%d) is on the y-axis", somePoint.y); } else if ((somePoint.x == 1 && somePoint.y == 1) || (somePoint.x == -1 && somePoint.y == 1) || (somePoint.x == -1 && somePoint.y == 1) || (somePoint.x == 1 && somePoint.y == -1)) { NSLog(@"(%d,%d) is in the corner of the half-sized square", somePoint.x, somePoint.y); } else if (abs(somePoint.x) == abs(somePoint.y)) { NSLog(@"(%d,%d) is on the diagonal", somePoint.x, somePoint.y); } else if (somePoint.x >= -2 && somePoint.x <= 2 && somePoint.y >= -2 && somePoint.y <= 2) { NSLog(@"(%d,%d) is inside the box", somePoint.x, somePoint.y); } else { NSLog(@"(%d,%d) is outside the box", somePoint.x, somePoint.y); }
Невооруженным взглядом можно увидеть, что в Objective-C код более громоздкий и менее читаемый, а для переменной somePoint, кортежем нам пришлось создать отдельную структуру. Swift же избавляет нас от громоздкости, значительно упрощая процедуру.
8. «Прокачан» enum
Его значением (raw values) теперь могут быть строки, символы, целые и дробные числа:
enum LineEnding : String { case Windows = "\r\n " case UnixOsX = "\n " case OldMac = "\r " }
Пример использования (определяем тип символов перевода строки):
var str = "12345\r 346\n 3467\r\n 457\r 65474567\n " while let lineR = str.rangeOfString("\(LineEnding.Windows.toRaw())|\(LineEnding.UnixOsX.toRaw())|\(LineEnding.OldMac.toRaw())", // key 1 options: .RegularExpressionSearch) { let subR = Range<String.Index>(start: str.startIndex, end: lineR.startIndex) let strR = Range<String.Index>(start: lineR.endIndex, end: str.endIndex) print(str.substringWithRange(subR)) if let ending = LineEnding.fromRaw(str.substringWithRange(lineR)) { // key 2 print(" -> has "); switch ending { case .Windows: print("a Windows") case .UnixOsX: print("a UnixOsX") case .OldMac: print("an OldMac") } println(" ending") } str = str.substringWithRange(strR) }
Программа выведет в консоль следующее:
12345 -> has an OldMac ending 346 -> has a UnixOsX ending 3467 -> has a Windows ending 457 -> has an OldMac ending 65474567 -> has a UnixOsX ending
Здесь была пара ключевых моментов:
— key 1 — при поиске по строке мы используем константы, прописанные в перечислении, добывая их из значений enum`a с помощью метода .toRaw();
— key 2 — при проверке, чем же именно заканчивается строка, мы делаем обратное преобразование с помощью метода .fromRaw(), что было невозможно в Objective-C.
Эквивалент на Objective-C:
NSString *const LineEndingWindowsStr = @"\r\n "; NSString *const LineEndingUnixOsXStr = @"\n "; NSString *const LineEndingOldMacStr = @"\r "; typedef NS_ENUM(NSInteger, LineEnding) { LineEndingWindows, LineEndingUnixOsX, LineEndingOldMac }; NSString* str = @"12345\r 346\n 3467\r\n 457\r 65474567\n "; NSRange lineR = [str rangeOfString:[NSString stringWithFormat:@"%@|%@|%@", LineEndingWindowsStr, LineEndingUnixOsXStr, LineEndingOldMacStr] options:NSRegularExpressionSearch]; while (lineR != NSNotFound) { NSRange subR = NSMakeRange(0,lineR.location); NSRange strR = NSMakeRange(lineR.location + lineR.length, str.length); NSString *endingStr = [str substringWithRange:lineR]; LineEnding ending; if ([LineEndingWindowsStr isEqualToString: endingStr]) { ending = LineEndingWindows; } else if ([LineEndingUnixOsXStr isEqualToString: endingStr]) { ending = LineEndingUnixOsX; } else if ([LineEndingOldMacStr isEqualToString: endingStr]) { ending = LineEndingOldMac; } NSString *msgStr; switch (ending) { case LineEndingWindows: msgStr = @"a Windows"; break; case LineEndingUnixOsX: msgStr = @"a UnixOsX"; break; case LineEndingOldMac: msgStr = @"an OldMac"; break; } NSLog([NSString stringWithFormat:@"%@ -> has %@ ending", [str substringWithRange:subR], msgStr]); str = [str substringWithRange: strR]; lineR = [str rangeOfString:[NSString stringWithFormat:@"%@|%@|%@", LineEndingWindowsStr, LineEndingUnixOsXStr, LineEndingOldMacStr] options:NSRegularExpressionSearch]; }
No comments.
Также enum обзавелся associatedValues. Это значения, которые могут быть присвоены какому-то из case-ов.
enum TrainDelay { case OnSchedule // поезд идет по расписанию case Delayed(Int) // если поезд задерживается, я хочу знать, на сколько } var td = TrainDelay.Delayed(10) // поезд задерживается на 10 минут switch td { case .OnSchedule: // вывод типов дал возможность не писать тип enum; он известен по типу переменной td println("Ok") case .Delayed(let delay): println("Train is delayed by \(delay) minutes") }
На Objective-C реализовать такое можно только при помощи класса:
//TrainDelay.h #import <Foundation.h> typedef NS_ENUM(NSInteger, TrainDelayState) { TrainDelayStateOnSchedule, TrainDelayStateDelayed }; @interface TrainDelay: NSObject - (void)setTraindelayStateOnSchedulle; - (void)setTraindelayStateDelayedBy: (int)minutes; - (NSString*)getTrainDelayStatus; @end //TrainDelay.m #import "TrainDelay.h" @interface TrainDelay() { TrainDelayState _trainDelayState; int _delay; } @end @implementation TrainDelay - (void)setTraindelayStateOnSchedulle { _trainDelayState = TrainDelayStateOnSchedule; } - (void)setTraindelayStateDelayedBy: (int)minutes { _trainDelayState = TrainDelayStateDelayed; _delay = minutes; } - (NSString*)getTrainDelayStatus { switch { case TrainDelayStateOnSchedule: return @"Ok"; case TrainDelayStateDelayed: return [NSString stringWithFormat:@"Train is delayed by %@ minutes", _delay]; } } @end
Здесь, чтобы скрыть поля класса, мы создали безымянную категорию TrainDelay(), а в интерфейсе класса оставили только методы. Для установки _trainDelayState пришлось создать два метода, чтобы учесть, что одно из состояний имеет дополнительный параметр, а другое его иметь не может.
По примерам делаем вывод, что со Swift мы выигрываем в читаемости, в длине кода и в работе с обратным преобразованием по методу .toRaw(), что абсолютно невозможно в Objective-C.
9. Добавлены возможности функционального программирования
В Swift есть функции высшего порядка, функции как значения, вложенные функции, замыкания, анонимные функции, идиомы .map(), .each() и прочие атрибуты настоящего функционального программирования!
Пример замыкания:
let arr = [5,7,3,11,-5,4] let a2 = map(arr) {x in x*x} //[25,49,9,121,25,16]
В ObjC для этого можно использовать блок:
NSArray *arr = @[@5,@7,@3,@11,@-5,@4]; NSMutableArray *a2 = [NSMutableArray arrayWithCapacity:arr.count]; [arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { int x = [(NSNumber *)obj integerValue]; [a2 addObject:[NSNumber numberWithInt:x*x]]; }]; NSArray *a3 = [a2 copy];
Но в Objective-C получилось в 4 раза больше кода, пропала ясность, и второй массив не получилось сразу сделать immutable — пришлось делать копию.
10. Появились Generics
struct Queue<T> { var items = [T]() mutating func push(item: T) { items.append(item) } mutating func pop() -> T { return items.removeAtIndex(0) } } var intQ = Queue<Int>() intQ.push(1) intQ.push(2) intQ.push(3) var i = intQ.pop() //returns 1, intQ.items = [2,3] var strQ = Queue<String>() strQ.push("раз") strQ.push("два") strQ.push("три") var s = strQ.pop() //returns "раз", strQ.items = ["два", "три"]
В ObjectiveC вместо этого использовался тип id — указатель. Чтобы повторить этот пример на нем, нам снова придется использовать класс, так как в ObjC структуры не могут содержать методов. Хотя, на самом деле, можно извратиться через указатели и явную передачу параметра self, но вряд ли кто-нибудь будет применять это на практике.
//Queue.h #import <Foundation.h> @interface Queue:NSObject - (void)push:(id)item; - (id)pop; @end //Queue.m @interface Queue() { NSMutableArray *_items; } @end @implementation Queue - (id)init { self = [super init]; if (self) { _items = [NSMutableArray array]; } return self; } - (void)push:(id)item { [_items addObject:item]; } - (id)pop { id el = [_items objectAtIndex:0]; [_items removeObjectAtIndex:0]; return el; } @end //main.m #import "Queue.h" int main(int argc, char *argv[]) { Queue *intQ = [[Queue alloc] init]; [intQ push:[NSNumber numberWithInt:1]]; [intQ.push:[NSNumber numberWithInt:2]]; [intQ.push:[NSNumber numberWithInt:3]]; int i = [(NSNumber *)[intQ pop] intValue]; Queue *strQ = [[Queue alloc] init]; [strQ push:@"раз"]; [strQ push:@"два"]; [strQ push:@"три"]; NSString *s = (NSString *)[strQ pop]; }
Видим ту же картину, что и в предыдущих примерах: в Swift всё стало лучше. Меньше кода и лучшая читаемость по сравнению с Objective-C.
Заключение
Подводя итог, можно с полной уверенностью сказать, что Swift — это будущее. Все познаётся в сравнении и, лишь отработав определенное количество проектов, можно утверждать, удобен ли язык, и можно ли на нём эффективно работать.
Несмотря на пессимизм и консерватизм многих компаний, основанный на множестве заявлений о том, что язык сыроват и есть проблемы с Xcode, что присутствуют недочёты, связанные с плохой автодоводкой кода (autocompletion), что есть недостаточная поддержка фреймворка для создания iOS приложений — UIKit, Redwerk уполномочен заявить — Swift экзамен сдал!
Хотели бы мы, чтобы все эти 10 полезных особенностей были доступны, когда мы разрабатывали iOS-приложения для ведущих американских новостных телеканалов. Они существенно облегчили бы нам жизнь.