Программирование SwiftARC и памятьСтарший разработчик iOS на Swift

Почему замена weak на unowned может уменьшить накладные расходы, но не устраняет риск обращения к уже уничт...

Почему замена weak на unowned может уменьшить накладные расходы, но не устраняет риск обращения к уже уничтоженному объекту?

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

Краткий ответ

weak — обнуляемая ссылка: она не владеет объектом, а при его уничтожении автоматически становится nil. unowned тоже не владеет объектом, но не обнуляется; обращение к ней после уничтожения объекта приводит к ошибке выполнения. Поэтому unowned может быть дешевле по runtime-обслуживанию, однако требует доказуемо более долгого времени жизни объекта.

Исторический контекст

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

weak предназначена для ситуаций, где объект может исчезнуть раньше ссылки. unowned предназначена для отношений, в которых цель обязана жить не меньше, чем ссылка. Такое разделение позволяет явно выразить контракт владения и выбрать компромисс между безопасностью и дополнительным runtime-обслуживанием.

Постановка проблемы

Если заменить weak на unowned только ради производительности, можно получить обращение к уже освобождённому объекту. Ошибка проявится не обязательно в момент нарушения логики: она возникнет позже, когда будет прочитана ссылка, и может привести к аварийному завершению приложения.

У weak runtime должен отслеживать объект и связанные с ним слабые ссылки, чтобы обнулить их при уничтожении. У обычной unowned нет такого обнуления, поэтому она не превращается в nil и не требует той же модели zeroing-ссылки. Но это не делает объект живым и не добавляет владения.

Подробное решение

При создании weak-ссылки сильный счётчик объекта не увеличивается. Ссылка хранится как специальная отслеживаемая запись; когда объект уничтожается, runtime делает связанные слабые ссылки равными nil. Чтение weak обычно даёт optional-значение, поэтому код обязан учитывать отсутствие объекта.

unowned также не увеличивает сильный счётчик и не продлевает время жизни объекта. В отличие от weak, она хранит ссылку без автоматического обнуления. Безопасная unowned при обращении проверяет, существует ли объект; если контракт нарушен, программа завершается с ошибкой выполнения, а не получает nil.

Потенциальная экономия unowned связана с отсутствием zeroing-механизма и optional-семантики. Это не универсальная гарантия заметного ускорения: стоимость зависит от реализации runtime, частоты доступа и общего профиля приложения. Выбор следует делать по графу владения и гарантии времени жизни, а не по предположению, что unowned всегда быстрее.

unowned(unsafe) идёт ещё дальше: runtime-проверка времени жизни не выполняется. После уничтожения цели обращение к такой ссылке имеет неопределённые последствия, поэтому этот вариант нельзя использовать как обычную оптимизацию.

Практическое правило: выбирайте weak, если цель может исчезнуть раньше владельца ссылки; выбирайте unowned, только если это инвариант архитектуры и он обеспечивается самим графом владения. Если инвариант невозможно доказать, безопаснее обработать отсутствие объекта через weak.

Ситуация из практики

Экран хранит callback, а callback обращается к модели. Если модель может быть удалена раньше экрана, захват модели через unowned опасен: callback сохранится, модель исчезнет, и следующий вызов завершит приложение. Вариант с weak безопаснее, но callback должен корректно обработать nil; его недостаток — дополнительное runtime-обслуживание слабой ссылки.

Другой вариант — изменить владение так, чтобы экран действительно гарантированно жил не дольше модели. Тогда unowned точно выражает контракт и может избежать zeroing-накладных расходов. Это решение оправдано только после проверки всех путей уничтожения, включая отмену операций, отписку и фоновые callback.

На практике выбирают weak, если жизненный цикл управляется внешними событиями или асинхронным кодом. unowned оставляют для строгих отношений вроде объекта-компонента, когда нарушение порядка уничтожения означает ошибку архитектуры; результатом становится отсутствие аварий при штатном жизненном цикле и предсказуемый fail-fast при нарушении инварианта.

Что кандидаты часто упускают

  1. Увеличивает ли unowned сильный счётчик объекта?

    Нет. Обычная unowned не владеет объектом и не продлевает его жизнь. Она лишь предполагает, что объект будет существовать к моменту каждого обращения. Если это требуется обеспечить самой ссылкой, нужна сильная ссылка, а не unowned.

  2. Можно ли заменить weak на unowned, если объект обычно не уничтожается раньше callback?

    Нет, слова «обычно» недостаточно. Контракт должен быть гарантирован для всех путей: раннего закрытия экрана, отмены задачи, удаления из контейнера, ошибок загрузки и конкурентного завершения операций. Если хотя бы один допустимый сценарий допускает более раннее уничтожение цели, нужна weak либо изменение архитектуры владения.

  3. Чем unowned(unsafe) отличается от обычной unowned с точки зрения последствий ошибки?

    Обычная unowned проверяет, что цель ещё жива, и при нарушении контракта аварийно завершает выполнение контролируемым runtime-ошибочным сценарием. unowned(unsafe) не выполняет такую проверку; обращение после уничтожения объекта может привести к чтению недействительной памяти и неопределённому поведению. Поэтому её применение требует ещё более строгой гарантии времени жизни и обычно не оправдывается без измерений.