【问题标题】:Why does a property observer run when a member of the existing value is changed?为什么更改现有值的成员时会运行属性观察器?
【发布时间】:2018-02-17 14:19:37
【问题描述】:

请考虑这个 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是在变量值改变时调用的。

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

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

【问题讨论】:

  • 您能否向我们展示您对这些属性进行实际分配的代码?
  • @ItaiFerber 最后三行代码sn-p。
  • 与您的问题完全无关,但不要使用隐式展开的可选属性来替代为您的类编写初始化程序(在这种情况下,属性可以是非可选的)。跨度>

标签: swift properties didset property-observer


【解决方案1】:

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

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

  2. myStr 属性要求有一个隐含的 mutating 设置器,因为 (1) 以及它没有被标记为 nonmutating 的事实。因此,协议类型的值很可能通过其myStr 要求在变异时发生变异。

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

struct S : MyProtocol {
  var msgStr: String?
}

在这种情况下,msgStr 的突变在语义上等同于将具有 msgStr 突变值的 S 值重新分配回 myValue(请参阅this Q&A for more info)。

或者默认实现可以重新分配给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 刚刚被键入为 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

【讨论】:

  • 好的,所以这个想法是——“每当一个值类型改变,无论是它本身还是它的一个属性,它就像创建一个新副本。”我的 sn-p 打印了额外的“In MyWrapperClass didSet”,因为(1)对象是协议类型,(2)协议的实现者也可以是值类型,因为协议不限于引用类型。对吗?
  • @AkshayShah 是的,当场 :) 虽然严格来说不仅仅是使用值类型,正如我在回答中所说,协议扩展(非类绑定协议)允许重新分配给self 在变异上下文中(例如尚未标记为 nonmutating 的属性设置器)。但是,一旦您将协议绑定到类,这将变得非法,因为属性设置器变得不可变(以提供引用语义)。
【解决方案2】:

我怀疑这是因为您的 protocol 未指定为 class protocol。因此,MyProtocol 可能是 struct,因此当对象以任何方式更改时都会触发 didSet(这对于值类型来说是正确的行为)。

如果您将protocol 更改为:

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

那么 Swift 就知道MyProtocol 代表一个引用类型,所以在设置字符串时,MyWrapperClass 中的myValue 不会被调用didSet

【讨论】:

    【解决方案3】:

    它看起来像一个错误,请参阅: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
    

    【讨论】:

    • 那个错误只是在谈论从触发didSet的协议类型值的属性中获取一个值,这绝对是错误的。然而,在 OP 的情况下,他们正在为所述属性分配一个新值,这应该会触发 didSet,因为这种突变 can (语义和字面上)会改变协议类型的值(作为属性要求)有一个隐含的mutating setter)。
    • 目前的情况是类的错误,但不是结构的错误。值由相同的值重新分配,这是不正确的。在网站内查看一个带有 swift bug 的新示例。
    猜你喜欢
    • 2017-08-09
    • 2011-12-23
    • 1970-01-01
    • 1970-01-01
    • 2020-08-15
    • 2014-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多