通过函数设置内部时,didSet如何再次调用?

Afs*_*hin 9 swift

据我所知和这个线程中提到的,如果我在其didSet观察者中设置属性值,它不应该再次触发观察者.好的,然后我写了一段这样的代码:

class B {
    var i = 0 {
        didSet {
            print("didSet called")
            self.i += 1
        }
    }
}

var y = B()
y.i = 2
print(y.i)
Run Code Online (Sandbox Code Playgroud)

此代码按预期打印"didSet called"3输出.但我对此代码做了一些小改动,如下所示:

class B {
    var i = 0 {
        didSet {
            print("didSet called")
            doit(val: self)
        }
    }

    func doit(val: B) {
        val.i += 1
    }
}

var y = B()
y.i = 2
print(y.i)
Run Code Online (Sandbox Code Playgroud)

但现在它陷入无限循环打印"didSet called".为什么如果我通过函数参数传递给内部didSet变量的值,它会didSet再次触发吗?由于传递的对象应该引用同一个对象,我不知道为什么会发生这种情况.我测试过,如果我通过闭包didSet而不是普通函数设置它,它会再次进入无限循环.

更新: 有趣的是,即使这会触发无限循环:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit()
        }
    }

    func doit() {
        self.i += 1
    }
}
Run Code Online (Sandbox Code Playgroud)

Afs*_*hin 6

在与 Swift github 核实并询问有关此问题的问题后,我发现这个问题看起来更复杂。但是关于这个问题有一个特定的规则:

didSet仅当didSet可以通过直接内存访问来访问其自己的观察者内的属性时,观察者才会触发。

问题是直接访问属性时会有点模棱两可(除非你是 Swift 的开发者)。对我的问题有影响的一个重要特征是:

实例方法从不直接访问类属性。

这句话显示了我的代码存在问题,尽管我可以争辩说,当您在didSetobserve 中调用实例成员时,它应该能够直接访问属性。当我有这样的代码时:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit()
        }
    }

    func doit() {
        self.i += 1
    }
}
Run Code Online (Sandbox Code Playgroud)

doit()函数无法i直接访问,didSet再次触发导致无限循环。

现在有什么解决方法?

您可以inout用于将属性从其自身传递didSet到实例函数而不触发didSet. 像这样的东西:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit(&i)
        }
    }

    func doit(_ i: inout Int) {
        i += 1
    }
}
Run Code Online (Sandbox Code Playgroud)

还有最后一件事。从Swift 5开始,为自己的属性选择直接内存访问的条件didSet将变得更加受限。基于github,只有使用直接内存访问的条件如下:

 Within a variable's own didSet/willSet specifier, access its storage
 directly if either:
 1) It's a 'plain variable' (i.e a variable that's not a member).
 2) It's an access to the member on the implicit 'self' declaration.
     If it's a member access on some other base, we want to call the setter
     as we might be accessing the member on a *different* instance.
Run Code Online (Sandbox Code Playgroud)

这意味着像下面这样的代码会触发无限循环,而现在不会:

class B {
    var i = 0 {
        didSet {
            print("called")            
            var s = self
            s.i += 1
        }
    }
}
Run Code Online (Sandbox Code Playgroud)