Как замыкания в Swift захватывают переменные из окружающего контекста?

«Как замыкания в Swift захватывают переменные из окружающего контекста?» — вопрос из категории Swift Core, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Замыкания захватывают переменные из окружающей области видимости для использования внутри своего тела. По умолчанию захват происходит по сильной ссылке, что может создавать циклы удержания (retain cycles).

Типы захвата:

1. Захват по сильной ссылке (по умолчанию):

class DataManager {
    var data: String = ""

    lazy var processor: () -> Void = {
        print(self.data) // Сильный захват self
    }
}
// Цикл удержания: DataManager ←→ processor

2. Захват по слабой ссылке (weak):

class DataManager {
    var data: String = ""

    lazy var processor: () -> Void = { [weak self] in
        guard let self = self else { return }
        print(self.data) // Временная сильная ссылка
    }
}
// Нет цикла удержания

3. Захват по бесхозной ссылке (unowned):

class Serializer {
    let format: String

    lazy var description: () -> String = { [unowned self] in
        return "Serializer with format: (self.format)"
    }
    // Используется, когда self гарантированно существует
}

4. Захват value types по значению:

var counter = 0
let closure = { [counter] in // Захват копии значения
    print(counter) // Всегда 0, даже если внешний counter изменится
}
counter = 10
closure() // Выводит: 0

Списки захвата (Capture Lists): Списки захвата определяются в квадратных скобках перед списком параметров:

{ [weak viewController, unowned database, count = self.count] in
    // viewController — weak optional
    // database — unowned (не optional)
    // count — копия self.count
}

Правила захвата:

  • Reference types всегда захватываются по ссылке (сильной, слабой или бесхозной)
  • Value types захватываются по значению (копия), если не указано иное
  • self в методах экземпляра захватывается неявно при обращении к свойствам
  • Захват происходит при создании замыкания, а не при его вызове

Пример с асинхронным кодом:

func fetchData(completion: @escaping (Result) -> Void) {
    let requestId = generateId() // Value type

    networkService.request { [weak self] result in
        // Захватываем weak self чтобы избежать цикла
        // requestId захвачен по значению (копия)
        self?.handle(result, requestId: requestId)
        completion(result)
    }
}

Правильное управление захватами критически важно для предотвращения утечек памяти в Swift.