Итерация for .. in меняющейся коллекции

Я экспериментирую с итерацией массива, используя цикл for .. in ... Мой вопрос связан со случаем, когда коллекция изменяется в теле цикла.

Кажется, что итерация безопасна, даже если список тем временем сокращается. Переменные итерации for последовательно принимают значения (индексов и) элементов, которые были в массиве в начале цикла, несмотря на изменения, сделанные в потоке. Пример:

var slist = [ "AA", "BC", "DE", "FG" ]

for (i, st) in slist.enumerated() {   // for st in slist gives a similar result
    print ("Index \(i): \(st)")
    if st == "AA" {    // at one iteration change completely the list
        print (" --> check 0: \(slist[0]), and 2: \(slist[2])")
        slist.append ("KLM") 
        slist.insert(st+"XX", at:0)   // shift the elements in the array
        slist[2]="bc"                 // replace some elements to come
        print (" --> check again 0: \(slist[0]), and 2: \(slist[2])")
        slist.remove(at:3)
        slist.remove(at:3)
        slist.remove(at:1)            // makes list shorter
    }
}
print (slist)

Это работает очень хорошо, итерация выполняется со значениями [ "AA", "BC", "DE", "FG" ], даже если после первой итерации массив полностью меняется на ["AAXX", "bc", "KLM"]

Я хотел знать, могу ли я безопасно полагаться на такое поведение. К сожалению, в языковом руководстве ничего не говорится об обработке коллекции при изменении коллекции. И в разделе for .. in этот вопрос также не рассматривается. Так:

  1. Могу ли я смело полагаться на гарантию такого поведения итераций, предоставленную в спецификациях языка?
  2. Или мне просто повезло с текущей версией Swift 5.4? В таком случае, есть ли в спецификации языка какая-то подсказка, которую нельзя принимать как должное? И есть ли накладные расходы на производительность для этого поведения итерации (например, некоторая копия) по сравнению с индексированной итерацией?

person Christophe    schedule 08.05.2021    source источник
comment
enumerated возвращает последовательность, так что это то, что вы повторяете, а не массив, который вы изменяете в цикле. Так что это два разных объекта. Если вы, с другой стороны, используете индекс i для доступа к исходному массиву, вы можете привести к сбою, как for (i, st) in slist.enumerated() { slist.remove(at: i) }   -  person Joakim Danielson    schedule 08.05.2021
comment
@JoakimDanielson, это очень хорошее замечание. Однако я начал этот эксперимент без enumerated(), и результат тот же (за исключением того, что у меня нет индекса для отображения). Так что должно быть больше. Это похоже на то, что для итерации будет выполняться копия массива.   -  person Christophe    schedule 08.05.2021
comment
@Christophe, это копирование при записи, вы повторяете исходный массив и изменяете новые копии. Это важная концепция в Swift, но я не знаю ее официального статуса в справочнике по языку.   -  person paiv    schedule 23.05.2021
comment
Отвечает ли это на ваш вопрос? – Удалить элемент из коллекции во время итерации с forEach   -  person Martin R    schedule 26.05.2021
comment
@MartinR Спасибо за эту подсказку. Я посмотрел на соответствующие ответы (я проголосовал за Хармиша и за вас): семантика значений массива - интересное объяснение, которое подтверждено экспериментально. Также интересно видеть, что есть два случая: массив и последовательность через итератор. Однако мне все еще не хватает авторитетной ссылки, которая связала бы for…in с семантикой значения (в случае массива). А для случая с итератором мне не ясно, есть ли такая копия итератора в базовой последовательности или есть копия последовательности для чтения.   -  person Christophe    schedule 26.05.2021


Ответы (2)


В документации для IteratorProtocol говорится, что всякий раз, когда вы используете цикл for-in с массивом, установите или любую другую коллекцию или последовательность, вы используете итератор этого типа. Таким образом, мы гарантируем, что цикл for in будет использовать .makeIterator() и .next(), которые чаще всего определяются для Sequence и IteratorProtocol соответственно.

В документации для Sequence говорится, что протокол Sequence не требует соответствия типов в отношении того, будет разрушительно поглощен итерацией. Как следствие, это означает, что итератор для Sequence не требуется для создания копии, и поэтому я не думаю, что изменение последовательности во время ее повторения в целом безопасно.

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

Но большинство типов коллекций в Swift — это struct с семантикой значений или семантикой копирования при записи. Я не совсем уверен, где находится документация для этого, но эта ссылка говорит, что в Swift все Array, String и Dictionary являются типами значений... Вам не нужно делать ничего особенного — например, создавать явную копию — чтобы предотвратить изменение этих данных другим кодом за вашей спиной. В частности, это означает, что для Array .makeIterator() не может содержать ссылку на ваш массив, потому что итератор для Array не должен делать ничего особенного, чтобы предотвратить изменение данных другим кодом (т. е. вашим кодом). держит.

Мы можем исследовать это более подробно. Тип Iterator для Array определен как тип IndexingIterator<Array<Element>>. В документации IndexingIterator говорится, что это реализация итератора по умолчанию для коллекций, поэтому мы можно предположить, что большинство коллекций будут использовать это. Мы можем увидеть в исходный код для IndexingIterator что он содержит копию своей коллекции

