При замене значения свойства определите, когда старый объект может быть уничтожен относительно didSet.
final class Token {
let id: String
init(_ id: String) { self.id = id }
deinit { print("deinit", id) }
}
final class Box {
var token: Token? {
didSet { print("didSet", oldValue?.id ?? "nil") }
}
}
let box = Box()
box.token = Token("A")
box.token = Token("B")
При второй записи Token("A") не может быть уничтожен до выполнения тела didSet, поскольку старое значение передаётся наблюдателю как oldValue. Если других сильных ссылок на объект нет, его deinit будет вызван после использования oldValue в didSet, когда временное удержание старого значения исчезнет.
Для приведённого кода существенный порядок такой: сначала печатается didSet A, затем становится возможным вызов deinit A. Создание Token("B") не уничтожает старый объект раньше: до завершения присваивания старое значение ещё нужно передать наблюдателю.
ARC появился в Swift как способ автоматически управлять подсчётом сильных ссылок без ручных вызовов retain/release. Это уменьшает количество ошибок управления памятью, но требует понимать границы владения: компилятор добавляет операции удержания и освобождения с учётом семантики языка.
Наблюдатели свойств добавляют к присваиванию обязательную точку выполнения пользовательского кода. Поэтому старое значение нельзя освободить до того, как оно станет доступно через oldValue.
При присваивании нового значения сильному свойству старый объект обычно теряет одну сильную ссылку. Если это была последняя ссылка, объект становится кандидатом на уничтожение. Ошибка в рассуждении состоит в предположении, что освобождение свойства обязательно происходит до didSet.
Это важно, если didSet читает свойства старого объекта, передаёт его другому владельцу или запускает код, зависящий от порядка сообщений и deinit. Нельзя строить логику только на приблизительном представлении «сначала присваивание, потом всё остальное».
При записи box.token = newValue Swift сохраняет старое значение для наблюдателя, устанавливает новое значение и вызывает didSet. Внутри наблюдателя старый объект доступен через oldValue, поэтому его время жизни должно продолжаться как минимум до завершения необходимого обращения к этому значению.
После выполнения наблюдателя временное удержание oldValue освобождается. Если свойство уже содержит новый объект и других сильных ссылок на старый объект нет, ARC уменьшает счётчик сильных ссылок до нуля и запускает deinit старого объекта.
При второй записи oldValue указывает на first, поэтому сначала выполняется печать old: first. Затем, если дополнительных владельцев нет, вызывается deinit first. Точный момент освобождения после последнего необходимого использования может оптимизироваться компилятором, но ARC не может уничтожить объект до обращения, требуемого семантикой oldValue.
Наблюдатель не делает старый объект доступным после завершения didSet. Если его нужно сохранить дольше, следует явно создать сильную ссылку, например передать объект в другой владеющий объект или сохранить его в локальной переменной; полагаться на область видимости свойства нельзя.
В компоненте интерфейса свойство currentTask заменяется при запуске новой операции, а didSet записывает сведения о завершившейся задаче. Разработчик ожидает, что deinit старой задачи произойдёт до записи в журнал, и удаляет из deinit данные, которые ещё нужны журналированию.
Вариант с использованием deinit для журналирования ненадёжен: deinit вызывается только при исчезновении последней сильной ссылки, а не строго в момент присваивания. Дополнительные владельцы, временные значения или асинхронные операции могут продлить жизнь объекта.
Надёжнее использовать oldValue непосредственно в didSet для формирования записи, а deinit оставить для освобождения ресурсов, которые действительно принадлежат объекту. Такой подход не зависит от точного момента уничтожения и сохраняет корректность при появлении новых владельцев.
1. Вопрос: гарантирует ли didSet, что старый объект будет уничтожен сразу после его завершения?
Нет. didSet гарантирует доступность старого значения во время наблюдателя, но не гарантирует, что это последний владелец. Другие свойства, коллекции, замыкания или временные значения могут продолжать сильно удерживать объект.
2. Вопрос: вызовется ли didSet, если новое и старое значения ссылаются на один экземпляр?
Наблюдатель свойства вызывается при присваивании значения свойству, даже если ссылка указывает на тот же экземпляр; проверка идентичности не является автоматическим условием подавления наблюдателя. При этом объект не уничтожается: присваивание той же сильной ссылки не приводит к потере последнего владения.
3. Вопрос: можно ли в didSet безопасно передать oldValue в асинхронное замыкание?
Да, если замыкание захватит oldValue сильной ссылкой. Тогда замыкание станет дополнительным владельцем объекта и продлит его жизнь; deinit может выполниться значительно позже завершения didSet. Если это нежелательно, нужно явно выбрать другую политику захвата, например weak, учитывая риск получить nil к моменту выполнения замыкания.