Suppose you have an array, and you want to do something with the elements one by one. As you proceed, sometimes you will notice that the current element should actually be removed from the array.
What’s the best way to achieve this?
I noticed on Apple’s SpriteKit documentation page for Disconnecting Bodies from Joints, there is a code example which reads:
SKPhysicsJoint code example
var springs: [SKPhysicsJointSpring] = []
let maxSpringDistance: CGFloat = 31
// Each `scene.physicsWorld.add(spring)` is accompanied by `springs.append(spring)`
func update(_ currentTime: TimeInterval, for scene: SKScene) {
for spring in springs {
if let bodyAPosition = spring.bodyA.node?.position,
let bodyBPosition = spring.bodyB.node?.position {
let distance = hypot(bodyAPosition.x - bodyBPosition.x,
bodyAPosition.y - bodyBPosition.y)
if distance > maxSpringDistance {
scene.physicsWorld.remove(spring)
if let index = springs.index(of: spring) {
springs.remove(at: index)
}
}
}
}}
If we simplify that down, we get a smaller example like so:
var list: [Foo] = ...
func bar() {
for x in list {
doSomething(x)
if shouldRemove(x) {
if let i = list.firstIndex(of: x) {
list.remove(at: i)
}
}
}
}
Now there are a couple of problems with that code. First, the use of list.firstIndex makes the entire operation accidentally quadratic. (Suppose the first half of the list stays and the last half goes. Then there will be n/2 calls to firstIndex, which each walk n/2 steps to find x.)
Second, and perhaps less obvious, CoW triggers a full copy of the entire list array, the first time remove is called inside the for loop.
So my question is, what’s the best way to write this type of code, so it will run in linear time without triggering CoW? Let’s also say we want the elements to be processed in list order.
The best I could come up with seems…unidiomatic:
var list: [Foo] = ...
func bar() {
list.removeAll {
doSomething($0)
return shouldRemove($0)
}
}
Using removeAll(where:) to perform side effects feels weird, especially since it relies on the closure being called exactly once for each element in iteration order. That is an implementation detail of the standard library which happens to hold today, but is not documented so it shouldn’t be relied upon.
In order to avoid relying on such undocumented details, we can instead iterate an index manually:
var list: [Foo] = ...
func bar() {
var i = list.startIndex
while i < list.endIndex {
let x = list[i]
doSomething(x)
if shouldRemove(x) {
list.remove(at: i)
} else {
list.formIndex(after: &i)
}
}
}
That works correctly for Array (albeit in quadratic time), but if instead we had a generic RangeReplaceableCollection then remove(at:) is documented as potentially invalidating “any existing indices”, meaning in particular i could become invalid.
Is there a better solution?