@frozen
public struct IndexingIterator<Elements: Collection> {
  @usableFromInline
  internal let _elements: Elements
  @usableFromInline
  internal var _position: Elements.Index

  @inlinable
  @inline(__always)
  /// Creates an iterator over the given collection.
  public /// @testable
  init(_elements: Elements) {
    self._elements = _elements
    self._position = _elements.startIndex
  }
  ...
}

и что по умолчанию .makeIterator() просто создает эту копию.

extension Collection where Iterator == IndexingIterator<Self> {
  /// Returns an iterator over the elements of the collection.
  @inlinable // trivial-implementation
  @inline(__always)
  public __consuming func makeIterator() -> IndexingIterator<Self> {
    return IndexingIterator(_elements: self)
  }
}

Хотя вы можете не доверять этому исходному коду, в документации по развитию библиотеки утверждается, что Атрибут @inlinable — это обещание разработчика библиотеки, что текущее определение функции останется правильным при использовании с будущими версиями библиотеки, а @frozen также означает, что члены IndexingIterator не могут изменяться.

В целом это означает, что любой тип коллекции с семантикой значений и IndexingIterator в качестве его Iterator должен создавать копию при использовании циклов for in (по крайней мере, до следующего разрыва ABI, который должен быть далеко ). Даже тогда я не думаю, что Apple изменит такое поведение.

В заключение

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

Тем не менее, существует достаточно документации, в которой говорится, что цикл for in просто вызывает .makeIterator() и что для любой коллекции с семантикой значений и типом итератора по умолчанию (например, Array) .makeIterator() создает копию, и поэтому на него не может повлиять код внутри цикла. петля. Кроме того, поскольку Array и некоторые другие типы, такие как Set и Dictionary, являются копируемыми при записи, изменение этих коллекций внутри цикла приведет к одноразовому штрафу за копирование, поскольку тело цикла не будет иметь уникальной ссылки на его хранилище (поскольку итератор будет). Это то же самое наказание, что и при изменении коллекции вне цикла, если у вас нет уникальной ссылки на хранилище.

Без этих предположений вам не гарантируется безопасность, но в некоторых случаях она все равно может быть.

Изменить:

Я только что понял, что мы можем создавать некоторые случаи, когда это небезопасно для последовательностей.

import Foundation

/// This is clearly fine and works as expected.
print("Test normal")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x {
        print(i)
    }
}

/// This is also okay. Reassigning `x` does not mutate the reference that the iterator holds.
print("Test reassignment")
for _ in 0...10 {
    var x: NSMutableArray = [0,1,2,3]
    for i in x {
        x = []
        print(i)
    }
}

/// This crashes. The iterator assumes that the last index it used is still valid, but after removing the objects, there are no valid indices.
print("Test removal")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x {
        x.removeAllObjects()
        print(i)
    }
}

/// This also crashes. `.enumerated()` gets a reference to `x` which it expects will not be modified behind its back.
print("Test removal enumerated")
for _ in 0...10 {
    let x: NSMutableArray = [0,1,2,3]
    for i in x.enumerated() {
        x.removeAllObjects()
        print(i)
    }
}

Тот факт, что это NSMutableArray, важен, потому что этот тип имеет ссылочную семантику. Поскольку NSMutableArray соответствует Sequence, мы знаем, что изменение последовательности во время ее повторения небезопасно, даже при использовании .enumerated().

person deaton.dg    schedule 27.05.2021
comment
Большое спасибо за этот хорошо документированный, логически структурированный и исчерпывающий ответ. Ссылки, которые вы предоставили, - это именно те, которые я искал. Могу ли я резюмировать это следующим образом: это хорошо работает со стандартными коллекциями, но это далеко не гарантируется для более общей последовательности; итератор является ключом к этому поведению? - person Christophe; 30.05.2021
comment
Я бы сказал, что это правильно, итератор является ключевым. Если итератор не делает копию, вы не можете безопасно мутировать внутри итерации, но если итератор делает копию (что легко для типов с семантикой значений, но все же может работать для семантики ссылок, я полагаю), мутация внутри итерации Безопасно. - person deaton.dg; 30.05.2021

slist.enumerate() создает новый экземпляр EnumeratedSequence<[String]>

Чтобы создать экземпляр EnumeratedSequence, вызовите enumerated() для последовательности или коллекции. В следующем примере перечисляются элементы массива. ссылка

Если вы удалите .enumerate(), вы получите тот же результат, любой st будет иметь старое значение. Это происходит потому, что for-in loop создает новый экземпляр IndexingIterator<[String]>.

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

О вопросах:

  • Вы сможете удалить все элементы и по-прежнему безопасно выполнять цикл, поскольку для выполнения взаимодействий создается новый экземпляр.
  • Swift использует итератор внутри, чтобы включить for-in, тогда нет накладных расходов для сравнения. Логично предположить, что чем больше массив, тем меньше будет производительность.
person TheOliverDenis    schedule 27.05.2021
comment
Спасибо за этот хорошо документированный ответ. Ссылка, которую вы предоставили, действительно очень полезна. - person Christophe; 30.05.2021