Урок по треку Swift и iOS 6 мин

Замыкания и захват значений в Swift

Синтаксис замыканий, захват значений и список захвата, @escaping, autoclosure, trailing closure.

Замыкания в iOS повсюду: completion-обработчики, анимации, подписки, сортировка. На собесе обязательно спросят про захват, @escaping и [weak self].

Синтаксис

let sorted = names.sorted(by: { (a: String, b: String) -> Bool in return a < b })
let sorted2 = names.sorted { $0 < $1 }   // вывод типов, неявный return, trailing closure
let sorted3 = names.sorted(by: <)        // оператор это функция

Если последних аргументов-замыканий несколько, с Swift 5.3 их пишут подряд: UIView.animate(withDuration: 0.3) { ... } completion: { _ in ... }.

Захват переменных

Замыкание захватывает переменную, а не её значение: переменная переезжает в бокс в куче, и замыкание видит последнее значение и может его менять.

var x = 10
let byRef = { print(x) }
let byValue = { [x] in print(x) }   // список захвата: копия в момент создания
x = 20
byRef()     // 20
byValue()   // 10

Классический пример: счётчик.

func makeCounter() -> () -> Int {
    var n = 0
    return { n += 1; return n }
}
let a = makeCounter()
a(); a()     // 1, 2
let b = a
b()          // 3: замыкания ссылочные, состояние общее

В цикле for-in каждая итерация создаёт новую константу, поэтому замыкания, созданные в цикле, увидят свои значения. Если же все замыкания захватили одну внешнюю var, изменяемую в while, они увидят её финальное значение.

escaping и non-escaping

Замыкание-параметр по умолчанию non-escaping: оно гарантированно не переживёт вызов функции. Компилятору это позволяет держать контекст на стеке и не требовать явного self.

Если замыкание сохраняется в свойство или вызывается асинхронно, нужен @escaping:

final class Loader {
    private var handlers: [(Data) -> Void] = []
    func load(completion: @escaping (Data) -> Void) {
        handlers.append(completion)
    }
}

Опциональное замыкание (() -> Void)? всегда escaping, атрибут к нему не пишется.

Ограничение: escaping-замыкание не может захватить inout-параметр, а значит и self внутри mutating-метода структуры.

self и циклы ссылок

В escaping-замыкании внутри класса self нужно писать явно: это напоминание, что вы захватываете сильную ссылку. Если объект хранит замыкание, а замыкание держит объект, получается цикл, и ARC их не освободит.

final class ProfileViewModel {
    var onUpdate: (() -> Void)?
    func bind() {
        api.subscribe { [weak self] profile in
            guard let self else { return }
            self.profile = profile
            self.onUpdate?()
        }
    }
}
  • [weak self]: замыкание не удерживает объект, self становится опциональным.
  • [unowned self]: без опционала, но крэш, если объект уже освобождён. Только когда замыкание точно не переживёт объект.

Когда weak не нужен: в non-escaping замыканиях (map, filter, UIView.animate) и в однократных асинхронных вызовах, где замыкание не хранится объектом (DispatchQueue.main.async). Там жизнь self лишь немного продлевается.

Ловушка с lazy: lazy var handler = { self.doWork() } создаёт цикл, потому что замыкание хранится в свойстве объекта.

@autoclosure

Оборачивает выражение-аргумент в замыкание, которое вычисляется по требованию:

func log(_ enabled: Bool, _ message: @autoclosure () -> String) {
    if enabled { print(message()) }
}
log(false, heavyDescription())   // heavyDescription не вызовется

Так устроены assert, precondition, правые части ?? и &&.

@Sendable

Замыкание, которое может выполниться в другом домене конкурентности, должно быть @Sendable: оно захватывает только Sendable-значения и не мутирует захваченные переменные. Подробнее в модуле про Swift Concurrency.

Частые ошибки

  • Забыть [weak self] в замыкании, которое объект хранит (подписки, обработчики, таймеры).
  • Ставить [weak self] везде подряд, даже в map, и плодить guard let self.
  • [unowned self] в сетевом колбэке: экран закрыли, ответ пришёл, крэш.
  • Ожидать, что замыкание «запомнило» значение переменной без списка захвата.

Что спросят на собесе

  • Как замыкание захватывает переменные, что такое список захвата?
  • Что такое @escaping и почему по умолчанию non-escaping?
  • Когда нужен [weak self], а когда [unowned self]?
  • Что выведет код со счётчиком на замыкании?
  • Что делает @autoclosure и где он используется в стандартной библиотеке?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.