Search code examples
swiftgetter-setter

When is the modify method of a computed property called and what does it do?


Consider the following class definition

class Class1 {
    var property: String {
        get {
            return ""
        }
        set {
            print("set called")
        }
    }
}

If you add breakpoint inside the get block and read property, the execution is paused and you observe that the topmost method in the call stack is Class1.property.getter

Similarly, if you add breakpoint inside the set block and set property, the execution is paused and you observe that the topmost method in the call stack is Class1.property.setter

While debugging a crash, I observed that the topmost method in the call stack was ClassName.computedPropertyName.modify where ClassName and computedPropertyName are placeholders.

Can anyone point out what the modify method does and when it is called?


Solution

  • Like get and set, modify is an accessor. It's a part of the move towards generalised accessors, and is used to obtain a mutable reference to an underlying value using a yield-once coroutine.

    You can actually write modify accessors in today's Swift using the _modify keyword. Though note however that it's not yet an official feature, so any code that explicitly depends on _modify and yield is subject to breaking without notice.

    class C {
      var _property: String = ""
      var property: String {
        get {
          return _property
        }
        _modify {
          yield &_property
        }
      }
    }
    
    let c = C()
    c.property += "hello"
    print(c.property) // hello
    

    Upon mutating c.property, the _modify accessor is called to obtain a mutable reference to some underlying storage. The yield keyword is used in order to transfer control back to the caller with a reference to _property's storage. At this point, the caller can apply arbitrary mutations to the storage, in this case calling +=. Once the mutation has finished, control is transferred back to _modify, at which point it returns.

    Why is the modify accessor useful?

    Simply put, it avoids the copying of values, which can trigger expensive copying operations for copy-on-write types such as String, Array, and Dictionary (I talk about this in more detail here). Mutating c.property through a modify accessor allows the string to be mutated in-place, rather than mutating a temporary copy which is then written back.

    Why does modify use coroutines?

    The use of a coroutine allows a mutable reference to be temporarily handed back to the caller, after which the accessor can then perform additional logic.

    For example:

    class C {
      var _property: String = ""
      var property: String {
        get {
          return _property
        }
        _modify {
          yield &_property
          _property += " world!"
        }
      }
    }
    
    let c = C()
    c.property += "hello"
    print(c.property) // hello world!
    

    which first lets the caller perform its mutations, and then appends " world!" to the end of the string.

    Why is the modify accessor showing up in your code?

    The Swift compiler can implicitly synthesise a modify accessor for mutable properties. For a computed property with a getter and setter, the implementation looks like this:

    class Class1 {
      var property: String {
        get {
          return ""
        }
        set {
          print("set called")
        }
        // What the compiler synthesises:
        _modify {
          var tmp = property.get() // Made up syntax.
          yield &tmp
          property.set(tmp)
        }
      }
    }
    

    The getter is first called in order to get a mutable copy of the value, a reference to this mutable copy is then passed back to the caller, and then the setter is called with the new value.

    The modify accessor is primarily used in this case in order to enable efficient mutation of a property through dynamic dispatch. Consider the following example:

    class C {
      var property = "hello" {
        // What the compiler synthesises:
        _modify {
          yield &property
        }
      }
    }
    
    class D : C {
      override var property: String {
        get { return "goodbye" }
        set { print(newValue) }
        // What the compiler synthesises:
        _modify {
          var tmp = property.get()
          yield &tmp
          property.set(tmp)
        }
      }
    }
    
    func mutateProperty(_ c: C) {
      c.property += "foo"
    }
    

    On mutating c.property, the modify accessor is dynamically dispatched to. If this is an instance of C, this allows a reference to the storage of property to be directly returned to the caller, allowing for an efficient in-place mutation. If this is an instance of D, then calling the modify just has the same effect as calling the getter followed by the setter.

    Why is modify showing up as the topmost call in the stack trace of your crash?

    I would assume this is because the compiler has inlined the implementation of your getter and setter into the modify accessor, therefore meaning that the crash has likely been caused by the implementation of either the getter or setter of your property.