ПрограммированиеiOS/Mobile разработчик

Как работают closures (замыкания) в Swift, чем они отличаются от функций и какие аспекты управления памятью связаны с их использованием?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

Замыкания как концепция пришли из функциональных языков и позволяют передавать блоки кода как значения. В Swift closures (аналог лямбд из других языков) появились с самого начала. Благодаря этому можно элегантно реализовывать асинхронные вызовы, делегировать действия, сортировать коллекции, фильтровать и т.д.

Проблема:

Closures захватывают переменные из окружающего контекста. Если среди этих переменных есть ссылки на объекты класса, может возникнуть retain cycle, особенно если closure хранится как свойство этого класса. Также важно понимать разницу между escaping и non-escaping closures, и осознавать, как работает захват значений.

Решение:

Swift реализует closures как самостоятельные объекты, они могут захватывать контекст, а владелец closure должен быть осторожен с архитектурой.

Пример кода:

class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("Completed work!") } } } // Захват self внутри closure class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }

Ключевые особенности:

  • closure может захватывать значения по ссылке или по значению
  • escaping/non-escaping влияет на жизненный цикл closure
  • closure в качестве хранящихся свойств легко создают retain cycle

Вопросы с подвохом.

Если closure не захватывает self явно, может ли возникнуть retain cycle?

Да, если closure сохраняется как strong property класса и обращается к self внутри, возникает retain cycle, даже если вы явно не пишете self. Используйте [weak self]/[unowned self] или делайте closure local, если это возможно.

В чем отличие между escaping и non-escaping closure?

Escaping closure может быть вызван после завершения функции и обычно используется для асинхронных операций. Non-escaping выполняется до окончания выполнения функции. У escaping capturing порядка захвата self другой.

Пример кода:

func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }

Может ли closure изменять значения захваченных переменных?

Да, если closure захватил переменную, объявленную как var, он может менять ее значение (для value типов). Для класса это будет ссылка и изменять свойства можно всегда.

Пример кода:

var value = 1 let closure = { value += 1 } closure() print(value) // 2

Типовые ошибки и анти-паттерны

  • Установка closure как свойства класса и обращение к self без [weak self].
  • Объявление closure слишком крупными — затрудняет понимание и отладку.
  • Использование escaping там, где можно обойтись non-escaping.

Пример из жизни

Негативный кейс

ViewController сохраняет closure как свойство (например, completionHandler) и внутри обращается к self напрямую. В результате возникает retain cycle: ViewController => closure => ViewController. Отключение экрана не освобождает память.

Плюсы: Код лаконичный и короткий на вид.

Минусы: Утечки памяти, ViewController "зависает" в памяти, потенциальные баги.

Позитивный кейс

Используется [weak self] или [unowned self] внутри closure, или closure хранится не дольше жизненного цикла объекта. Review подобных мест на code-review.

Плюсы: Корректное освобождение ресурсов, отсутствие непредсказуемых утечек.

Минусы: [weak self] требует внимательности при разыменовании, возможны неявные краши при неправильном использовании.