为什么在更改现有值的成员时运行属性观察器?



请考虑这个 Swift 代码。我有一个包装另一个类的实例的类。当我在持有的值上设置属性时,将运行包装器类的属性观察器。

protocol MyProtocol {
var msgStr: String? { get set }
}
class MyClass: MyProtocol {
var msgStr: String? {
didSet {
print("In MyClass didSet")
}
}
}
class MyWrapperClass {
var myValue: MyProtocol! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
let wrapperObj = MyWrapperClass()
wrapperObj.myValue = MyClass() // Line1
wrapperObj.myValue.msgStr = "Some other string" // Line2

上述代码的输出为:

In MyWrapperClass didSet

In MyClass didSet
In MyWrapperClass didSet

我知道当变量的值发生变化时会调用didSet

因此,当"Line1"处的上述代码执行时,我理解打印了"In MyWrapperClass didSet",这很好。

接下来,当 Line2 执行时,我希望打印"In MyClass didSet",这正确发生,但我不确定为什么打印"In MyWrapperClass didSet",因为属性myValue没有改变。有人可以解释为什么吗?

Swift 需要将myValue.msgStr的突变视为具有值语义;这意味着需要触发myValue上的属性观察者。这是因为:

  1. myValue是一个协议类型的属性(它也恰好是可选的)。此协议不受类限制,因此符合类型可以同时是值类型和引用类型。

  2. myStr属性要求有一个隐式mutating,因为 (1) 和它没有被标记为nonmutating的事实。因此,协议类型的值很可能在变异时发生突变,尽管其myStr要求。

考虑该协议可能已被值类型采用:

struct S : MyProtocol {
var msgStr: String?
}

在这种情况下,msgStr的突变在语义上等效于将具有msgStr突变值的S值重新分配给myValue(有关更多信息,请参阅此问答)。

或者默认实现可以重新分配给self

protocol MyProtocol {
init()
var msgStr: String? { get set }
}
extension MyProtocol {
var msgStr: String? {
get { return nil }
set { self = type(of: self).init() }
}
}
class MyClass : MyProtocol {
required init() {}
}
class MyWrapperClass {
// consider writing an initialiser rather than using an IUO as a workaround.
var myValue: MyProtocol! {
didSet {
print("In MyWrapperClass didSet")
}
}
}

在这种情况下,myValue.myStr的突变会重新分配一个全新的实例给myValue

如果MyProtocol是受类限制的:

protocol MyProtocol : class {
var msgStr: String? { get set }
}

或者,如果msgStr要求指定二传手必须是非变异的:

protocol MyProtocol {
var msgStr: String? { get nonmutating set }
}

那么 Swift 会将myValue.msgStr的突变视为具有引用语义;也就是说,myValue上的属性观察者不会被触发。

这是因为 Swift 知道属性值不能更改:

  1. 在第一种情况下,只有类可以符合,类的属性 setter 不能self变异(因为这是对实例的不可变引用)。

  2. 在第二种情况下,msgStr要求只能由类中的属性(并且此类属性不会改变引用)或值类型中的计算属性来满足,其中 setter 是非变异的(因此必须具有引用语义)。

或者,如果myValue刚刚被键入为MyClass!,你也会得到引用语义,因为 Swift 知道你正在处理一个类:

class MyClass {
var msgStr: String? {
didSet {
print("In MyClass didSet")
}
}
}
class MyWrapperClass {
var myValue: MyClass! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
let wrapperObj = MyWrapperClass()
wrapperObj.myValue = MyClass() // Line1
wrapperObj.myValue.msgStr = "Some other string" // Line2
// In MyWrapperClass didSet
// In MyClass didSet

我怀疑发生这种情况是因为您的protocol未指定为classprotocol。 因此,MyProtocol可能是一个struct,因此当对象以任何方式更改时会触发didSet(这是值类型的正确行为)。

如果您将protocol更改为:

protocol MyProtocol: class {
var msgStr: String? { get set }
}

那么 Swift 知道MyProtocol表示一个引用类型,所以在设置字符串时,MyWrapperClass中不会调用didSetmyValue

它看起来像一个错误,请参阅:https://bugs.swift.org/browse/SR-239

解决方法是预定义变量,例如:

protocol MyProtocol {
var msgStr: String? { get set }
}
class MyClass: MyProtocol {
var msgStr: String? {
didSet {
print("In MyClass didSet")
}
}
}
class MyWrapperClass {
var myValue: MyProtocol! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
let wrapperObj = MyWrapperClass()
wrapperObj.myValue = MyClass() // Line1
var obj = wrapperObj.myValue!
obj.msgStr = "Some other string" // Line2

相关内容

最新更新