3

请考虑这个 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

上面代码的输出是:

在 MyWrapperClass didSet
在 MyClass didSet
在 MyWrapperClass didSet

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

因此,当执行“Line1”的上述代码时,我知道打印了“In MyWrapperClass didSet”,这很好。

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

4

3 回答 3

4

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

  1. myValue是一个协议类型的属性(也恰好是可选的)。该协议不是类绑定的,因此符合类型可以是值类型和引用类型。

  2. 由于(1) 和未标记的事实,myStr属性要求具有隐式设置器。因此,协议类型的值很可能会根据其要求在变异时发生变异。mutatingnonmutatingmyStr

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

struct S : MyProtocol {
  var msgStr: String?
}

在这种情况下, 的突变在msgStr语义上等同于重新分配S具有突变值的值msgStrback to myValue(有关更多信息,请参阅此 Q&A)。

或者默认实现可能已重新分配给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要求已指定 setter 必须是非变异的:

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

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

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

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

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

或者,如果myValue刚刚键入 as 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
于 2018-02-17T16:25:12.260 回答
2

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

如果您更改protocol为:

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

那么 Swift 知道它MyProtocol代表一个引用类型,所以在设置字符串时didSet不会被调用。myValueMyWrapperClass

于 2018-02-17T15:30:59.067 回答
0

它看起来像一个错误,请参阅: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
于 2018-02-17T15:17:40.283 回答