How do dictionary default subscripts and nil assignment behave when you group and count, and why is removing inside a for-in over the same array safe?

27.5.2 · L2 · Interview

Predict first

What does this print?

var a = [1, 2, 3]
for x in a {
    if x == 1 { a.append(4) }
    print(x, terminator: " ")
}
print(a)
var e = [1, 2, 3, 4, 5]
for x in e where x % 2 == 1 { e.removeAll { $0 == x } }
print(e)
var groups: [Int: [String]] = [:]
for w in ["x", "yy", "zz", "w"] { groups[w.count, default: []].append(w) }
print(groups.sorted { $0.key < $1.key })
var scores = ["a": 1]
scores["b"] = nil
scores["a", default: 0] += 10
scores["c", default: 0] += 1
print(scores.sorted { $0.key < $1.key })
let lookup = scores["zzz", default: -1]
print(lookup, scores.count)
  • 1 2 3 [1, 2, 3, 4] / [2, 4] / buckets by length / a: 11, c: 1 / -1 2
  • 1 2 3 4 [1, 2, 3, 4] / [2, 4] / buckets / a: 11, c: 1 / -1 3
  • 1 2 3 [1, 2, 3, 4] / [1, 2, 3, 4, 5] / buckets / a: 11, c: 1 / -1 3

Mental model. A for loop reads from a photograph of the list taken at the start; you can scribble on the original and the photograph does not change.

for x in a iterates over a copy of the array taken when the loop starts. Appending to a inside the loop triggers copy-on-write, so the loop still visits only the original three elements while a ends up with four. Likewise removeAll inside a loop over e does not disturb the iteration: it visits 1 to 5 and removes the odd ones, leaving [2, 4]. It is safe, but easy to misread, and it copies the buffer once.

The subscript(key, default:) form returns the default without storing it when you read, and mutates in place when you write through it: groups[n, default: []].append(w) builds buckets with no copy, and scores["a", default: 0] += 10 adds to an existing value. Assigning nil to an absent key does nothing. Dictionary iteration order is unspecified, so the program sorts before printing.

The loop iterates a snapshot; a default read does not insert.

Common traps

  • Expecting the loop to see appended elements. It does not.
  • Relying on dictionary order. It is unspecified and varies between runs.

They might follow up with

  • When would this be a bug? Removing by index while iterating indices shifts positions.

Related questions

  • 1.1.7 What is copy-on-write? Which types use it?
  • 27.5.1 What does copying a struct that holds a class reference, or building an array with Array(repeating:)…

← What does copying a struct that holds a class reference, or building…   Why do count, unicodeScalars, utf16 and utf8 give different numbers… →