Программирование
Вопрос с собеседования, на котором сыпятся 90% iOS-разработчиков (даже Senior-ы)
Вы увидите точные шаблоны кода, которые используют интервьюеры, узнаете, почему большинство разработчиков всё ещё отвечают неправильно, и как наконец овладеть концептуальным пониманием, которое отличает разработчиков среднего уровня от senior инженеров.
Каждый разработчик iOS знакомо это чувство. Вы сидите напротив интервьюера, MacBook повернут к вам, перед вами фрагмент кода Swift, и вы уже чувствуете подвох. Он всегда замаскирован под что-то безобидное. Замыкание здесь. Небольшой колбек там. Маленький таймер. Такой фрагмент вы видели сотни раз за свою карьеру.
Интервьюер откидывается назад.
«Что не так с этим кодом?»
Вы перечитываете его. Он выглядит нормально. Он компилируется. Он запускается. Он выводит результат. Ничего странного. И всё же вы знаете, что есть что-то скрытое… что-то тонкое… то, что интервьюеры любят, потому что это показывает, действительно ли вы понимаете модель памяти Swift или просто используете шаблоны, потому что их используют другие разработчики.
А потом, через несколько секунд, вас осеняет.
Они не проверяют синтаксис. Они не проверяют знание UIKit. Они проверяют, насколько хорошо вы понимаете владение переменными, ARC, семантику захвата и жизненный цикл.
Это единственный вопрос на собеседовании, который выглядит как разминка перед программированием, но на самом деле является полноценной оценкой. И знаете, что шокирует?
Даже опытные iOS-разработчики проваливают этот тест.
Не потому, что они не знают, в чем разница между weak и unowned. Все это знают. Они проваливают, потому что не знают, зачем им это.
В этой статье подробно рассматривается этот печально известный вопрос на собеседовании. Вы увидите точные шаблоны кода, которые используют интервьюеры, узнаете, почему большинство разработчиков всё ещё отвечают неправильно, и как наконец овладеть концептуальным пониманием, которое отличает разработчиков среднего уровня от senior инженеров.
Давайте сразу же попадём в ловушку — и выйдем из неё умнее.
Невинный на вид фрагмент кода, который всё раскрывает
Вот классическая версия вопроса на собеседовании:
class MyViewController: UIViewController {
var completion: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
completion = {
self.doSomething()
}
}
func doSomething() {
print("Doing something")
}
deinit {
print("MyViewController deinitialized")
}
}
Интервьюер спрашивает: «Почему MyViewController может никогда не быть деалоцирован?»
В этот момент 90% кандидатов отвечают:
«Вам следует использовать weak self».
Неверно.
Не потому, что этот ответ неверный, а потому, что он неполный. Этот вопрос не о том, как исправить. Он о том, как обосновать свой ответ.
Интервьюеры хотят увидеть, понимаете ли вы, как и зачем работают циклы удержания.
Давайте разберёмся, что здесь скрыто.
Что на самом деле тестирует интервьюер
Когда интервьюер задаёт этот вопрос, он на самом деле проверяет вашу способность рассуждать о четырёх ключевых концепциях:
- ARC и сильные ссылки;
- Как замыкания сохраняют значения;
- Экранирующие и неэкранирующие замыкания;
- Время жизни объектов в асинхронных операциях;
А не то, можете ли вы сказать «используйте weak self».
Большинство разработчиков терпят неудачу, потому что начинают с решения, а не с объяснения. Но senior разработчик — именно такой, какой нужен компаниям — объясняет владение, как историю.
Вот чёткое объяснение, которое хочет услышать каждый интервьюер:
completion— это хранимое свойство контроллера представления- замыкание, назначенное
completion, по умолчанию строго захватывает себя
Поэтому:
- контроллер представления строго ссылается на замыкание
- замыкание строго ссылается на контроллер представления
- это цикл удержания
- следовательно,
deinitникогда не запустится
Просто. Точно. Чётко.
Теперь вы показали, что понимаете, что такое владение.
Затем вы рассказываете об исправлении — и о компромиссах, — которые мы скоро рассмотрим.
Но сначала давайте рассмотрим настоящие ловушки, которые расставляют интервьюеры.
Дальнейшие варианты, на которые попадаются даже опытные разработчики
Фрагмент кода выше — это разминка. Большинство опытных разработчиков ошибаются не с ним.
Дело в вариантах — тех, которые скрывают цикл сохранения внутри фреймворка или системного API.
Давайте рассмотрим наиболее распространённые из них.
1. Ловушка таймера
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.updateUI()
}
Почему это ловушка:
- Повторяющийся таймер удерживает своё замыкание
- Замыкание удерживает
self - Контроллер представления обычно удерживает таймер
Это идеальный цикл удержания.
Большинство кандидатов говорят:
«Я буду использовать [weak self]».
Верно, но неполно.
Лучший ответ:
«Вы должны использовать [weak self], а также инвалидировать таймер в deinit. Даже с weak сам таймер сохраняет замыкание активным до отмены».
Этот ответ показывает, что вы понимаете как захват, так и жизненный цикл.
2. Ловушка DispatchQueue.asyncAfter
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.finishSetup()
}
Почему интервьюеры любят этот вопрос:
Потому что большинство разработчиков считают GCD простым. Но вот правда:
asyncAfterудерживает замыкание- замыкание удерживает
self - Если задержка большая, а контроллер представления быстро исчезает, система всё равно удерживает замыкание до его выполнения
Получается скрытая утечка.
Интервьюер ожидает, что вы скажете:
«Используйте [weak self], потому что asyncAfter экранирует и удерживает замыкание до конца выполнения».
Просто. Правильно. Ответ уровня Senior.
3. Ловушка NotificationCenter
observer = NotificationCenter.default.addObserver(
forName: .dataUpdated,
object: nil,
queue: .main
) { notification in
self.handle(notification)
}
Это одна из главных ловушек, поскольку block-based наблюдатели удерживаются.
Большинство разработчиков об этом не помнят.
А поскольку NotificationCenter удерживает замыкание, замыкание сохраняет self — полный цикл удержания.
Ожидаемый ответ:
«Необходимо удалить обсервер в deinit, поскольку блочный наблюдатель строго удерживается в NotificationCenter».
Если вы ответите на этот вопрос чётко, интервьюер будет знать, что вы отлаживали реальные утечки памяти.
4. Ловушка UIView.animate
UIView.animate(withDuration: 0.3) {
self.container.alpha = 0
} completion: { _ in
self.removeChild()
}
Анимации сохраняют замыкания до завершения. Замыкания удерживают self.
Это ещё один цикл.
Большинство разработчиков забывают об этом.
5. Combine или асинхронные фреймворки
Что-то вроде:
publisher.sink { value in
self.apply(value)
}
Или:
Task {
await self.loadData()
}
Оба удерживают замыкания. Замыкания удерживают self.
Именно здесь даже опытные разработчики начинают ошибаться.
Потому что они решили, что это вопрос начального уровня, но интервьюер на самом деле глубоко углубляется в рассуждения о владении.
Почему большинство разработчиков проваливают этот вопрос (настоящая причина)
Разработчики редко задумываются о владельце, пока что-то не рухнет или не произойдет утечка памяти. Swift настолько упрощает управление памятью, что мы вообще забываем об этом думать.
Но интервьюерам нужен человек, который понимает архитектуру, лежащую в основе этого удобства. Тот, кто может предотвратить утечки, а не обнаруживать их спустя месяцы, когда приложение начинает тихо падать.
Ошибка не в незнании weak.
Ошибка в том, что бы думать, что weak это ответ.
Настоящий ответ — это умение объяснить:
- Почему происходит цикл удержания;
- Какой компонент что сохраняет;
- Как ARC ведёт себя при обходе замыканий;
- Как такие фреймворки, как Timer, NotificationCenter, animations, Combine и асинхронные задачи, хранят замыкания;
- Когда
weakуместен; - Когда
unownedбезопасен; - Когда необходимо явно инвалидировать или отменить что-либо.
Это то, что отличает iOS-разработчика с 3-летним стажем от 8-летнего.
И это также то, что превращает кандидата из «возможно» в «нанимаем».
Объясним циклы удержания так, чтобы это понравилось интервьюерам
Вот чёткое объяснение уровня senior, которое вы всегда должны давать:
В Swift замыкания по умолчанию захватывают значения по сильной ссылке. Если view controller хранит замыкание в своём свойстве, а внутри замыкания есть обращение к
self, то контроллер сильно удерживает замыкание, а замыкание — контроллер. Так образуется цикл удержания. ARC не может автоматически разорвать такую сильную циклическую ссылку, поэтому контроллер никогда не освобождается из памяти.
Идеально. Коротко. Ясно.
Теперь давайте углубимся, потому что настоящая статья должна быть полной и практической.
Понимание Strong, Weak и Unowned
Сильная ссылка
Обычная ссылка. Сохраняет объект живым. Замыкания используют сильные захваты, если не указано иное.
Слабая ссылка
- Ссылка не сохраняет объект живым
- Она становится равной
nilпри освобождении объекта - Должна быть опциональной
Пример:
{ [weak self] in self?.run() }
Идеально подходит для UI колбеков.
Unowned
- Также не поддерживает объект в активном состоянии
- Но предполагает, что объект будет существовать всегда
- Вызывает сбой при обращении после освобождения объекта.
Пример:
{ [unowned self] in self.run() }
Используйте только тогда, когда абсолютно уверены в правильности порядка времени жизни.
Интервьюер может спросить:
«Когда следует использовать unowned вместо weak?»
Хороший ответ:
Когда время жизни замыкания гарантированно короче времени жизни объекта. Например, при взаимодействии между представлениями или при ленивых свойствах, которые должны обращаться к
selfво время инициализации.
Это senior-level объяснение.
Исправление классического фрагмента — с компромиссами
Давайте исправим исходный фрагмент:
completion = { [weak self] in
self?.doSomething()
}
Но интервьюера волнует причина, а не синтаксис.
Итак, вот объяснение на уровне senior-а:
«Я использую [weak self] потому что замыкание выходит за пределы области действия функции и может пережить контроллер представления. Слабый self не позволяет замыканию поддерживать контроллер представления активным. Внутри замыкания я безопасно разворачиваю self, потому что к тому времени он может стать равным nil».
Отлично.
Затем интервьюер может спросить:
«Когда unowned безопасен?»
Ваш ответ:
«Unowned безопасен только тогда, когда вы можете гарантировать, что self переживет замыкание. В UI-коде с асинхронными операциями это редко так».
Он кивнет. И что-нибудь запишет в блокнот. Это хороший знак.
Выход за рамки основ: неэкранируемые и экранируемые замыкания
Ещё одна тема для опытных разработчиков: разница между escaping и non-escaping замыканиями.
Неэкранирующее замыкание
- Не может быть сохранено
- Не выходит за пределы тела функции
- Захваченные значения являются временными и не создают долгосрочных циклов удержания
Пример:
func run(_ action: () -> Void) {
action()
}
Это никогда не может привести к циклу удержания.
Экранирующее замыкание
- Можно сохранить и выполнить позже
- Может легко формировать циклы удержания
Пример:
func register(_ action: @escaping () -> Void) {
stored = action
}
Интервьюеры спрашивают об этом специально, чтобы узнать, понимаете ли вы, почему пометка замыкания как @escaping важнее, чем вы думаете.
Часть, которую большинство разработчиков забывают: явное прерывание циклов
Даже если вы правильно используете слабые ссылки, некоторые системы будут продолжать сохранять ваше замыкание до тех пор, пока вы явно не очистите его.
Кандидаты в Senior должны знать следующее:
- Таймеру требуется
invalidate() - Наблюдатели NotificationCenter должны быть удалены
- Подписчики Combine должны быть отменены
- Задачи могут удерживать замыкания, если они не отменены
- Анимации View удерживают замыкание до своего завершения
- Очередь операций OperationQueue удерживает операции до их завершения
Вот где ошибаются junior-разработчики.
Вот где ошибаются и senior, если они не отладили достаточно реальных проблем.
Вот где вы можете выделиться.
Как отладить эти утечки в реальной жизни
Вот процесс отладки, который интервьюеры ожидают от старшего разработчика:
- Добавьте операторы печати в deinit
Если печать не происходит, что-то удерживает объект. - Откройте отладчик графа памяти в Xcode
Ищите сильные цепочки ссылок. - Используйте Instruments (Leaks / Allocations)
Наблюдайте за ростом памяти с течением времени. - Временное обнуление ссылки
Посмотрите, какая из них разрывает цикл.
Если вы упомянете отладчик графа памяти во время своего ответа, интервьюеры мгновенно повысят ваш уровень.
Эта небольшая деталь сигнализирует о том, что вы исправили реальные утечки памяти на уровне прода, а не только знаете вопрос теоретически.
Чистый, готовый к собеседованию ответ-резюме
Вот окончательный, отшлифованный ответ, который вы можете использовать на собеседованиях:
«View Controller хранит замыкание в своём свойстве completion. Замыкание по умолчанию сильно захватывает self, поэтому контроллер сильно удерживает замыкание, а замыкание — контроллер, образуя цикл удержания. ARC не умеет разрывать сильные циклы, поэтому контроллер никогда не освобождается. Чтобы исправить это, используйте перехват вроде [weak self] внутри замыкания и безопасно разворачивайте self. Также учитывайте очистку, зависящую от API: инвалидируйте таймеры, удаляйте наблюдателей из NotificationCenter или отменяйте подписки, чтобы избежать дополнительных удерживаемых ссылок».
Одно это объяснение повысит вашу оценку до 90% кандидатов.
Как объяснить это как Senior разработчик (более простая версия)
Если интервьюер перебивает вас, используйте краткую форму:
«Потому что замыкание экранирует и удерживает self, а self удерживает замыкание».
Кратко. Точно. Демонстрирует понимание.
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46

