Avatar ·

Можно ли заставить didSet вызываться при инициализации в Swift?

Привет! Я новичок в Swift и столкнулся с неожиданным поведением наблюдателей свойств. У меня есть класс с stored property, для которого я установил didSet, чтобы выполнять валидацию или обновлять UI при каждом изменении значения. Но я заметил, что didSet не вызывается, когда я устанавливаю значение свойства в инициализаторе. Вот пример: ```swift class Temperature { var celsius: Double = 0 { didSet { print("Температура изменилась на \(celsius)°C") // Здесь я хочу обновить UI или сделать валидацию } } init(celsius: Double) { self.celsius = celsius // didSet НЕ вызывается! } } ``` При создании объекта `let temp = Temperature(celsius: 25)` я ожидаю увидеть в консоли "Температура изменилась на 25.0°C", но ничего не происходит. Я понимаю, что это особенность Swift, но мне нужно, чтобы didSet отрабатывал при любом присваивании, включая инициализацию. Как можно обойти это ограничение? Есть ли какой-то элегантный способ заставить наблюдатель работать в инициализаторе, не прибегая к грязным хакам? Заранее спасибо за помощь! <<>>

Почему didSet не вызывается при инициализации?

Это поведение заложено в Swift намеренно. Во время инициализации свойствам только присваиваются начальные значения, и наблюдатели (willSet/didSet) игнорируются. Swift считает, что в этот момент объект ещё не полностью готов к использованию, поэтому вызов наблюдателей может привести к нежелательным побочным эффектам (например, обращение к другим, ещё не инициализированным свойствам).

Решение №1: Вызов didSet вручную (простой способ)

Самый прямолинейный способ — явно вызвать код из didSet после присваивания в инициализаторе:

class Temperature {
    var celsius: Double = 0 {
        didSet {
            print("Температура изменилась на \(celsius)°C")
            validateTemperature()
        }
    }
    
    init(celsius: Double) {
        self.celsius = celsius
        // Явно вызываем логику didSet
        print("Температура изменилась на \(celsius)°C")
        validateTemperature()
    }
    
    private func validateTemperature() {
        // Валидация или обновление UI
    }
}

Минусы: дублирование кода. Если логика в didSet сложная, легко забыть обновить её в инициализаторе.

Решение №2: Использование defer (элегантнее)

Можно поместить вызов наблюдателя в блок defer, который выполнится после завершения инициализации:

class Temperature {
    var celsius: Double = 0 {
        didSet {
            print("Температура изменилась на \(celsius)°C")
        }
    }
    
    init(celsius: Double) {
        defer { self.celsius = celsius }
        // Другая инициализация, если нужно
    }
}

Хитрость в том, что defer выполняется после того, как все свойства уже инициализированы. Присваивание внутри defer запускает didSet, так как объект уже считается полностью инициализированным.

Решение №3: Фабричный метод (рекомендуемый)

Лучшая практика — вынести создание объекта в статический фабричный метод, где didSet будет работать нормально:

class Temperature {
    var celsius: Double = 0 {
        didSet {
            print("Температура изменилась на \(celsius)°C")
        }
    }
    
    private init() {} // Приватный инициализатор
    
    static func create(celsius: Double) -> Temperature {
        let temp = Temperature()
        temp.celsius = celsius // Здесь didSet уже вызывается!
        return temp
    }
}

// Использование
let temp = Temperature.create(celsius: 25) // "Температура изменилась на 25.0°C"

Плюсы: чистый код, нет дублирования, инициализация через публичный интерфейс.

Решение №4: computed property с backing storage (для сложных случаев)

Если у вас много свойств и сложная логика, используйте отдельное хранилище:

class Temperature {
    private var _celsius: Double = 0
    
    var celsius: Double {
        get { _celsius }
        set {
            _celsius = newValue
            print("Температура изменилась на \(_celsius)°C")
        }
    }
    
    init(celsius: Double) {
        self.celsius = celsius // Вызовет set и print
    }
}

Здесь мы полностью контролируем процесс присваивания, и наблюдатель срабатывает всегда.

Итог

  • Для простых случаев — используйте фабричный метод или defer.
  • Для сложных проектов — computed property с backing storage даёт полный контроль.
  • Избегайте дублирования кода из didSet в инициализаторе — это чревато ошибками.

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

<<>>

Альтернативный подход: Использование willSet и проверка инициализации

Можно комбинировать willSet и didSet, чтобы элегантно обрабатывать начальное присваивание. Добавьте флаг, который показывает, завершена ли инициализация:

class Temperature {
    private var isInitialized = false
    var celsius: Double = 0 {
        willSet {
            if isInitialized {
                // Действия до изменения, только после инициализации
            }
        }
        didSet {
            if isInitialized {
                print("Температура изменилась на \(celsius)°C")
            } else {
                // Первое присваивание — выполняем нужные действия
                print("Начальная температура установлена: \(celsius)°C")
                isInitialized = true
            }
        }
    }
    
    init(celsius: Double) {
        self.celsius = celsius
        // isInitialized ещё false, didSet выполнит только начальную логику
    }
}

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

Этот подход удобен, когда нужно различать первое присваивание (в инициализаторе) и последующие изменения. Например, для аналитики или логирования первого запуска.