Какую ссылку выбрать для дочернего объекта, если его время жизни гарантированно не превышает время жизни владельца?
В такой ситуации подходит unowned-ссылка: она не увеличивает счётчик сильных ссылок, не становится nil и предполагает, что владелец будет жить дольше дочернего объекта. Если это предположение нарушится, обращение к ссылке завершится аварийно во время выполнения.
ARC автоматизировал управление памятью в Swift: компилятор добавляет операции удержания и освобождения объектов вместо ручного управления счётчиками ссылок. Однако автоматическое управление не может само определить, какие связи между объектами являются владением, а какие описывают уже гарантированно существующую зависимость.
Для этого используются разные виды ссылок. weak предотвращает удержание объекта и обнуляется после его освобождения, а unowned также не удерживает объект, но требует заранее гарантировать корректное время его жизни.
Рассмотрим дочерний объект, который логически принадлежит владельцу и не может существовать без него. Сильная ссылка от дочернего объекта к владельцу создаст цикл: владелец удерживает дочерний объект, а дочерний — владельца. Оба объекта могут остаться в памяти после потери внешних ссылок.
Если заменить обратную ссылку на weak, цикл исчезнет, но ссылка станет опциональной. Если владелец уже освобождён, это безопасно обработается как nil, однако потеря владельца может означать нарушение инварианта модели. unowned позволяет выразить более строгую гарантию, но ошибка в этой гарантии приведёт к аварийному завершению.
unowned не увеличивает число сильных ссылок на объект. При этом Swift не обнуляет такую ссылку после освобождения объекта, поэтому она обычно используется как необязательная только по смыслу, а не по типу: обращаться к ней можно без проверки на nil.
Гарантия должна быть такой: пока существует объект, содержащий unowned-ссылку, объект, на который она указывает, уже создан и ещё не освобождён. Нарушение этой гарантии превращает ссылку в недействительную; чтение или вызов через неё вызывает runtime trap.
Минимальная модель зависимости:
Здесь Parent сильно владеет Child, а Child не владеет Parent. Пока Child доступен через Parent, владелец существует, поэтому unowned выражает ожидаемый инвариант без optional-доступа и без цикла.
Главный компромисс — безопасность против строгости модели. weak предпочтительнее, если объект действительно может исчезнуть раньше или жизненный цикл гарантировать сложно; unowned уместен только при доказуемой зависимости времени жизни. Не следует выбирать его лишь для того, чтобы убрать ? или подавить предупреждение о цикле.
В редакторе документа Document создаёт Selection, а Selection обращается к своему документу для пересчёта диапазона. Документ владеет выбором, выбор не должен продлевать жизнь документа.
Вариант с сильной ссылкой прост, но создаёт цикл и удерживает закрытый документ в памяти. Вариант с weak безопаснее при сложной архитектуре, однако требует обработки nil и допускает состояние, в котором выбор существует без документа.
Команда выбирает unowned, если Selection не передаётся наружу и всегда уничтожается вместе с Document. Это фиксирует архитектурное правило в типе, устраняет цикл без optional-проверок. Если позже выбор начнут хранить отдельно, ссылку нужно будет пересмотреть и, вероятно, заменить на weak или изменить владение.
Что произойдёт при освобождении объекта, на который указывает unowned?
Ссылка не станет nil. Она больше не может безопасно использоваться, и обращение к ней вызовет аварийное завершение во время выполнения. Поэтому unowned нельзя считать просто «weak без optional» — у него другой контракт безопасности.
Всегда ли unowned предпочтительнее weak, если ссылка не должна владеть объектом?
Нет. Отсутствие владения — только часть решения. Если объект может исчезнуть раньше содержащего ссылку объекта, или это трудно доказать для всех путей выполнения, нужен weak. Он обнуляется автоматически и позволяет продолжить работу по безопасной ветке, хотя добавляет optional-семантику.
Почему unowned может скрыть ошибку проектирования жизненного цикла?
Такая ссылка заявляет, что зависимый объект не переживёт владельца. Если на практике зависимый объект помещается в кэш, очередь, замыкание или другой контейнер, гарантия может нарушиться. Программа будет работать до редкого сценария освобождения владельца, после чего обычный доступ к ссылке приведёт к runtime trap; это делает проверку графа владения обязательной при изменении архитектуры.