Pull to refresh

Пять ловушек для начинающего свифтера

Reading time 5 min
Views 39K
Всем привет! Я — начинающий свифтер, то есть изучаю 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}).

image

При этом сам 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 — ваши замечания будут очень полезны мне и всем начинающим свифтерам.
Tags:
Hubs:
+22
Comments 29
Comments Comments 29

Articles