Удалить элемент из коллекции во время итерации с помощью forEach

Недавно я написал этот код, не задумываясь об этом:

myObject.myCollection.forEach { myObject.removeItem($0) }

где myObject.removeItem(_) удаляет элемент из myObject.myCollection.

Посмотрев на код сейчас, я озадачен тем, почему это работает, - не следует ли мне получать исключение по строкам Collection was mutated while being enumerated? Один и тот же код работает даже при использовании обычного цикла for-in!

Является ли это ожидаемым поведением или мне повезло, что он не сбой?

Ответ 1

Это действительно ожидаемое поведение - и связано с тем, что Array в Swift (как и многие другие коллекции в стандартной библиотеке) является типом значения с семантикой копирования при записи. Это означает, что его базовый буфер (который хранится косвенно) будет скопирован после мутации (и, как оптимизация, только когда на него нет уникальной ссылки).

Когда вы перебираете Sequence (например, массив), будь то с помощью forEach(_:) или стандарта for in, итератор создается из makeIterator() последовательности makeIterator(), а метод next() повторяется применяется для последовательного создания элементов.

Вы можете думать об итерации последовательности так:

let sequence = [1, 2, 3, 4]
var iterator = sequence.makeIterator()

// 'next()' will return the next element, or 'nil' if
//  it has reached the end sequence.
while let element = iterator.next() { 
    // do something with the element
}

В случае Array в качестве итератора используется IndexingIterator который будет перебирать элементы данной коллекции, просто сохраняя эту коллекцию вместе с текущим индексом итерации. Каждый раз, когда вызывается next(), базовая коллекция подписывается индексом, который затем увеличивается, пока не достигнет endIndex (вы можете увидеть его точную реализацию здесь).

Поэтому, когда вы пришли к мутированию массива в цикле, его базовый буфер не имеет уникальных ссылок, так как итератор также имеет на него представление. Это заставляет копию буфера, который затем использует myCollection.

Итак, теперь есть два массива - тот, который перебирается, и тот, который вы мутируете. Любые дальнейшие мутации в цикле не будут вызывать другую копию, пока в буфере myCollection остаются уникальные ссылки.

Следовательно, это означает, что совершенно безопасно изменять коллекцию с семантикой значения при ее перечислении. Перечисление будет повторяться по всей длине коллекции - полностью независимо от того, какие мутации вы делаете, так как они будут сделаны в копии.

Ответ 2

Я задал аналогичный вопрос на форуме разработчиков Apple, и ответ "да, из-за семантики значения массива".

@originaluser2 уже говорил, но я бы поспорил немного иначе: когда myObject.removeItem($0), создается новый массив и сохраняется под именем myObject, но массив, к которому был вызван forEach(), не изменяется.

Вот более простой пример, демонстрирующий эффект:

extension Array {
    func printMe() {
        print(self)
    }
}

var a = [1, 2, 3]
let pm = a.printMe // The instance method as a closure.
a.removeAll() // Modify the variable 'a'.
pm() // Calls the method on the value that it was created with.
// Output: [1, 2, 3]