Что произойдёт с willSet и didSet хранимого свойства при присваивании ему начального значения внутри инициа...

Что произойдёт с willSet и didSet хранимого свойства при присваивании ему начального значения внутри инициализатора?

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

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

При присваивании начального значения хранимому свойству внутри инициализатора willSet и didSet не вызываются. Наблюдатели начинают работать после завершения инициализации, когда свойство изменяется обычным присваиванием.

Это правило относится к этапу создания объекта: Swift записывает начальные значения напрямую, не рассматривая их как последующие изменения уже существующего экземпляра.

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

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

Во время инициализации объект ещё не считается полностью готовым. Поэтому запускать наблюдатели на этом этапе было бы опасно: другие свойства могут быть не инициализированы, а методы или внешние компоненты могут увидеть частично созданный объект.

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

Если разработчик ожидает вызов didSet при начальном присваивании, он может ошибочно рассчитывать на первичную настройку интерфейса, регистрацию события или вычисление зависимого состояния.

В результате начальное состояние будет установлено, но побочный код наблюдателя не выполнится. При этом последующее присваивание тому же свойству после завершения инициализатора уже вызовет наблюдатели.

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

Swift разделяет два процесса:

  • инициализация — формирование корректного начального состояния экземпляра;
  • мутация после инициализации — изменение состояния готового экземпляра.

Начальные значения хранимых свойств, заданные непосредственно в инициализаторе или через значения по умолчанию, не считаются наблюдаемыми изменениями. Поэтому willSet и didSet пропускаются.

После завершения инициализатора обычное присваивание вызывает наблюдатели в таком порядке: сначала willSet, затем фактическая запись нового значения, затем didSet. В willSet доступно новое значение, а в didSet — старое значение через специальное имя oldValue.

final class Profile { var name: String { willSet { print("willSet") } didSet { print("didSet") } } init(name: String) { self.name = name print("init done") } } var profile = Profile(name: "A") profile.name = "B"

При создании экземпляра напечатается только init done. При последующем присваивании будут вызваны сначала willSet, затем didSet.

Наблюдатели применяются к хранимым свойствам. Для вычисляемого свойства вместо них используются собственные get и set. Если необходима общая логика и для начального состояния, и для последующих изменений, её следует вызвать явно после инициализации либо вынести в отдельный метод, отвечающий за синхронизацию состояния.

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

Есть модель экрана с хранимым свойством status. В didSet разработчик обновляет индикатор загрузки. При создании модели статус получает значение ready, но индикатор не обновляется, потому что это начальная инициализация, а не последующая мутация.

Возможны три решения. Можно вручную вызвать обновление в конце инициализатора: это просто, но создаёт зависимость модели от конкретного побочного действия. Можно заменить свойство вычисляемым: это подходит, если обновление полностью выражается через get и set, но усложняет хранение состояния. Можно вынести синхронизацию в отдельный метод applyCurrentState и вызывать его после создания модели и в местах последующих изменений.

На практике обычно выбирают явную синхронизацию состояния на границе модели и интерфейса. Наблюдатель оставляют для изменений после инициализации, а первичную настройку выполняют отдельным вызовом. Так поведение не зависит от невызываемых на этапе инициализации наблюдателей, а ответственность за начальное отображение остаётся очевидной.

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

  1. Вызываются ли наблюдатели, если новое значение равно старому?

Да. Само присваивание после инициализации считается изменением свойства, поэтому willSet и didSet обычно вызываются даже при равных старом и новом значениях. Если повторная обработка нежелательна, сравнение значений нужно выполнить явно, например в начале didSet, используя oldValue.

  1. Что произойдёт при передаче свойства в функцию через inout?

Для операции inout Swift временно предоставляет функции доступ к переменной, а затем записывает результат обратно. Наблюдатели свойства не должны восприниматься как вызываемые при каждом внутреннем изменении локального значения; для свойства они срабатывают при возврате результата и записи обратно. Поэтому код не следует проектировать с расчётом на несколько вызовов didSet внутри одной операции inout.

  1. Можно ли безопасно присвоить наблюдаемому свойству новое значение внутри его didSet?

Да, присваивание этому же свойству из собственного наблюдателя не приводит к бесконечному повторному вызову того же наблюдателя. Это позволяет, например, нормализовать значение непосредственно в didSet, однако такой подход следует применять осторожно: сложные взаимные изменения нескольких свойств могут создать трудно отслеживаемые побочные эффекты. Для нетривиальной валидации часто яснее использовать отдельный метод или вычисляемое свойство с контролируемым set.