Циклы в didSet

Мы столкнулись с этим странным поведением при использовании циклов в didSet. Идея заключалась в том, что у нас есть тип данных с древовидной структурой, и в каждом элементе мы хотели сохранить уровень, на котором находится этот элемент. Таким образом, в didSet атрибута level мы также устанавливаем атрибут level дочерних элементов. Однако мы поняли, что это работает только при использовании forEach, а не при использовании for .. in. Вот краткий пример:

class Item {

    var subItems: [Item] = []
    var depthA: Int = 0 {
        didSet {
            for item in subItems {
                item.depthA = depthA + 1
            }
        }
    }
    var depthB: Int = 0 {
        didSet {
            subItems.forEach({ $0.depthB = depthB + 1 })
        }
    }

   init(depth: Int) {
        self.depthA = 0

        if depth > 0 {
            for _ in 0 ..< 2 {
                subItems.append(Item(depth: depth - 1))
            }
        }
   }

   func printDepths() {
        print("\(depthA) - \(depthB)")

        subItems.forEach({ $0.printDepths() })
   }
}

let item = Item(depth: 3)
item.depthA = 0
item.depthB = 0
item.printDepths()

Когда я запускаю это, я получаю следующий вывод:

0 - 0
1 - 1
0 - 2
0 - 3
0 - 3
0 - 2
0 - 3
0 - 3
1 - 1
0 - 2
0 - 3
0 - 3
0 - 2
0 - 3
0 - 3

Кажется, что он не будет вызывать didSet атрибута subItems, когда он вызывается из цикла for .. in. Кто-нибудь знает, почему это так?

ОБНОВЛЕНИЕ: проблема не в том, что didSet не вызывается из init. После этого мы меняем атрибут (см. последние 4 строки кода), и только один из двух атрибутов глубины будет передавать новое значение дочерним элементам.


person M. Schrick    schedule 13.02.2018    source источник
comment
Я бы сказал, что это поведение достаточно сложное, чтобы гарантировать функцию. Во-первых, он развеивает любые сомнения относительно того, вызывается ли он в init.   -  person Alexander    schedule 13.02.2018
comment
Это известная ошибка: bugs.swift.org/browse/SR-419. Использование замыкания (и, как отмечали другие, defer) позволяет избежать (в настоящее время нарушенного) семантического анализа, который предотвращает вызов didSet наблюдателя изнутри самого себя; for item in subItems { { item.depthA = depthA + 1 }() } тоже работает.   -  person Hamish    schedule 13.02.2018
comment
И теперь это будет исправлено в режиме Swift 5 :)   -  person Hamish    schedule 27.03.2018


Ответы (2)


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

        for item in subItems {
            defer{
                item.depthA = depthA + 1
            }
        }

Когда вы используете forEach, kindOf "договаривается" с элементами, и поскольку это метод экземпляра, в отличие от цикла for .. in, он запускает didSet переменной. Приведенный выше случай применяется, когда мы используем цикл, мы должны запускать didSet вручную.

Это решает проблему, я думаю. Надеюсь, поможет!!

person Agent Smith    schedule 13.02.2018
comment
Спасибо! Но зачем мне это нужно в цикле for .. in, а в цикле forEach не нужно? - person M. Schrick; 13.02.2018
comment
@ M.Schrick Когда вы используете forEach, он заключает контракт kindOf с элементами, и поскольку это метод экземпляра, в отличие от цикла for .. in, он запускает didSet переменной. Вышеупомянутый случай относится к случаю, когда мы используем цикл, который мы должны запустить didSet вручную. Надеюсь смысл есть? - person Agent Smith; 13.02.2018
comment
У вас случайно нет под рукой ссылки, где я могу прочитать об этом? - person M. Schrick; 13.02.2018
comment
@ M.Schrick Поскольку это функция более высокого порядка (forEach), для получения дополнительной информации вы можете исследовать замыкание, которое она принимает в качестве аргумента. Я думаю, что знакомство с основами работы всех этих функций более высокого порядка прояснило для меня. - person Agent Smith; 13.02.2018

Кажется, что в init didSet не вызывается.

Пробовал эту строку на Swift REPL

class A { var a: Int { didSet { print("A") } }; init() { a = 5 } }

Затем позвонил A()

didSet НЕ вызывается

Но

A().a = 7

Нашел, что didSet называется

Итак, решение состоит в том, чтобы сделать функцию (предпочтительно final), которая делает ваш эффект, который вам нужно поместить в didSet. Затем вызовите его как из didSet, так и из init. Вы можете поместить это в свой класс.

final func updateA() {
    // Do your "didSet" here
}

var a: Int {
    didSet {
        updateA()
    }
}

init() {
    a = 5
    updateA()
}

Итак, в вашем случае:

func updateDepthA() {
    for item in subItems {
        item.depthA = depthA + 1
    }
}

var depthA: Int = 0 {
    didSet {
        updateDepthA()
    }
}

...

init(depth: Int) {
    self.depthA = 0
    updateDepthA()

    ...
person user9335240    schedule 13.02.2018
comment
Проблема не в том, что didSet не вызывается из init. Мы устанавливаем атрибут depth после инициализации (см. последние четыре строки кода), и didSet дочерних атрибутов не будет вызываться, если мы используем цикл for .. in. - person M. Schrick; 13.02.2018