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

Когда переопределение наблюдателя 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

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

Ответ 1

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

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

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

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

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

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

Ответ 2

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

Я могу только понять, что это ошибка в 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:

Примечание

Если вы присвоите значение свойству в своем собственном наблюдателе bySet, новое назначенное вами значение заменит тот, который был только что установлен.

и после их образца 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
            }
        }
    }
}

Ответ 3

Проблема заключается в том, что вы не должны использовать didSet для изменения значения, потому что это вызовет рекурсию, вместо этого вы должны использовать set:

var TwiceInt : Int {
    get {
        return TwiceInt
    }
    set (newValue) {
        TwiceInt *= 2
    }
}

Ответ 4

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

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

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

quad.value = 5

value = * 2 (superclass didSet) * 4 (subClass didSet) * 2 (superClass didSet) = 80