swiftpropertiesdidsetproperty-observer

Why does a property observer run when a member of the existing value is changed?


Please consider this Swift code. I have a class which wraps an instance of another class. When I set a property on the held value, the wrapper class's property observer is run.

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

The output of above code is:

In MyWrapperClass didSet
In MyClass didSet
In MyWrapperClass didSet

I know that didSet is called when the value of the variable changes.

So when above code at "Line1" executes I understand that "In MyWrapperClass didSet" is printed, and that is fine.

Next when Line2 executes, I expect "In MyClass didSet" to be printed which correctly happens, but I am not sure why "In MyWrapperClass didSet" is printed, as the property myValue is not changed. Can someone explain why?


Solution

  • Swift needs to treat the mutation of myValue.msgStr as having value semantics; meaning that a property observer on myValue needs to be triggered. This is because:

    1. myValue is a protocol-typed property (which also just happens to be optional). This protocol isn't class-bound, so conforming types could be both value and reference types.

    2. The myStr property requirement has an implicitly mutating setter because of both (1) and the fact that it hasn't been marked nonmutating. Therefore the protocol-typed value may well be mutated on mutating though its myStr requirement.

    Consider that the protocol could have been adopted by a value type:

    struct S : MyProtocol {
      var msgStr: String?
    }
    

    In which case a mutation of msgStr is semantically equivalent to re-assigning an S value with the mutated value of msgStr back to myValue (see this Q&A for more info).

    Or a default implementation could have re-assigned to 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")
        }
      }
    }
    

    In which case the mutation of myValue.myStr re-assigns a completely new instance to myValue.

    If MyProtocol had been class-bound:

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

    or if the msgStr requirement had specified that the setter must be non-mutating:

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

    then Swift would treat the mutation of myValue.msgStr as having reference semantics; that is, a property observer on myValue won't get triggered.

    This is because Swift knows that the property value cannot change:

    1. In the first case, only classes can conform, and property setters on classes cannot mutate self (as this is an immutable reference to the instance).

    2. In the second case, the msgStr requirement can only either be satisfied by a property in a class (and such properties don't mutate the reference) or by a computed property in a value type where the setter is non-mutating (and must therefore have reference semantics).

    Alternatively, if myValue had just been typed as MyClass!, you would also get reference semantics because Swift knows you're dealing with a class:

    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