Есть ли в Kotlin аналог didSet/willSet?

Мне нравится этот синтаксис Swift; это очень полезно для многих вещей:

var foo: Bar = Bar() {
    willSet {
        baz.prepareToDoTheThing()
    }
    didSet {
        baz.doTheThing()
    }
}

и я хотел бы сделать это в Котлине. Однако Я не могу найти правильный синтаксис!

Есть ли что-нибудь в Котлине?

var foo: Bar = Bar()
    willSet() {
        baz.prepareToDoTheThing()
    }
    didSet() {
        baz.doTheThing()
    }

Ответы

Ответ 1

Хотя Kotlin не предоставляет встроенного Swift-стиля для наблюдения за изменениями свойств, вы все равно можете сделать это несколькими способами в зависимости от вашей цели.

  • observable(...) delegate (в stdlib), который позволяет обрабатывать изменения свойств. Пример использования:

    var foo: String by Delegates.observable("bar") { property, old, new ->
        println("$property has changed from $old to $new")
    }
    

    Здесь "bar" - начальное значение для свойства foo, и lambda вызывается каждый раз после назначения свойства, что позволяет вам наблюдать за изменениями. Также vetoable(...) delegate, который позволяет предотвратить изменение.

  • Вы можете использовать настраиваемый сеттер для свойства для выполнения произвольного кода до/после изменения фактического значения:

    var foo: String = "foo"
        set(value: String) {
            baz.prepareToDoTheThing()
            field = value
            baz.doTheThing()
        }
    

    Как отметил @KirillRakhman, это решение довольно эффективно, так как оно не вводит никаких накладных расходов в вызовы и объекты методов, хотя код будет немного дублирован в случай нескольких свойств.

  • В общем, вы можете реализовать собственный делегат свойств, явно предоставляя поведение свойства в getValue(...) и setValue(...) функциях,

    Чтобы упростить задачу, используйте ObservableProperty<T> абстрактный класс, который позволяет реализовать делегаты, которые наблюдают изменения свойств (например, observable и vetoable выше) Пример:

    var foo: String by object : ObservableProperty<String>("bar") {
        override fun beforeChange(property: KProperty<*>, oldValue: String, newValue: String): Boolean {
            baz.prepareToDoTheThing()
            return true // return false if you don't want the change
        }
    
        override fun afterChange(property: KProperty<*>, oldValue: String, newValue: String) {
            baz.doTheThing()
        }
    }
    

    Для вашего удобства вы можете написать функцию, которая создает объект делегата:

    fun <T> observing(initialValue: T,
                      willSet: () -> Unit = { },
                      didSet: () -> Unit = { }
    ) = object : ObservableProperty<T>(initialValue) {
        override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean =
                true.apply { willSet() }
    
        override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = didSet()
     }
    

    Затем вы просто передаете lambdas ему как willSet и didSet (аргумент по умолчанию для них { }). Использование:

    var foo: String by observing("bar", willSet = {
        baz.prepareToDoTheThing()
    }, didSet = {
        baz.doTheThing()
    })
    
    var baq: String by observing("bar", didSet = { println(baq) })
    

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