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

Разбор ошибки: к чему приводит обращение к unowned unsafe ссылке после уничтожения объекта владельца?

Разбор ошибки: к чему приводит обращение к unowned(unsafe)-ссылке после уничтожения объекта-владельца?

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

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

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

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

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

Для таких связей появились weak и unowned. Вариант unowned(unsafe) предоставляет ещё меньше гарантий, сохраняя только адресоподобную связь без проверки корректности времени жизни.

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

Если объект хранит unowned(unsafe)-ссылку, эта ссылка не увеличивает счётчик сильных ссылок. Когда последняя сильная ссылка на целевой объект исчезает, объект уничтожается, а unowned(unsafe) продолжает содержать недействительное значение.

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

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

Механизм можно свести к трём свойствам:

  • unowned(unsafe) не владеет объектом и не влияет на его время жизни;
  • ссылка не становится nil после уничтожения объекта;
  • при обращении нет гарантированной проверки, что объект всё ещё существует.

Минимальный пример:

final class Owner {} final class Child { unowned(unsafe) let owner: Owner init(owner: Owner) { self.owner = owner } } var owner: Owner? = Owner() let child = Child(owner: owner!) owner = nil _ = child.owner

После присваивания nil переменной owner объект может быть уничтожен, потому что child.owner не является сильной ссылкой. Последующее обращение к child.owner использует недействительную ссылку; результат нельзя надёжно предсказать.

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

unowned(unsafe) оправдана только при доказуемом внешнем инварианте: целевой объект гарантированно живёт дольше любого использования ссылки. Даже в таком случае выигрыш от отказа от проверки обычно не компенсирует риск use-after-free. Если время жизни может быть неопределённым, следует выбрать weak; если объект обязан существовать, но ошибка должна быть обнаружена явно, — обычную unowned.

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

В высокопроизводительном графе объектов дочерние узлы хранили обратную ссылку на корневой объект. Команда выбрала unowned(unsafe), предполагая, что корень всегда живёт дольше графа. Позже появился отдельный API удаления подграфа, и часть узлов стала использоваться после освобождения корня; ошибка проявлялась нерегулярно и выглядела как повреждение данных.

Рассматривались три варианта. Сильная ссылка устраняла бы обращение к уничтоженному корню, но могла создать цикл и удерживать весь граф в памяти. Weak устраняла use-after-free, однако требовала обработки nil и добавляла ветвление в горячий путь. Обычная unowned сохранила бы модель невладеющей ссылки и завершала бы выполнение при нарушении инварианта.

Выбрали обычную unowned, поскольку жизненный инвариант был ожидаемым, но не абсолютным: ошибка стала диагностируемой, а цикл не возник. unowned(unsafe) оставили только для участков, где время жизни обеспечивалось внешним протоколом и было проверено архитектурно.

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

  1. Увеличивает ли unowned(unsafe) время жизни объекта?

Нет. Такая ссылка не является владельцем и не увеличивает количество сильных ссылок. Объект может быть уничтожен сразу после исчезновения последней сильной ссылки, даже если unowned(unsafe) всё ещё хранится в другом объекте.

  1. Чем нарушение времени жизни отличается для unowned и unowned(unsafe)?

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

  1. Можно ли сделать unowned(unsafe) безопасной проверкой другой переменной?

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