Ссылки на функции в Swift и циклические ссылки

Язык программирования Swift появился с некоторыми замечательными возможностями. И, хотя эти функции давно уже есть в других языках программирования, в мире iOS это как глоток свежего воздуха. Во-первых это замыкания. Это функции специального типа замыкания, которые могут быть переданы как аргументы в другие функции и установлены как значение соответствующих свойств. В этом посте разберемся что такое замыкания и как их употреблять. 

Для начала создадим кнопку, которая выполняет определенный блок кода при тапе.

class SwiftClosureButton: UIButton {
 
    var actionOntap: (() -> ())? {
        didSet {
            addTarget(self, action: "_callAction", forControlEvents: .TouchUpInside)
        }
    }
 
    func _callAction() {
        if let action = actionOntap {
            action()
        }
    }
}

Начало положено. Мы определили отдельный класс кнопки и указали в нем свойство типа замыкание. Которое будет вызвано, когда произойдет событие нажатия на кнопку (TouchUpInside). Рассмотрим как использовать этот класс. Мы можем написать примерно следующее.

button.actionOnTap = {
    print("SwiftClosureButton tapped")
}

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

button.actionOnTap = actionOnTap

func actionOnTap() {
    print("SwiftClosureButton tapped")    
}

С виду этот код ничем принципиально не отличается от предыдущей версии. Аналогично первому блоку мы указываем свойству кнопки некую функции. Казалось бы, какая разница кнопке какая это будет функция, бери и исполняй. Но тут кроется очень хитрая деталь. А именно: во втором случае происходит захват переменной self функцией замыканием. Даже если в коде мы не используем self, в случае передачи свойству указателя на ссылку произойдет захват. Проблема кроется в том, что actionOnTap во втором случае является частью UIViewController. Это создает сильную ссылку на UIViewController, что разумеется приводит к тому, что этот контроллер не освобождается из памяти.

Возможным решением является указание Swift, что ссылка на self должна быть слабая. Например weak self. Но, к сожалению, при передачи указателя на функции нет такой возможности.

Решение: Обертка в замыкание

Если обработчик actionOnTap занимает не 1-3 строки разумно вынести его в отдельную функцию, что мы и сделали ранее. После чего как раз появилась проблема. Первый способ решения сильных ссылок - обернуть вызов функции в замыкание.

button.actionOnTap = { [unowned self]
    self!.actionOnTap()    
}

В этом примере используется unowned. Возможно использование weak. О различиях между weak и unowned поговорим в следующий раз. 

Другие попытки

Один из возможных путей решения: определить глобальную переменную для хранения функции замыкания. 

var myFunc: (() -> ())?
 
override func viewDidLoad() {
    myFunc = myFuncInViewController
}
 
func myFuncInViewController() {
    print("Everything is ok!")
}

Кажется, что сейчас негде захватить переменную self. Но тут нас тоже ждет провал. Даже если определить глобальную myFunc как lazy.

Заключение

Чтобы избежать создания сильных связей нужно всегда помнить, что ссылка на тело функции означает что это ссылка на весь объект. Когда устанавливается значение в переменную типа функции или замыкание - устанавливается сильная ссылка на целый объект. В любом случае, нужно убедиться, что аналогичные функции обернуты в слабые ссылки (weak).

Комментарии

comments powered by Disqus