SE-0231 — Optional iteration

This introduces a very interesting addition data point I had missed: ?? [] is a significant performance and correctness trap.

Not because [] creates an array unnecessarily (it doesn't, the empty array is a static singleton in the standard library via a performance hack that gives me heartburn).

It's because when the array isn't nil, the presence of ?? [] affects the type checker in ways you don't expect:

let maybeHugeRange: Range<Int>? = 0..<Int.max

if let hugeRange = maybeHugeRange {
  for i in hugeRange.reversed() {  // fine
    print(i) // prints Int.max
    break    // and then breaks
  }
}

for i in maybeHugeRange?.reversed() ?? [] { // doom occurs here
  print(i) // your app will jetsam without ever reaching here
  break
}

What's happening here? Well, Range is a collection that cannot be created via []. It's a very low-cost collection: it just stores the upper and lower bound, and iteration is just incrementing from one to the other. On bidirectional collections, reversed() just returns a lightweight wrapper over the base collection, that forwards calls on to the base collection but reversing the direction of traversal. So in this case, a ReversedCollection<Range<Int>>, just 16 bytes in size. It is also not creatable via []. All nice and efficient.

So what does maybeHugeRange?.reversed() ?? [] do? The ReversedCollection answer won't type check, because the rhs of ?? can't be one. So instead it falls back to the version on forward-only collections. That returns an array. So now, just because of that ?? [], we are attempting to allocate and fill an array of size Int.max. Which blows up.

Obviously this is an extreme example that crashes. But in a more likely case, if the optional reversed collection is only of modest size but rarely nil, you could be causing huge numbers of array allocations without realizing it.

21 Likes