Mutating a struct secretly performs reassignment?

I have a code:

class A {

var foo: [Val] = []
var bar: [Val] = []

func a() {
    var baz: [Val] {
        get {
            if foo.isEmpty {
                return bar
            }
            return foo
        }
        set {
            if foo.isEmpty {
                bar = newValue
                return
            }
            foo = newValue
        }
    }
    
    
    // ....
    
    let lastVal = baz.removeLast()
}
}

I wrote this, but then I was like:

  • Wait! But because, I am returning a member of self from a getter, this is actually going to create a copy of foo or bar
  • so only the copy will recieve removeLast()
  • so the original array on self will not change

But then I tested the code, and found out that actually foo or bar gets correctly mutated by removeLast()!

Question #1:
Why did it happen? Did this happen because the struct after mutation was assigned from scratch through baz setter to foo or bar? Does this mean that there was a copy of struct Array created as a result of calling mutating func removeLast?

Question #2:
removeLast probably modifies the underlying storage of the array, of course. When it was called, did it cause the backing storage of the array to be copied from scratch because there were 2 references to that underlying storage (self and the copy returned by baz)? If so, does this mean that this approach is inherently less memory-efficient because of extra copying? Would this not happen if I just called removeLast on self member?

baz.removeLast() means: call the getter of baz to produce a value, call removeLast() to mutate this value, call the setter of baz to store the new value back.

Yes, removeLast modifies the array in-place. But before we modify an array in-place, we have to check if its reference count is greater than 1; if so, we copy the array’s buffer first.

Yes, baz.removeLast() requires O(n) time and space when baz is a computed property with a getter and setter. Stored properties and computed properties that provide a _modify accessor can be modified without copying.

5 Likes

Exactly what Slava said.

You may be interested in the yielding coroutine accessors pitch which would formalize _modify into a full-fledged language feature.

5 Likes

This talk from @lukasa has some great information to detect and defend against the "accidental quadratic" problem when performing similar operations.

1 Like