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.