Экранированные и неэкранированные замыкания
Одним из более сложных аспектов замыканий, которые важно понимать, яв
-
ляется управление памятью. Всякий раз, когда создается замыкание, перемен
-
ные и экземпляры, содержащиеся в нем, «замыкаются», и создаются сильные
ссылки на эти объекты. Поэтому объекты остаются в памяти даже после выхода
из области видимости кода, благодаря чему могут использоваться замыкани
-
ем. Например:
class Incrementor {
var count = 0
func increment() {
count += 1
print(count)
100
Передача сообщений
}
}
let incrementor = Incrementor()
let closure = {
incrementor.increment() // 1
incrementor.increment() // 2
}
closure() // Выведет: "1\n2"
Здесь сначала определяется класс, увеличивающий значение своего счетчи
-
ка на единицу при каждом вызове метода
increment
. Затем создается экземпляр
этого класса. Потом этот объект передается в замыкание, в результате чего соз
-
дается сильная ссылка на него. Это позволяет объекту оставаться в текущем
состоянии после выхода из области видимости. В конечном итоге, когда вы
-
зывается метод
closure()
, объект все еще хранится в памяти и может увеличить
свой счетчик на единицу при вызове метода
increment
(в данном случае метод
вызывается дважды).
Актуальность объекта обеспечивается особенностями механизма управ
-
ления памятью. Swift и его предшественник Objective-C подвержены целому
классу ошибок, известных как циклические ссылки, когда объект нельзя уда
-
лить после выхода из области видимости, потому что другие объекты удержи
-
вают ссылку на него. Поскольку замыкания создают сильную ссылку на храни
-
мые в них объекты, они фактически добавляют «метку» к объектам, которыми
владеют. Она говорит компилятору, что тот не должен удалять такие объекты
из памяти, потому что они могут продолжать использоваться другими объек
-
тами. В нашем примере это пригодилось в замыкании, потому что
incrementor
продолжает оставаться в области видимости и может вызываться позже, при
обращении к замыканию.
Замыкание, которое передается в параметре и не живет дольше вызываемой
функции, называют
неэкранированным
замыканием. Замыкания этого типа ис
-
пользуются по умолчанию, когда передаются в качестве аргумента. Вот пример
неэкранированного замыкания:
class Incrementor {
var count = 0
func increment(with closure: () > ()) {
count += 1
closure()
}
}
let incrementor = Incrementor()
let printCount = {
print(incrementor.count)
}
incrementor.increment(with: printCount) // 1
incrementor.increment(with: printCount) // 2
Мы изменили метод
increment()
в нашем классе
Incrementor
, чтобы он при
-
нимал замыкание в параметре, поэтому теперь сигнатура метода имеет вид
iOS
101
increment(with:)
. Это замыкание вызывается сразу после увеличения пере
-
менной
count
на единицу. В нашем примере мы передаем замыкание, которое
просто выводит значение
count
, вызывая его непосредственно из объекта
in
crementor
. Мы обращаемся к
incrementor
непосредственно, а это означает, что
компилятор создаст сильную ссылку и
incrementor
будет оставаться в памяти до
завершения приложения.
В нашем примере аргумент
closure
, передаваемый в
increment(with:)
, явля
-
ется неэкранированным замыканием. Оно никогда не живет дольше вызывае
-
мой функции. Давайте теперь рассмотрим экранированное замыкание, чтобы
узнать, как оно выглядит:
class Incrementor {
var count = 0
var printingMethod: (() > ())?
func increment(with closure: () > ()) {
if printingMethod == nil {
printingMethod = closure
}
count += 1
printingMethod?()
}
}
let incrementor = Incrementor()
let printCount = {
print(incrementor.count)
}
incrementor.increment(with: printCount)
incrementor.increment(with: printCount)
Как видите, мы добавили новое свойство
printingMethod
для сохранения по
-
лученного замыкания. Далее, в
increment(with:)
, мы записываем полученное
замыкание в переменную
printingMethod
. Попробовав скомпилировать этот
код, вы получите ошибку компиляции в строке, где выполняется присваивание
переменной
printingMethod
, потому что мы не указали, что это замыкание яв
-
ляется экранированным. Эта проблема решается просто: достаточно добавить
ключевое слово
escape
в объявление параметра
closure
в метод
increment(with:)
,
например:
func increment(with closure: @escaping () > ()) {
if printingMethod == nil {
printingMethod = closure
}
count += 1
printingMethod?()
}
Полный пример теперь выглядит так:
class Incrementor {
var count = 0
var printingMethod: (() > ())?
102
Передача сообщений
func increment(with closure: @escaping () > ()) {
if printingMethod == nil {
printingMethod = closure
}
count += 1
printingMethod?()
}
}
let incrementor = Incrementor()
let printCount = {
print(incrementor.count)
}
incrementor.increment(with: printCount)
incrementor.increment(with: printCount)
Это объявление предотвращает ошибку компиляции. Но мы создали дру
-
гую досадную ошибку, известную как циклическая ссылка. Такие ошибки легко
создаются, но с трудом выявляются. Это не очень заметно в нашем небольшом
примере, но если у вас имеется большой объект или объект, который создается
много раз, вы можете быстро исчерпать доступную память или столкнуться
с нежелательными и неожиданными побочными эффектами. Итак, в чем же
наша ошибка?
Выше мы упоминали, что при создании замыкания создается сильная ссыл
-
ка, мешающая удалению замыкания из памяти, пока не исчезнут все ссылки на
него. В нашем примере мы создаем сильную ссылку на
incrementor
в замыкании
printCount
. Но эта сильная ссылка на замыкание создается при сохранении его
в
printingMethod
внутри метода
increment(with:)
. То есть объект хранит сильную
ссылку на самого себя, из-за чего он никогда не выйдет из области видимости
и не будет удален из памяти!
К счастью, в Swift есть способ превратить сильную ссылку в слабую. Давайте
внутри замыкания объявим
incrementor
как слабую ссылку, например:
let printCount = { [weak incrementor] in
guard let incrementor = incrementor else { return }
print(incrementor.count)
}
Обратите внимание: мы использовали объявление
[weak
incrementor]
, чтобы
подсказать компилятору, что
incrementor
является слабой ссылкой. Мы также
добавили спецификатор
guard
в объявление
incrementor
, чтобы гарантировать
невозможность обращения к ссылке, если она равна
nil
. Так мы разорвали цикл,
потому что теперь
Incrementor
больше не сохраняет сильную ссылку на себя при
сохранении замыкания в
printingMethod
внутри метода
increment(with:)
; он со
-
храняет слабую ссылку. То есть после последнего обращения к
incrementor
ссыл
-
ка обнулится, и объект будет благополучно удален из памяти.
В настоящее время замыкания, безусловно, являются наиболее современ
-
ным способом передачи сообщений между объектами, но иногда их возмож
-
ности избыточны. Кроме того, использование замыканий чревато ошибками,
как только что было показано. Как оказывается, в Cocoa Touch имеются другие
способы передачи сообщений между объектами. Давайте теперь обратим наше
iOS
Do'stlaringiz bilan baham: |