Swift: переопределение didSet приводит к рекурсии

Когда переопределение наблюдателя didSet свойства приводит к рекурсии, почему?

class TwiceInt {
    var value:Int  = 0 {
        didSet {
            value *= 2
        }
    }
}

class QuadInt : TwiceInt {
    override var value:Int {
        didSet {
            value *= 4
        }
    }
}

let t = TwiceInt()
t.value = 5 // this works fine


let q = QuadInt()
q.value = 5 // this ends up in recursion

Если я обновлю QuadInt с помощью

class QuadInt : TwiceInt {
    override var value:Int {
        didSet {
            super.value *= 4
        }
    }
}

q.value = 5 // q.value = 80

Итак, я предполагаю, что вызов будет примерно таким:

value = 5
QuadInt:didSet ( value *= 4 )
value = 20
TwiceInt:didSet ( value *= 2 )
value = 40
TwiceInt:didSet ( value *= 2 )
value = 80

Это более или менее похоже на стрельбу в темноте. Есть ли документ о том, что происходит, когда свойство обновляется?


person chunkyguy    schedule 21.01.2015    source источник


Ответы (3)


Вы не можете переопределить didSet, это не обычный метод. На самом деле вы не переопределили didSet, вы переопределили само свойство.

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

Теперь, если вы измените значение в своем собственном наблюдателе didSet, это не вызовет рекурсии, поскольку среда выполнения Swift достаточно умна, чтобы понять, что реализация didSet, изменяющая собственное наблюдаемое свойство, не ожидает повторного вызова после этого. Среда выполнения знает, какой метод didSet выполняется в данный момент, и не будет выполнять этот метод снова, если переменная изменится до возврата этого метода. Эта проверка не работает для суперклассов.

Таким образом, *= 4 вызывает вызов наблюдателя суперкласса, который устанавливает *= 2, и это вызывает повторный вызов наблюдателя подкласса, который снова устанавливает *= 4, вызывая повторный вызов наблюдателя суперкласса... и так далее.

Явным образом используя super, вы разрываете этот цикл, так как теперь вы устанавливаете не свое переопределенное свойство, а унаследованное суперсвойство, и на самом деле вы не наблюдаете это суперсвойство, вы только наблюдаете за своим собственным переопределенным.

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

person Mecki    schedule 06.07.2016

Поместив println() в оба блока didSet, вы увидите, что он неоднократно вызывает сначала суперреализацию, затем переопределение, затем супер, затем переопределение... пока не взорвется.

Я могу только представить, что это ошибка в Swift. Я получаю ту же проблему в Swift 1.2 (в комплекте с бета-версией Xcode 6.3).


Он определенно должен работать, по крайней мере, как я это читал. Из https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html#//apple_ref/doc/uid/TP40014097-CH14-ID254:

ПРИМЕЧАНИЕ

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

и после их образца AudioChannel (цитируется ниже примечания):

ПРИМЕЧАНИЕ

В первой из этих двух проверок наблюдатель didSet устанавливает для currentLevel другое значение. Однако это не приводит к повторному вызову наблюдателя.

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // cap the new audio level to the threshold level
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // store this as the new overall maximum input level
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}
person Fizker    schedule 02.03.2015

по какой-то причине он появляется, несмотря на переопределение, он все еще вызывает didSet суперкласса.

В первом примере вы попадаете в рекурсию, потому что установка quad устанавливает суперкласс didSet, который, в свою очередь, устанавливает набор quads и т. д. и т. д.

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

quad.value = 5

значение =*2(суперкласс didSet) *4(подкласс didSet) *2(суперкласс didSet) =80

person A. Millett    schedule 02.03.2015