[Из песочницы] Пять ловушек для начинающего свифтера
Всем привет! Я — начинающий свифтер, то есть изучаю Swift без опыта ObjC. Недавно мы с компаньонами начали проект, требующий приложение под iOS. А еще у нас есть idée fixe: с нами непременно должен работать студент из Физтеха, а приложение должно быть написано на языке Swift. И вот, пока мы ищем физтеховцев и знакомимся с ними, я решил не терять время и параллельно начать своими силами пилить проект на Swift. Так я впервые открыл XCode.Вдруг обнаружилось много знакомых, которые точно так же не имея опыта мобильной разработки, стали осваивать ее именно посредством Swift, а не ObjC. Кто-то из них подтолкнул меня поделиться опытом на Хабре.
Итак, вот топ пять «ловушек», своевременное понимание которых точно бы сэкономило мне время.
1. Блоки (замыкания) могут порождать утечки памятиЕсли вы, как и я, пришли в мобильную разработку минуя ObjC, то, наверное, одним из самых важных вводных материалов я бы назвал документацию Apple по Automatic Reference Counting. Дело в том, что при «скоростном» изучении нового языка путем погружения (то есть, начав сразу пилить реальный проект) у вас может развиться склонность пропускать «теорию», не имеющую отношения к задачам типа «показать всплывающее окно здесь и сейчас». Однако мануал по ARC содержит очень важный раздел, специально объясняющий неочевидное свойство замыканий, порождающее утечки.Итак, пример «ловушки». Простой контроллер, который никогда не очистится из памяти:
class ViewController: UIViewController { var theString = «Hello World» var whatToDo: (()→Void)! override func viewDidLoad () { whatToDo = { println (self.theString) } } override func touchesBegan (touches: NSSet, withEvent event: UIEvent) { whatToDo () navigationController!.setViewControllers ([], animated: true) } deinit { println («removed from memory») } } Запускаем и тычем пальцем в экран. Если у нас мало опыта, то мы ошибочно ожидаем увидеть в консоли: Hello World removed from memory Но на самом деле мы видим: Hello World То есть мы потеряли возможность обращаться к нашему контроллеру, а тот остался висеть в памяти.Почему же? Оказывается, вызов self вот в этой невинной строчке
{ println (self.theString) } автоматически создает строгую ссылку на контроллер из замыкания whatToDo. Так как на whatToDo уже строго ссылается сам контроллер, то в результате мы получаем два объекта в памяти, строго ссылающихся друг на друга — и они никогда не вычистятся.Если внутри замыкания НЕ используется вызов self, то такого подвоха НЕ возникает.
В свифте, конечно, предусмотрено решение, которое Apple почему-то называет элегантным. Вот оно:
whatToDo = { [unowned self] in println (self.theString) } Et voila! Вывод: будьте внимательны с жизненным циклом всех замыканий, содержащих вызов self.2. Array, Dictionary и Struct по умолчанию немутабельные типы, никогда не передающиеся по ссылке Когда стоит задача освоить новый язык очень быстро, я склонен забивать на чтение доков по таким интуитивно очевидным типам, как массивы и словари, полагаясь на то, что autocomplete научит меня всему, что надо, непосредственно в процессе кодинга. Такой торопливый подход все-таки подвел меня в ключевом месте, когда я всю дорогу воспринимал «массивы массивов» и «массивы страктов» как наборы ссылок (по аналогии с JS) — они оказался наборами копий.После прочтения доков я все-таки прозрел: в Свифте массивы и словари являются страктами и поэтому, как любые стракты, передаются не по ссылке, а по значению (путем копирования, который компилятор оптимизирует под капотом).
Пример, иллюстрирующий мега-подвох, который вам приготовил Свифт:
struct Person: Printable { var name: String var age: Int var description: String { return name + » (\(age))» } } class ViewController: UIViewController { var teamLeader: Person! var programmers:[Person] = [] func addJoeyTo (var persons:[Person]) { persons.append (Person (name: «Joey», age: 25)) } override func viewDidLoad () { teamLeader = Person (name: «Peter», age: 30) programmers.append (teamLeader) // Строим ошибочные ожидания… teamLeader.name = «Peter the Leader» addJoeyTo (programmers) // …и вот он, момент истины println (programmers) } } При запуске, если мы ошибочно мыслим в ключе «передача по ссылке», то ожидаем увидеть в консоли: [Peter the Leader (30), Joey (25)] // Результат 1 Вместо этого видим: [Peter (30)] // Результат 2 Будьте внимательны! Как же выйти из положения, если нам в действительности нужен именно первый результат? На самом деле, каждый конкретный случай требует индивидуального решения. В данном примере сработает вариант замены struct на class и замены [Person] на NSMutableArray.3. Singleton Instance — выбираем наилучший «хак» Ловушка заключается в том, что на текущий момент классы в Swift не могут иметь статических хранимых свойств, а только статические методы (class func) или статические вычисляемые свойства (class var x: Int {return 0}).
При этом сам Apple вообще не имеет предубеждений против глобальных инстансов в духе паттерна Singleton — в этом мы регулярно убеждаемся, используя такие перлы, как NSUserDefaults.standardUserDefaults (), NSFileManager.defaultManager (), NSNotificationCenter.defaultCenter (), UIApplication.sharedApplication (), ну и так далее. Мы действительно получим статические переменные в следующем общем обновлении — Swift 1.2.
Так как же нам создать собственные такие же инстансы в текущей версии Swift? Есть несколько возможных «хаков» под общим названием Nested Struct, но самый лаконичный из них — это следующий:
extension MyManager { class var instance: MyManager { func instantiate () → MyManager { return … // постройте свой инстанс здесь } struct Static { static let instance = instantiate () // lazily loaded + thread-safe! } return Static.instance } } Стракты в свифте не только поддерживают статические хранимые свойства, но также по умолчанию дают им отложенную поточно-ориентированную инициализацию. Вот это профит! Не зная об этом заранее, можно зря потратить время на написание и отладку лишнего кода.Внимание! В следующей версии свифта (1.2) этот «хак» уже не понадобится, но дата общего релиза не известна. (Уже доступна бета-версия для тестирования, но для этого необходима также бета-версия XСode6.3, билд из которой от вас не примет Appstore. Короче — ждем глобального релиза.)
4. Методы didSet и willSet не будут вызваны в процессе выполнения конструктора Вроде мелочь, но это способно ввести вас в тотальный ступор при отладке багов, если вы не знаете этого. Поэтому если вы запланировали какой-то набор манипуляций внутри didSet, который важен как при инициализации, так и далее в течение жизненного цикла объекта, делать это нужно таким образом: class MyClass { var theProperty: OtherClass! { didSet { doLotsOfStuff () } } private func doLotsOfStuff () { // здесь реагируем на didSet theProperty } … init (theProperty: OtherClass) { self.theProperty = theProperty doLotsOfStuff () } } 5. Нельзя просто так взять и обновить UI, когда пришел ответ с сервера Программисты с опытом ObjC могут посмеяться над этой «ловушкой», потому что она должна быть общеизвестна: методы, связанные с UI, безопасно дергать только из главного потока. Иначе — непредсказуемость и баги, толкающие в тотальный ступор. Но это наставление почему-то проходило мимо меня, пока я, наконец, не столкнулся с жуткими багами.Пример «проблемного» кода:
func fetchFromServer () { let url = NSURL (string: urlString)! NSURLSession.sharedSession ().dataTaskWithURL (url, completionHandler: { data, response, error in if (error!= nil) { … } else { self.onSuccess (data) } })!.resume () } func onSuccess (data) { updateUI () } Обратите внимание на блок completionHandler — все это будет исполняться вне главного потока! Тем, кто еще не столкнулся с последствиями, советую не экспериментировать, а просто не забыть обставить updateUI следующим образом: func onSuccess (data) { dispatch_sync (dispatch_get_main_queue (), { updateUI () }) } Это типичное решение. Одной строчкой мы возвращаем updateUI обратно в главный поток и избегаем неожиданностей.На сегодня все. Всем новичкам успехов!
Опытные хабровчане из mobile — ваши замечания будут очень полезны мне и всем начинающим свифтерам.