请考虑这个 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 didSetIn MyClass didSet
In MyWrapperClass didSet
我知道当变量的值发生变化时会调用didSet
。
因此,当"Line1"处的上述代码执行时,我理解打印了"In MyWrapperClass didSet",这很好。
接下来,当 Line2 执行时,我希望打印"In MyClass didSet",这正确发生,但我不确定为什么打印"In MyWrapperClass didSet",因为属性myValue
没有改变。有人可以解释为什么吗?
Swift 需要将myValue.msgStr
的突变视为具有值语义;这意味着需要触发myValue
上的属性观察者。这是因为:
myValue
是一个协议类型的属性(它也恰好是可选的)。此协议不受类限制,因此符合类型可以同时是值类型和引用类型。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 知道属性值不能更改:
在第一种情况下,只有类可以符合,类的属性 setter 不能
self
变异(因为这是对实例的不可变引用)。在第二种情况下,
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
未指定为class
protocol
。 因此,MyProtocol
可能是一个struct
,因此当对象以任何方式更改时会触发didSet
(这是值类型的正确行为)。
如果您将protocol
更改为:
protocol MyProtocol: class {
var msgStr: String? { get set }
}
那么 Swift 知道MyProtocol
表示一个引用类型,所以在设置字符串时,MyWrapperClass
中不会调用didSet
myValue
。
它看起来像一个错误,请参阅: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